Merge architecture-cleanup epic (jolt-ogib)

Dead code removal, O(n^2) hot-path fixes (transients/queues/deps), wiring the
optimization pass pipeline into compile + build, deterministic seed emission,
splitting the oversized files (20-coll, host-static, records, types lattice),
and restructuring the two worst maintainability smells: the str-render /
instance-check set!-override chains became registries, and the type-inference
walk now threads an immutable env instead of ~14 module atoms.

Adds an inference gate (make infer, 26 cases) so the type pass — which the
corpus/unit gates don't exercise — has real coverage. Full gate green:
self-host fixpoint holds, corpus 2735 (0 new divergences).
This commit is contained in:
Yogthos 2026-06-23 09:38:53 -04:00
commit 91eed2b622
47 changed files with 3195 additions and 3191 deletions

View file

@ -4,7 +4,7 @@
# build step. `make test` is the full gate. `make remint` rebuilds the seed after a
# source change.
.PHONY: test ci values corpus unit smoke buildsmoke selfhost sci certify ffi transient remint
.PHONY: test ci values corpus unit smoke buildsmoke selfhost sci certify ffi transient infer remint
# Full gate (dev machine). Includes the self-host byte-fixpoint, which only holds
# on the same Chez that minted the seed.
@ -15,7 +15,7 @@ test: selfhost ci
# lockfile) — it RUNS correctly on any Chez, but `selfhost` rebuilds it and a
# different Chez version may emit byte-different (gensym/order) output, so the
# byte-fixpoint is a dev-machine check, not a CI one (jolt-8479).
ci: values corpus unit smoke buildsmoke sci ffi transient certify
ci: values corpus unit smoke buildsmoke sci ffi transient infer certify
@echo "OK: CI gates passed"
# Self-host fixpoint: bootstrap.ss rebuild == checked-in seed.
@ -55,6 +55,12 @@ ffi:
transient:
@chez --script test/chez/transient-test.ss
# Inference / success-type checking: drive jolt.passes.types directly and assert
# diagnostic counts + collected calls/escapes (the optimization pass the other
# gates don't exercise).
infer:
@chez --script host/chez/run-infer.ss
# JVM oracle: certify the corpus against reference Clojure. Skips if clojure absent.
certify:
@if command -v clojure >/dev/null 2>&1; then \

13
ffi_smoke.clj Normal file
View file

@ -0,0 +1,13 @@
(ns ffi-smoke
(:require [jolt.ffi :as ffi]))
(ffi/load-library "libglib-2.0.dylib")
(prn :sizeof-pointer (ffi/sizeof :pointer))
(prn :null (ffi/null))
(prn :null? (ffi/null? (ffi/null)))
(ffi/defcfn g-get-monotonic-time "g_get_monotonic_time" [] :int64)
(prn :monotonic-time (g-get-monotonic-time))
(let [p (ffi/alloc 16)]
(ffi/write p :int64 0 42)
(prn :wrote-and-read (ffi/read p :int64 0))
(ffi/free p))
(prn :done)

View file

@ -21,20 +21,39 @@
(define (jolt-async-sliding-buffer n) (make-async-buffer n 'sliding))
;; --- channels ---------------------------------------------------------------
;; items: a FIFO list of (value . box); box is #f for a buffered value or a 1-slot
;; vector for an unbuffered rendezvous put (set #t when taken, waking the putter).
;; items: an amortized-O(1) FIFO held as a mutable #(out in len) — `out` is the
;; front (pop from its head), `in` holds pushed entries reversed onto it, `len` is
;; the count (an append-to-a-list FIFO is O(n) per push and O(n) to measure).
;; Each entry is (value . box); box is #f for a buffered value or a 1-slot vector
;; for an unbuffered rendezvous put (set #t when taken, waking the putter).
;; cap 0 + kind 'unbuffered = rendezvous; cap>0 with kind fixed/dropping/sliding.
(define-record-type async-chan
(fields mu cv (mutable items) cap kind (mutable closed?) (mutable xrf))
(nongenerative async-chan-v1))
(define (ac-qlen ch) (length (async-chan-items ch)))
(define (ac-qempty? ch) (null? (async-chan-items ch)))
(define (ac-qpush! ch entry) (async-chan-items-set! ch (append (async-chan-items ch) (list entry))))
(define (ac-qnew) (vector '() '() 0))
(define (ac-qlen ch) (vector-ref (async-chan-items ch) 2))
(define (ac-qempty? ch) (fx=? 0 (vector-ref (async-chan-items ch) 2)))
(define (ac-qpush! ch entry)
(let ((q (async-chan-items ch)))
(vector-set! q 1 (cons entry (vector-ref q 1)))
(vector-set! q 2 (fx+ 1 (vector-ref q 2)))))
(define (ac-qfront! q) ; ensure `out` is non-empty: out := reverse in
(when (null? (vector-ref q 0))
(vector-set! q 0 (reverse (vector-ref q 1)))
(vector-set! q 1 '())))
(define (ac-qpop! ch)
(let ((e (car (async-chan-items ch))))
(async-chan-items-set! ch (cdr (async-chan-items ch))) e))
(define (ac-qdrop-oldest! ch) (async-chan-items-set! ch (cdr (async-chan-items ch))))
(let ((q (async-chan-items ch)))
(ac-qfront! q)
(let ((out (vector-ref q 0)))
(vector-set! q 0 (cdr out))
(vector-set! q 2 (fx- (vector-ref q 2) 1))
(car out))))
(define (ac-qdrop-oldest! ch)
(let ((q (async-chan-items ch)))
(ac-qfront! q)
(vector-set! q 0 (cdr (vector-ref q 0)))
(vector-set! q 2 (fx- (vector-ref q 2) 1))))
;; enqueue honoring the buffer kind (used by the transducer step + buffered puts).
(define (ac-buf-give! ch v)
@ -54,7 +73,7 @@
((null? (cdr args)) (car args)) ; completion
(else (ac-buf-give! ch (cadr args)) (car args))))) ; step
(define (ac-make cap kind xrf) (make-async-chan (make-mutex) (make-condition) '() cap kind #f xrf))
(define (ac-make cap kind xrf) (make-async-chan (make-mutex) (make-condition) (ac-qnew) cap kind #f xrf))
;; (chan) | (chan n) | (chan buf) | (chan n|buf xform)
(define (jolt-async-chan . args)
@ -133,11 +152,19 @@
(else ac-poll-empty))))
;; (alts! [ch ...]) — take from whichever channel is ready first; returns
;; [value channel] (value nil if that channel closed). Take-only.
;; [value channel] (value nil if that channel closed). Take-only: every port must
;; be a channel — put specs [ch val] and the :default option are not supported, so
;; reject them with a clear error instead of crashing inside ac-poll!.
;; Polls with a 1ms backoff — no cross-channel wait-set yet.
(define ac-1ms (make-time 'time-duration 1000000 0))
(define (jolt-async-alts chans)
(let ((cs (seq->list (jolt-seq chans))))
(for-each (lambda (c)
(unless (async-chan? c)
(jolt-throw (jolt-ex-info
"alts! supports channel ports only (put specs [ch val] and :default are not supported)"
(jolt-hash-map)))))
cs)
(let loop ()
(let try ((rest cs))
(if (null? rest)

View file

@ -68,8 +68,7 @@
(else (%bd-jolt=2 a b)))))
;; str drops the M; pr/pr-str keep it.
(define %bd-str-render jolt-str-render-one)
(set! jolt-str-render-one (lambda (x) (if (jbigdec? x) (jbigdec->string x) (%bd-str-render x))))
(register-str-render! jbigdec? jbigdec->string)
(define %bd-pr-str jolt-pr-str)
(set! jolt-pr-str (lambda (x) (if (jbigdec? x) (string-append (jbigdec->string x) "M") (%bd-pr-str x))))
(define %bd-pr-readable jolt-pr-readable)

View file

@ -27,9 +27,12 @@
(let ((l (get-line in)))
(if (eof-object? l)
(begin (close-port in)
(let ((s (apply string-append (reverse acc))))
;; trim a trailing newline-equivalent (we joined without them)
s))
;; rejoin with newlines (get-line stripped them). Callers use
;; single-line output; this just avoids silently concatenating
;; two lines into one corrupt token if a command emits more.
(let ((ls (reverse acc)))
(if (null? ls) ""
(fold-left (lambda (s x) (string-append s "\n" x)) (car ls) (cdr ls)))))
(loop (cons l acc)))))))
(define (bld-system cmd)
@ -138,36 +141,16 @@
(for-each (lambda (l) (bld-inline-line l out 0)) bld-runtime-manifest))
;; --- app emission -----------------------------------------------------------
;; Re-emit one app namespace to a list of Scheme strings. Like emit-image's
;; ei-emit-ns but WITHOUT the silent (guard ...) wrapper — a form that fails to
;; emit must fail the build, not vanish.
(define (bld-emit-ns ns-name src)
(let loop ((forms (ei-read-all src)) (acc '()))
(if (null? forms)
(reverse acc)
(let ((f (car forms)))
(ce-scan-requires! f ns-name)
(cond
((ei-ns-form? f) (loop (cdr forms) acc))
((ce-macro-form? f)
(let-values (((nm fn-form) (ce-defmacro->fn f)))
(let ((scm (let ((ctx (make-analyze-ctx ns-name)))
(jolt-ce-emit (jolt-ce-analyze ctx fn-form)))))
(loop (cdr forms)
(cons (string-append
"(def-var! " (ei-str-lit ns-name) " " (ei-str-lit nm) "\n "
scm ")\n(mark-macro! "
(ei-str-lit ns-name) " " (ei-str-lit nm) ")")
acc)))))
(else
(let* ((ctx (make-analyze-ctx ns-name))
(scm (jolt-ce-emit (jolt-ce-analyze ctx f))))
(loop (cdr forms) (cons scm acc)))))))))
;; Re-emit one app namespace to a list of Scheme strings: optimize (run-passes)
;; and stay strict — a form that fails to emit must fail the build, not vanish.
;; The loop itself is emit-image's ei-emit-ns* (optimize? #t, guard? #f).
(define (bld-emit-ns ns-name src) (ei-emit-ns* ns-name src #t #f))
;; --- the build --------------------------------------------------------------
;; entry-ns: the app's main namespace (a string). out-path: the binary to write.
;; mode: "dev" | "release" | "optimized" (recorded; optimization passes wired in a
;; later stage). Deps + source roots are already applied by the caller.
;; mode: "dev" | "release" | "optimized". Every form runs through jolt.passes/
;; run-passes (const-fold always; inline + type inference when optimized turns on
;; direct-linking). Deps + source roots are already applied by the caller.
(define (build-binary entry-ns out-path mode)
(bld-check-toolchain)
;; 1. record app namespaces in dependency order as they finish loading.
@ -181,9 +164,7 @@
(error 'jolt-build (string-append "no source namespace loaded for " entry-ns
" — is it on the source roots?")))
;; 2. emit each app namespace. `optimized` turns on the inference + flatten
;; + scalar-replace passes (closed world). release/dev stay on proven
;; var-deref codegen until those passes are validated on Chez — they were
;; dormant before `jolt build` (inline-enabled? was hardwired off).
;; + scalar-replace passes (closed world); release/dev get const-fold only.
(set-optimize! (string=? mode "optimized"))
(let* ((app-strs (apply append
(map (lambda (nf) (bld-emit-ns (car nf) (read-file-string (cdr nf))))

View file

@ -243,7 +243,8 @@
(cond ((pvec? coll) (let ((v (pvec-v coll)))
(if (and (fx>=? i 0) (fx<? i (vector-length v))) (vector-ref v i)
(error 'nth "index out of bounds"))))
((string? coll) (string-ref coll i))
((string? coll) (if (and (fx>=? i 0) (fx<? i (string-length coll))) (string-ref coll i)
(error 'nth "index out of bounds")))
((or (cseq? coll) (empty-list-t? coll)) (seq-nth coll i #f jolt-nil))
(else (error 'nth "unsupported collection")))))
((coll i d)

View file

@ -11,6 +11,10 @@
(define jolt-ce-analyze (var-deref "jolt.analyzer" "analyze"))
(define jolt-ce-emit (var-deref "jolt.backend-scheme" "emit"))
;; jolt.passes/run-passes: const-fold every analyzed form, plus inline + type
;; inference when the unit opted into direct-linking (jolt build --opt). Off that
;; path it is a pure const-fold. Loaded from the compiler image (jolt.passes).
(define jolt-ce-run-passes (var-deref "jolt.passes" "run-passes"))
(define jolt-ce-read (var-deref "clojure.core" "read-string"))
;; The spine ALWAYS runs with the full clojure.core prelude loaded, so a clojure.*
@ -59,13 +63,9 @@
(define (jolt-analyze-emit-form form ns)
(ce-scan-requires! form ns)
(let* ((ctx (make-analyze-ctx ns))
(ir (jolt-ce-analyze ctx form)))
(ir (jolt-ce-run-passes (jolt-ce-analyze ctx form) ctx)))
(jolt-ce-emit ir)))
;; Source string -> Scheme source string (read then analyze -> emit, all on Chez).
(define (jolt-analyze-emit src ns)
(jolt-analyze-emit-form (jolt-ce-read src) ns))
;; --- runtime defmacro -------------------------------------------------------
;; Shared with emit-image.ss (loaded after this). A defmacro lowers to a def of
;; its expander fn + a macro flag, exactly as the prelude emits build-time macros.

View file

@ -96,7 +96,6 @@
#t)))))
cancelled))
(define (jolt-future-done?* f) (and (jolt-future? f) (jolt-future-done? f)))
(define (jolt-native-future-done? x)
(if (jolt-future? x) (jolt-future-done? x)
(jolt-throw (jolt-ex-info "future-done? requires a future" (jolt-hash-map)))))
@ -158,22 +157,34 @@
(let loop ((o opts) (validator jolt-nil))
(cond
((or (null? o) (null? (cdr o)))
(make-jolt-agent state jolt-nil validator '() #f (make-mutex) (make-condition)))
(make-jolt-agent state jolt-nil validator (vector '() '()) #f (make-mutex) (make-condition)))
((and (keyword-t? (car o)) (string=? (keyword-t-name (car o)) "validator"))
(loop (cddr o) (cadr o)))
(else (loop (cddr o) validator)))))
;; The action queue is an amortized-O(1) FIFO held as a mutable #(out in): `out` is
;; the front, `in` holds sends reversed onto it (an append-to-a-list send was O(n)).
;; All three helpers run under the agent mutex.
(define (jagent-q-empty? a)
(let ((q (jolt-agent-queue a))) (and (null? (vector-ref q 0)) (null? (vector-ref q 1)))))
(define (jagent-q-push! a entry)
(let ((q (jolt-agent-queue a))) (vector-set! q 1 (cons entry (vector-ref q 1)))))
(define (jagent-q-pop! a)
(let ((q (jolt-agent-queue a)))
(when (null? (vector-ref q 0))
(vector-set! q 0 (reverse (vector-ref q 1))) (vector-set! q 1 '()))
(let ((out (vector-ref q 0))) (vector-set! q 0 (cdr out)) (car out))))
;; Drain the queue, applying each action (f state arg*) outside the lock (an action
;; may send/deref the same agent). A validator rejection or a thrown action puts the
;; agent in an error state and halts the queue (JVM :fail mode).
(define (jolt-agent-worker a)
(let loop ()
(let ((act (with-mutex (jolt-agent-mu a)
(if (or (not (jolt-nil? (jolt-agent-err a))) (null? (jolt-agent-queue a)))
(if (or (not (jolt-nil? (jolt-agent-err a))) (jagent-q-empty? a))
(begin (jolt-agent-running?-set! a #f)
(condition-broadcast (jolt-agent-cv a)) #f)
(let ((x (car (jolt-agent-queue a))))
(jolt-agent-queue-set! a (cdr (jolt-agent-queue a))) x)))))
(jagent-q-pop! a)))))
(when act
(guard (e (#t (with-mutex (jolt-agent-mu a)
(jolt-agent-err-set! a e)
@ -190,7 +201,7 @@
;; the JVM's fixed/cached pool split.)
(define (jolt-agent-send a f . args)
(with-mutex (jolt-agent-mu a)
(jolt-agent-queue-set! a (append (jolt-agent-queue a) (list (cons f args))))
(jagent-q-push! a (cons f args))
(unless (jolt-agent-running? a)
(jolt-agent-running?-set! a #t)
(fork-thread (lambda () (jolt-agent-worker a)))))
@ -202,7 +213,7 @@
(lambda (a)
(with-mutex (jolt-agent-mu a)
(let loop ()
(when (or (jolt-agent-running? a) (pair? (jolt-agent-queue a)))
(when (or (jolt-agent-running? a) (not (jagent-q-empty? a)))
(condition-wait (jolt-agent-cv a) (jolt-agent-mu a)) (loop)))))
agents)
jolt-nil)

View file

@ -3,8 +3,19 @@
;; the printer. int/long truncate toward zero to an exact integer; compare returns
;; an exact -1/0/1; double yields a flonum.
;; str: nil -> "", string raw, char bare (not \c), regex -> raw source, else the
;; printer (which renders collections with readable elements).
;; str rendering for the value types not handled by the fast arms below. A host
;; shim loaded later (records, host-table, inst-time, …) registers an arm with
;; register-str-render! instead of set!-wrapping jolt-str-render-one — the arms
;; are type-disjoint, so the full behavior is the base arms here plus the
;; registry, gathered in one place rather than scattered across a set! chain.
;; Newest registration is checked first (matches the old outermost-wins order).
(define str-render-registry '()) ; list of (pred . render), checked front-to-back
(define (register-str-render! pred render)
(set! str-render-registry (cons (cons pred render) str-render-registry)))
;; str: nil -> "", string raw, char bare (not \c), regex -> raw source, a
;; registered host type via its arm, else the printer (which renders collections
;; with readable elements).
(define (jolt-str-render-one v)
(cond
((jolt-nil? v) "")
@ -16,7 +27,12 @@
((and (flonum? v) (fl= v +inf.0)) "Infinity")
((and (flonum? v) (fl= v -inf.0)) "-Infinity")
((and (flonum? v) (not (fl= v v))) "NaN")
(else (jolt-pr-str v))))
(else
(let loop ((rs str-render-registry))
(cond
((null? rs) (jolt-pr-str v))
(((caar rs) v) ((cdar rs) v))
(else (loop (cdr rs))))))))
(define (jolt-str . xs)
(let loop ((xs xs) (acc '()))
(if (null? xs)

View file

@ -35,7 +35,27 @@
;; + alias tables, not ctx-accumulated state, so this matches the spine's per-form
;; analyze. A defmacro emits its expander fn as (def-var! ns name <fn>) +
;; (mark-macro! ns name) so the on-Chez analyzer can expand it.
(define (ei-emit-ns ns-name src)
;; Analyze -> (optionally run passes) -> emit one form. optimize? runs
;; jolt.passes/run-passes (build optimizes; the seed minter stays un-optimized so
;; the self-host fixpoint is independent of the passes).
(define (ei-compile-form ctx f optimize?)
(let ((ir (jolt-ce-analyze ctx f)))
(jolt-ce-emit (if optimize? (jolt-ce-run-passes ir ctx) ir))))
;; The emitted `(def-var! …)(mark-macro! …)` pair for a defmacro, guard-wrapped
;; (tolerant) or bare (strict) to match guard?.
(define (ei-macro-string ns-name nm scm guard?)
(if guard?
(string-append "(guard (e (#t #f))\n (def-var! " (ei-str-lit ns-name) " " (ei-str-lit nm)
"\n " scm ")\n (mark-macro! " (ei-str-lit ns-name) " " (ei-str-lit nm) "))")
(string-append "(def-var! " (ei-str-lit ns-name) " " (ei-str-lit nm) "\n " scm
")\n(mark-macro! " (ei-str-lit ns-name) " " (ei-str-lit nm) ")")))
;; Cross-compile one namespace's source to a list of Scheme strings — shared by
;; the seed minter (ei-emit-ns: optimize? #f, guard? #t — tolerant, skips a form
;; that fails to emit) and `jolt build` (bld-emit-ns: optimize? #t, guard? #f —
;; strict, a failing form errors the build).
(define (ei-emit-ns* ns-name src optimize? guard?)
(let loop ((forms (ei-read-all src)) (acc '()))
(if (null? forms)
(reverse acc)
@ -45,35 +65,38 @@
((ei-ns-form? f) (loop (cdr forms) acc))
((ce-macro-form? f)
(let-values (((nm fn-form) (ce-defmacro->fn f)))
(let ((scm (guard (e (#t #f))
(let ((ctx (make-analyze-ctx ns-name)))
(jolt-ce-emit (jolt-ce-analyze ctx fn-form))))))
(let ((scm (if guard?
(guard (e (#t #f)) (ei-compile-form (make-analyze-ctx ns-name) fn-form optimize?))
(ei-compile-form (make-analyze-ctx ns-name) fn-form optimize?))))
(loop (cdr forms)
(if scm
(cons (string-append
"(guard (e (#t #f))\n (def-var! "
(ei-str-lit ns-name) " " (ei-str-lit nm) "\n "
scm ")\n (mark-macro! "
(ei-str-lit ns-name) " " (ei-str-lit nm) "))")
acc)
acc)))))
(if (and guard? (not scm)) acc
(cons (ei-macro-string ns-name nm scm guard?) acc))))))
(else
(let* ((ctx (make-analyze-ctx ns-name))
(scm (guard (e (#t #f)) (jolt-ce-emit (jolt-ce-analyze ctx f)))))
(let ((scm (if guard?
(guard (e (#t #f)) (ei-compile-form (make-analyze-ctx ns-name) f optimize?))
(ei-compile-form (make-analyze-ctx ns-name) f optimize?))))
(loop (cdr forms)
(if scm
(cons (string-append "(guard (e (#t #f))\n " scm ")") acc)
acc)))))))))
(if (and guard? (not scm)) acc
(cons (if guard? (string-append "(guard (e (#t #f))\n " scm ")") scm) acc))))))))))
(define (ei-emit-ns ns-name src) (ei-emit-ns* ns-name src #f #t))
;; Scheme string literal for a ns/name — uses the runtime's own writer
;; (printable ASCII identifiers only here).
(define (ei-str-lit s) (with-output-to-string (lambda () (write s))))
;; The compiler namespaces, in load order.
;; The compiler namespaces, in load order. The passes (fold/inline/types + the
;; jolt.passes façade) load after ir so run-passes is available to the back end;
;; fold/inline/types come before the façade that :refers them.
(define ei-compiler-ns-files
(list (cons "jolt.ir" "jolt-core/jolt/ir.clj")
(cons "jolt.analyzer" "jolt-core/jolt/analyzer.clj")
(cons "jolt.backend-scheme" "jolt-core/jolt/backend_scheme.clj")))
(cons "jolt.backend-scheme" "jolt-core/jolt/backend_scheme.clj")
(cons "jolt.passes.fold" "jolt-core/jolt/passes/fold.clj")
(cons "jolt.passes.inline" "jolt-core/jolt/passes/inline.clj")
(cons "jolt.passes.types.lattice" "jolt-core/jolt/passes/types/lattice.clj")
(cons "jolt.passes.types" "jolt-core/jolt/passes/types.clj")
(cons "jolt.passes" "jolt-core/jolt/passes.clj")))
;; The clojure.core tiers + stdlib namespaces, in load order.
;; Re-emitting these on Chez is the
@ -81,7 +104,7 @@
(define ei-prelude-ns-files
(append
(map (lambda (tf) (cons "clojure.core" (string-append "jolt-core/clojure/core/" tf ".clj")))
'("00-syntax" "00-kernel" "10-seq" "20-coll" "25-sorted" "30-macros" "40-lazy" "50-io"))
'("00-syntax" "00-kernel" "10-seq" "20-coll" "21-coll" "22-coll" "25-sorted" "30-macros" "40-lazy" "50-io"))
(list (cons "clojure.string" "stdlib/clojure/string.clj")
(cons "clojure.walk" "stdlib/clojure/walk.clj")
(cons "clojure.template" "stdlib/clojure/template.clj")

View file

@ -0,0 +1,502 @@
;; host-static-objects.ss — host object classes (ArrayList, HashMap, the
;; String/Reader/Writer/Tokenizer shims, BigInteger/MapEntry ctors, URL codecs)
;; and the tagged-table method dispatch + pluggable instance? hook. Continues
;; host-static-statics.ss; loaded last of the three.
;; ---- java.util.ArrayList ----------------------------------------------------
;; A mutable list backed by a growable Scheme vector. State is #(backing count);
;; .add amortizes O(1) and .get is O(1) (a list backing made both O(n)). medley's
;; stateful transducers (window / partition-between) build one with .add / .size /
;; .toArray / .clear / .remove. (ArrayList.) | (ArrayList. n) | (ArrayList. coll).
(define al-min-cap 8)
(define (al-vec self) (vector-ref (jhost-state self) 0))
(define (al-cnt self) (vector-ref (jhost-state self) 1))
(define (al-cnt! self n) (vector-set! (jhost-state self) 1 n))
(define (make-arraylist xs) ; xs: a Scheme list of initial elements
(let* ((n (length xs)) (cap (fxmax al-min-cap n)) (v (make-vector cap jolt-nil)))
(let loop ((i 0) (xs xs)) (when (pair? xs) (vector-set! v i (car xs)) (loop (fx+ i 1) (cdr xs))))
(make-jhost "arraylist" (vector v n))))
(define (al-ensure! self need) ; grow the backing vector (doubling) to fit `need`
(let ((v (al-vec self)))
(when (fx>? need (vector-length v))
(let grow ((cap (fxmax al-min-cap (vector-length v))))
(if (fx<? cap need) (grow (fx* cap 2))
(let ((nv (make-vector cap jolt-nil)))
(let cp ((i 0)) (when (fx<? i (al-cnt self)) (vector-set! nv i (vector-ref v i)) (cp (fx+ i 1))))
(vector-set! (jhost-state self) 0 nv)))))))
(define (al-push! self x)
(let ((n (al-cnt self))) (al-ensure! self (fx+ n 1)) (vector-set! (al-vec self) n x) (al-cnt! self (fx+ n 1))))
(define (al-insert-at! self i x)
(let ((n (al-cnt self)))
(al-ensure! self (fx+ n 1))
(let ((v (al-vec self)))
(let shift ((j n)) (when (fx>? j i) (vector-set! v j (vector-ref v (fx- j 1))) (shift (fx- j 1))))
(vector-set! v i x) (al-cnt! self (fx+ n 1)))))
(define (al-remove-at! self i)
(let ((n (al-cnt self)) (v (al-vec self)))
(let shift ((j i)) (when (fx<? j (fx- n 1)) (vector-set! v j (vector-ref v (fx+ j 1))) (shift (fx+ j 1))))
(vector-set! v (fx- n 1) jolt-nil) (al-cnt! self (fx- n 1))))
(define (al->list self) ; first `count` elements as a Scheme list
(let ((v (al-vec self)))
(let loop ((i (fx- (al-cnt self) 1)) (acc '())) (if (fx<? i 0) acc (loop (fx- i 1) (cons (vector-ref v i) acc))))))
(register-class-ctor! "ArrayList"
(lambda args
(cond ((null? args) (make-arraylist '()))
((number? (car args)) (make-arraylist '())) ; initial capacity, ignored
(else (make-arraylist (seq->list (jolt-seq (car args))))))))
(register-class-ctor! "java.util.ArrayList"
(lambda args
(cond ((null? args) (make-arraylist '()))
((number? (car args)) (make-arraylist '()))
(else (make-arraylist (seq->list (jolt-seq (car args))))))))
(register-host-methods! "arraylist"
(list
(cons "add" (lambda (self . a)
;; (.add x) -> append+true; (.add i x) -> insert at i, returns nil.
(if (= 1 (length a))
(begin (al-push! self (car a)) #t)
(begin (al-insert-at! self (jnum->exact (car a)) (cadr a)) jolt-nil))))
(cons "add!" (lambda (self x) (al-push! self x) #t))
(cons "get" (lambda (self i) (vector-ref (al-vec self) (jnum->exact i))))
(cons "set" (lambda (self i x)
(let* ((idx (jnum->exact i)) (old (vector-ref (al-vec self) idx)))
(vector-set! (al-vec self) idx x) old)))
(cons "size" (lambda (self) (->num (al-cnt self))))
(cons "isEmpty" (lambda (self) (fx=? 0 (al-cnt self))))
(cons "remove" (lambda (self i)
(let* ((idx (jnum->exact i)) (old (vector-ref (al-vec self) idx)))
(al-remove-at! self idx) old)))
(cons "clear" (lambda (self) (vector-set! (jhost-state self) 0 (make-vector al-min-cap jolt-nil)) (al-cnt! self 0) jolt-nil))
(cons "contains" (lambda (self x) (and (memp (lambda (e) (jolt=2 e x)) (al->list self)) #t)))
(cons "toArray" (lambda (self . _) (apply jolt-vector (al->list self))))
(cons "iterator" (lambda (self) (make-jiterator (list->cseq (al->list self)))))
(cons "toString" (lambda (self) (jolt-pr-str (list->cseq (al->list self)))))))
(register-class-ctor! "StringBuilder"
(lambda args (make-jhost "string-builder"
;; a numeric first arg is a CAPACITY hint, not content.
(vector (if (and (pair? args) (not (number? (car args)))) (render-piece (car args)) "")))))
(register-host-methods! "string-builder"
(list (cons "append" (lambda (self x) (sb-set! self (string-append (sb-str self) (render-piece x))) self))
(cons "toString" (lambda (self) (sb-str self)))
(cons "length" (lambda (self) (->num (string-length (sb-str self)))))
(cons "charAt" (lambda (self i) (string-ref (sb-str self) (jnum->exact i))))
(cons "setLength" (lambda (self n)
(let ((cur (sb-str self)) (n (jnum->exact n)))
(sb-set! self (if (< n (string-length cur))
(substring cur 0 n)
(string-append cur (make-string (- n (string-length cur)) #\nul)))))
jolt-nil))))
;; ---- StringWriter -----------------------------------------------------------
;; Writer.write(int) writes the CHAR for that code; append(char) appends the char.
(define (writer-piece x) (if (number? x) (string (integer->char (jnum->exact x))) (render-piece x)))
(register-class-ctor! "StringWriter" (lambda args (make-jhost "writer" (vector ""))))
(register-host-methods! "writer"
(list (cons "write" (lambda (self x) (sb-set! self (string-append (sb-str self) (writer-piece x))) jolt-nil))
(cons "append" (lambda (self x) (sb-set! self (string-append (sb-str self) (render-piece x))) self))
(cons "flush" (lambda (self) jolt-nil))
(cons "close" (lambda (self) jolt-nil))
(cons "toString" (lambda (self) (sb-str self)))))
;; a file-backed writer (clojure.java.io/writer of a File/path): accumulates like
;; StringWriter, then persists to the path on flush/close, so
;; (with-open [w (io/writer "f")] (.write w …)) writes the file. State #(path buf).
(define (fw-path self) (vector-ref (jhost-state self) 0))
(define (fw-buf self) (vector-ref (jhost-state self) 1))
(define (fw-append! self s) (vector-set! (jhost-state self) 1 (string-append (fw-buf self) s)))
(define (fw-flush! self) (jolt-spit (fw-path self) (fw-buf self))) ; jolt-spit: io.ss
(register-host-methods! "file-writer"
(list (cons "write" (lambda (self x) (fw-append! self (writer-piece x)) jolt-nil))
(cons "append" (lambda (self x) (fw-append! self (render-piece x)) self))
(cons "flush" (lambda (self) (fw-flush! self) jolt-nil))
(cons "close" (lambda (self) (fw-flush! self) jolt-nil))
(cons "toString" (lambda (self) (fw-buf self)))))
;; a writer over a real Chez port — the values *out* / *err* hold. write/append
;; push to the port (so (.write *out* s) and (binding [*out* *err*] …) work);
;; it isn't a buffer, so toString is empty. Lets libraries that touch *out*/*err*
;; (tools.logging, selmer) compile and run.
(register-host-methods! "port-writer"
(list (cons "write" (lambda (self x) (display (writer-piece x) (vector-ref (jhost-state self) 0)) jolt-nil))
(cons "append" (lambda (self x) (display (render-piece x) (vector-ref (jhost-state self) 0)) self))
(cons "flush" (lambda (self) (flush-output-port (vector-ref (jhost-state self) 0)) jolt-nil))
(cons "close" (lambda (self) jolt-nil))
(cons "toString" (lambda (self) ""))))
(def-var! "clojure.core" "*out*" (make-jhost "port-writer" (vector (current-output-port))))
(def-var! "clojure.core" "*err*" (make-jhost "port-writer" (vector (current-error-port))))
;; ---- java.util.HashMap ------------------------------------------------------
;; A mutable map keyed by jolt values (jolt-hash / jolt=2). State #(chez-hashtable).
;; Constructors: () | (capacity) | (capacity load-factor) [sizing args ignored] |
;; (Map m) [copy]. Enough of the Map surface for libraries that build a fast lookup
;; (malli's fast-registry: (doto (HashMap. 1024 0.25) (.putAll m)) then .get).
(define (hm-hash k) (let ((h (jolt-hash k)))
(bitwise-and (if (and (integer? h) (exact? h)) (abs h) 0) #x3FFFFFFF)))
(define (hm-tbl self) (vector-ref (jhost-state self) 0))
(define (hm-hashmap? x) (and (jhost? x) (string=? (jhost-tag x) "hashmap")))
(define (hm-copy-into! ht src) ; src: a jolt map or another hashmap
(if (hm-hashmap? src)
(vector-for-each (lambda (k) (hashtable-set! ht k (hashtable-ref (hm-tbl src) k jolt-nil)))
(hashtable-keys (hm-tbl src)))
(for-each (lambda (e) (hashtable-set! ht (jolt-nth e 0) (jolt-nth e 1)))
(seq->list (jolt-seq src)))))
(register-class-ctor! "HashMap"
(lambda args
(let ((ht (make-hashtable hm-hash jolt=2)))
(when (and (pair? args) (or (pmap? (car args)) (hm-hashmap? (car args))))
(hm-copy-into! ht (car args)))
(make-jhost "hashmap" (vector ht)))))
(define (hm->pmap self)
(let ((m (jolt-hash-map)))
(vector-for-each (lambda (k) (set! m (jolt-assoc m k (hashtable-ref (hm-tbl self) k jolt-nil))))
(hashtable-keys (hm-tbl self)))
m))
(register-host-methods! "hashmap"
(list (cons "put" (lambda (self k v) (let ((old (hashtable-ref (hm-tbl self) k jolt-nil)))
(hashtable-set! (hm-tbl self) k v) old)))
(cons "get" (lambda (self k) (hashtable-ref (hm-tbl self) k jolt-nil)))
(cons "getOrDefault" (lambda (self k d) (hashtable-ref (hm-tbl self) k d)))
(cons "containsKey" (lambda (self k) (if (hashtable-contains? (hm-tbl self) k) #t #f)))
(cons "containsValue" (lambda (self v)
(let ((found #f))
(vector-for-each (lambda (k) (when (jolt=2 v (hashtable-ref (hm-tbl self) k jolt-nil)) (set! found #t)))
(hashtable-keys (hm-tbl self))) found)))
(cons "size" (lambda (self) (hashtable-size (hm-tbl self))))
(cons "isEmpty" (lambda (self) (= 0 (hashtable-size (hm-tbl self)))))
(cons "remove" (lambda (self k) (let ((old (hashtable-ref (hm-tbl self) k jolt-nil)))
(hashtable-delete! (hm-tbl self) k) old)))
(cons "clear" (lambda (self) (hashtable-clear! (hm-tbl self)) jolt-nil))
(cons "putAll" (lambda (self m) (hm-copy-into! (hm-tbl self) m) jolt-nil))
(cons "keySet" (lambda (self) (apply jolt-hash-set (vector->list (hashtable-keys (hm-tbl self))))))
(cons "values" (lambda (self) (apply jolt-vector
(map (lambda (k) (hashtable-ref (hm-tbl self) k jolt-nil))
(vector->list (hashtable-keys (hm-tbl self)))))))
(cons "entrySet" (lambda (self) (jolt-seq (hm->pmap self))))
(cons "toString" (lambda (self) (jolt-pr-str (hm->pmap self))))))
;; ---- StringReader -----------------------------------------------------------
;; state: a vector #(string pos marked).
(register-class-ctor! "StringReader"
;; src is a String or a char[] ((StringReader. (char-array s)) — selmer's parser
;; reads templates this way); a char-array becomes the string of its chars.
(lambda (src . _)
(make-jhost "string-reader"
(vector (cond ((string? src) src)
((jolt-array? src) (apply string-append (map jolt-str-render-one (seq->list (jolt-seq src)))))
(else (jolt-str-render-one src)))
0 0))))
(define (sr-s self) (vector-ref (jhost-state self) 0))
(define (sr-pos self) (vector-ref (jhost-state self) 1))
(define (sr-pos! self p) (vector-set! (jhost-state self) 1 p))
(register-host-methods! "string-reader"
(list (cons "read" (lambda (self)
(let ((s (sr-s self)) (p (sr-pos self)))
(if (>= p (string-length s)) -1 ; EOF -> exact int -1 (= JVM)
(begin (sr-pos! self (+ p 1)) (->num (char->integer (string-ref s p))))))))
(cons "mark" (lambda (self . _) (vector-set! (jhost-state self) 2 (sr-pos self)) jolt-nil))
(cons "reset" (lambda (self) (sr-pos! self (vector-ref (jhost-state self) 2)) jolt-nil))
(cons "skip" (lambda (self n) (let ((n (jnum->exact n)))
(sr-pos! self (min (string-length (sr-s self)) (+ (sr-pos self) n))) (->num n))))
;; readLine: the next line without its terminator (\n or \r\n), nil at EOF —
;; what line-seq drives over a BufferedReader.
(cons "readLine"
(lambda (self)
(let ((s (sr-s self)) (p (sr-pos self)) (len (string-length (sr-s self))))
(if (>= p len) jolt-nil
(let scan ((i p))
(cond
((>= i len) (sr-pos! self len) (substring s p len))
((char=? (string-ref s i) #\newline)
(sr-pos! self (+ i 1))
(substring s p (if (and (> i p) (char=? (string-ref s (- i 1)) #\return)) (- i 1) i)))
(else (scan (+ i 1)))))))))
(cons "close" (lambda (self) jolt-nil))))
;; ---- PushbackReader ---------------------------------------------------------
;; state: a vector #(wrapped-reader pushed-list)
(register-class-ctor! "PushbackReader"
(lambda (rdr . _) (make-jhost "pushback-reader" (vector rdr '()))))
(define (read-unit r) ; read one code unit (flonum) from any reader, -1 at EOF
(record-method-dispatch r "read" jolt-nil))
(register-host-methods! "pushback-reader"
(list (cons "read" (lambda (self)
(let ((pushed (vector-ref (jhost-state self) 1)))
(if (pair? pushed)
(begin (vector-set! (jhost-state self) 1 (cdr pushed)) (car pushed))
(read-unit (vector-ref (jhost-state self) 0))))))
(cons "unread" (lambda (self ch)
(vector-set! (jhost-state self) 1
(cons (if (char? ch) (->num (char->integer ch)) ch) (vector-ref (jhost-state self) 1)))
jolt-nil))
(cons "close" (lambda (self) jolt-nil))))
;; ---- StringTokenizer --------------------------------------------------------
;; state: a vector #(tokens-list pos)
(define (tokenize s delims)
(let ((dset (string->list delims)))
(let loop ((chars (string->list s)) (cur '()) (toks '()))
(cond ((null? chars) (reverse (if (null? cur) toks (cons (list->string (reverse cur)) toks))))
((memv (car chars) dset)
(loop (cdr chars) '() (if (null? cur) toks (cons (list->string (reverse cur)) toks))))
(else (loop (cdr chars) (cons (car chars) cur) toks))))))
(register-class-ctor! "StringTokenizer"
(lambda (s . delims) (make-jhost "string-tokenizer"
(vector (tokenize (if (string? s) s (jolt-str-render-one s))
(if (null? delims) " \t\n\r\f" (car delims))) 0))))
(register-host-methods! "string-tokenizer"
(list (cons "hasMoreTokens" (lambda (self) (< (vector-ref (jhost-state self) 1) (length (vector-ref (jhost-state self) 0)))))
(cons "countTokens" (lambda (self) (->num (- (length (vector-ref (jhost-state self) 0)) (vector-ref (jhost-state self) 1)))))
(cons "nextToken" (lambda (self)
(let ((toks (vector-ref (jhost-state self) 0)) (p (vector-ref (jhost-state self) 1)))
(if (< p (length toks))
(begin (vector-set! (jhost-state self) 1 (+ p 1)) (list-ref toks p))
(error #f "NoSuchElementException")))))))
;; ---- String / BigInteger / MapEntry constructors ----------------------------
;; (String. bytes [charset]) decodes bytes (a bytevector OR a jolt byte-array)
;; with the named charset (UTF-8 default; ISO-8859-1/latin1/ascii = one byte per
;; char); else stringify. clj-http-lite's body coercion is (String. ^[B body cs).
(define (string-charset-name rest)
(if (pair? rest)
(let ((c (car rest)))
(cond ((string? c) c)
((and (jhost? c) (string=? (jhost-tag c) "charset"))
(let ((p (assq 'name (jhost-state c)))) (if p (jolt-str-render-one (cdr p)) "UTF-8")))
(else "UTF-8")))
"UTF-8"))
(define (decode-bytevector bv rest)
(let ((cs (ascii-string-down (string-charset-name rest))))
(cond
((or (string=? cs "utf-8") (string=? cs "utf8")) (utf8->string bv))
((or (string=? cs "iso-8859-1") (string=? cs "latin1") (string=? cs "iso8859-1")
(string=? cs "us-ascii") (string=? cs "ascii"))
(list->string (map integer->char (bytevector->u8-list bv))))
((or (string=? cs "utf-16") (string=? cs "utf16") (string=? cs "utf-16be") (string=? cs "unicode"))
(utf16->string bv (endianness big))) ; respects a leading BOM
((string=? cs "utf-16le") (utf16->string bv (endianness little)))
((or (string=? cs "utf-32") (string=? cs "utf32") (string=? cs "utf-32be"))
(utf32->string bv (endianness big)))
((string=? cs "utf-32le") (utf32->string bv (endianness little)))
(else (guard (e (#t (list->string (map integer->char (bytevector->u8-list bv))))) (utf8->string bv))))))
(register-class-ctor! "String"
(lambda (x . rest)
(cond ((bytevector? x) (decode-bytevector x rest))
((and (jolt-array? x) (eq? (jolt-array-kind x) 'byte)) (decode-bytevector (na-bytearray->bv x) rest))
((string? x) x)
(else (jolt-str-render-one x)))))
(register-class-ctor! "BigInteger"
(lambda (v) (parse-int-or-throw v 10 "BigInteger")))
(register-class-ctor! "MapEntry" (lambda (k v) (make-map-entry k v)))
;; JVM exception ctors -> a typed host throwable carrying the canonical :jolt/class
;; (so class / instance? / getMessage / ex-message reflect the real type) and the
;; message. Supports (E. msg), (E. msg cause), (E. cause), and (E.).
(for-each
(lambda (nm)
(let ((canonical (or (resolve-class-hint nm) nm)))
(register-class-ctor! nm
(lambda args
(let* ((a0 (if (pair? args) (car args) jolt-nil))
(rest (if (pair? args) (cdr args) '()))
(cause (if (pair? rest) (car rest) jolt-nil)))
(cond
((string? a0) (jolt-host-throwable canonical a0 cause))
((jolt-nil? a0) (jolt-host-throwable canonical jolt-nil))
;; (E. cause): a lone throwable arg is the cause, message nil.
((and (null? rest) (ex-info-map? a0)) (jolt-host-throwable canonical jolt-nil a0))
(else (jolt-host-throwable canonical (jolt-str-render-one a0) cause))))))))
'("Throwable" "Exception" "RuntimeException" "IllegalArgumentException" "IllegalStateException"
"InterruptedException" "UnsupportedOperationException" "IOException" "NumberFormatException"
"ArithmeticException" "NullPointerException" "ClassCastException" "IndexOutOfBoundsException"
"FileNotFoundException" "UnsupportedEncodingException"))
;; ---- URLEncoder / URLDecoder (www-form-urlencoded) --------------------------
(define (url-unreserved? b)
(or (and (>= b 48) (<= b 57)) (and (>= b 65) (<= b 90)) (and (>= b 97) (<= b 122))
(= b 46) (= b 42) (= b 95) (= b 45)))
(define hex-digits "0123456789ABCDEF")
(define (url-encode s . _)
(let ((bs (string->utf8 (if (string? s) s (jolt-str-render-one s)))) (out '()))
(let loop ((i 0))
(if (= i (bytevector-length bs)) (list->string (reverse out))
(let ((b (bytevector-u8-ref bs i)))
(cond ((url-unreserved? b) (set! out (cons (integer->char b) out)))
((= b 32) (set! out (cons #\+ out)))
(else (set! out (cons (string-ref hex-digits (bitwise-and b 15))
(cons (string-ref hex-digits (bitwise-arithmetic-shift-right b 4))
(cons #\% out))))))
(loop (+ i 1)))))))
(define (hexv c)
(cond ((and (char<=? #\0 c) (char<=? c #\9)) (- (char->integer c) 48))
((and (char<=? #\A c) (char<=? c #\F)) (- (char->integer c) 55))
((and (char<=? #\a c) (char<=? c #\f)) (- (char->integer c) 87))
(else (error #f "URLDecoder: malformed escape"))))
(define (url-decode s . _)
(let* ((str (if (string? s) s (jolt-str-render-one s))) (n (string-length str)) (out '()))
(let loop ((i 0))
(if (>= i n) (utf8->string (u8-list->bytevector (reverse out)))
(let ((c (string-ref str i)))
(cond ((char=? c #\+) (set! out (cons 32 out)) (loop (+ i 1)))
((char=? c #\%)
(set! out (cons (+ (* 16 (hexv (string-ref str (+ i 1)))) (hexv (string-ref str (+ i 2)))) out))
(loop (+ i 3)))
(else (set! out (cons (char->integer c) out)) (loop (+ i 1)))))))))
(define (u8-list->bytevector lst)
(let ((bv (make-bytevector (length lst))))
(let loop ((l lst) (i 0)) (if (null? l) bv (begin (bytevector-u8-set! bv i (car l)) (loop (cdr l) (+ i 1)))))))
(register-class-statics! "URLEncoder" (list (cons "encode" url-encode)))
(register-class-statics! "URLDecoder" (list (cons "decode" url-decode)))
;; Charset/forName yields the canonical name STRING (not an opaque object) so it
;; threads straight into (.getBytes s cs) / (String. bytes cs), which take a name.
(register-class-statics! "Charset" (list (cons "forName" (lambda (nm) (jolt-str-render-one nm)))))
;; ---- Base64 (RFC 4648) ------------------------------------------------------
(define b64-alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
(define (->bytevector x)
(cond ((bytevector? x) x)
((and (jolt-array? x) (eq? (jolt-array-kind x) 'byte)) (na-bytearray->bv x))
((string? x) (string->utf8 x))
(else (string->utf8 (jolt-str-render-one x)))))
(define (b64-encode x)
(let* ((bs (->bytevector x)) (n (bytevector-length bs)) (out '()))
(let loop ((i 0))
(if (>= i n) (list->string (reverse out))
(let* ((b0 (bytevector-u8-ref bs i))
(b1 (if (< (+ i 1) n) (bytevector-u8-ref bs (+ i 1)) #f))
(b2 (if (< (+ i 2) n) (bytevector-u8-ref bs (+ i 2)) #f)))
(set! out (cons (string-ref b64-alphabet (bitwise-arithmetic-shift-right b0 2)) out))
(set! out (cons (string-ref b64-alphabet (bitwise-ior (bitwise-arithmetic-shift-left (bitwise-and b0 3) 4)
(bitwise-arithmetic-shift-right (or b1 0) 4))) out))
(set! out (cons (if b1 (string-ref b64-alphabet (bitwise-ior (bitwise-arithmetic-shift-left (bitwise-and b1 15) 2)
(bitwise-arithmetic-shift-right (or b2 0) 6))) #\=) out))
(set! out (cons (if b2 (string-ref b64-alphabet (bitwise-and b2 63)) #\=) out))
(loop (+ i 3)))))))
(define (b64-char-val c)
(let loop ((i 0)) (cond ((= i 64) (error #f "Base64: illegal character")) ((char=? (string-ref b64-alphabet i) c) i) (else (loop (+ i 1))))))
(define (b64-decode x)
(let* ((str (let ((s (if (string? x) x (utf8->string (->bytevector x)))))
(list->string (filter (lambda (c) (not (char=? c #\=))) (string->list s)))))
(out '()) (acc 0) (bits 0))
(for-each (lambda (c)
(set! acc (bitwise-ior (bitwise-arithmetic-shift-left acc 6) (b64-char-val c)))
(set! bits (+ bits 6))
(when (>= bits 8)
(set! bits (- bits 8))
(set! out (cons (bitwise-and (bitwise-arithmetic-shift-right acc bits) 255) out))))
(string->list str))
(u8-list->bytevector (reverse out))))
(register-host-methods! "b64-encoder"
(list (cons "encode" (lambda (self bs) (string->utf8 (b64-encode bs))))
(cons "encodeToString" (lambda (self bs) (b64-encode bs)))))
(register-host-methods! "b64-decoder"
(list (cons "decode" (lambda (self s) (b64-decode s)))))
(register-class-statics! "Base64"
(list (cons "getEncoder" (lambda () (make-jhost "b64-encoder" '())))
(cons "getDecoder" (lambda () (make-jhost "b64-decoder" '())))))
;; ---- java.util.regex.Pattern ------------------------------------------------
;; Pattern/compile returns a jolt-regex value (regex-t), so str/replace, re-find,
;; .split etc. accept it transparently.
(define pattern-multiline 8.0)
(define (pattern-quote s)
(let ((meta "\\.[]{}()*+-?^$|&") (s (if (string? s) s (jolt-str-render-one s))) (out '()))
(let loop ((i 0))
(if (= i (string-length s)) (list->string (reverse out))
(let ((c (string-ref s i)))
(when (memv c (string->list meta)) (set! out (cons #\\ out)))
(set! out (cons c out))
(loop (+ i 1)))))))
(register-class-statics! "Pattern"
(list (cons "compile" (lambda (s . flags)
(if (and (pair? flags) (= (bitwise-and (jnum->exact (car flags)) 8) 8))
(jolt-regex (string-append "(?m)" s))
(jolt-regex s))))
(cons "quote" (lambda (s) (pattern-quote s)))
(cons "MULTILINE" pattern-multiline)))
;; record-method-dispatch already routes string? -> jolt-string-method. Add a
;; regex-t arm (Pattern .split / .matcher-less surface used by corpus) by wrapping
;; once more — a regex-t isn't a jhost.
(define %hs-rmd2 record-method-dispatch)
(set! record-method-dispatch
(lambda (obj method-name rest-args)
(if (regex-t? obj)
(let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args))))
(cond ((string=? method-name "split")
;; .split returns a String[] — a seq (prints
;; (a b c), not a vector). re-split with no limit; drop trailing
;; empties (JVM default).
(let ((parts (re-split (regex-t-irx obj) (car rest) #f)))
(list->cseq (str-split-drop-trailing parts))))
((string=? method-name "pattern") (regex-t-source obj))
(else (error #f (string-append "No method " method-name " on Pattern")))))
(%hs-rmd2 obj method-name rest-args))))
;; ---- def-var! the registry entry points so emit can also reach them ---------
(def-var! "clojure.core" "host-static-ref" host-static-ref)
(def-var! "clojure.core" "host-static-call" (lambda (c m . a) (apply host-static-call c m a)))
(def-var! "clojure.core" "host-new" (lambda (c . a) (apply host-new c a)))
;; Clojure-visible class-registration hooks. A host shim (e.g. reitit.trie-jolt,
;; which mirrors the reitit.Trie Java class) registers a constructor proc or a
;; map of static members against a class token so (Class. args) / (Class/member
;; args) resolve to it. The statics argument is a jolt map {member-name -> val}.
(define (jmap->static-alist m)
(let loop ((s (jolt-seq m)) (acc '()))
(if (jolt-nil? s) acc
(let ((e (jolt-first s)))
(loop (jolt-seq (jolt-rest s)) (cons (cons (jolt-nth e 0) (jolt-nth e 1)) acc))))))
(def-var! "clojure.core" "__register-class-ctor!"
(lambda (name proc) (register-class-ctor! name proc) jolt-nil))
(def-var! "clojure.core" "__register-class-statics!"
(lambda (name members) (register-class-statics! name (jmap->static-alist members)) jolt-nil))
;; ---- tagged-table method dispatch + pluggable instance? --------------------
;; A jolt library can build stateful host objects with (jolt.host/tagged-table
;; tag) and dispatch (.method obj ...) to handlers registered here, keyed by the
;; table's "jolt/type" tag — the htable analogue of the jhost method registry
;; above. jolt-lang/http-client uses this to emulate java.net URL /
;; HttpURLConnection / java.io byte streams so clj-http-lite runs unchanged.
(define tagged-methods-tbl (make-hashtable string-hash string=?)) ; tag-key -> (method-ht)
(define (tag->method-key tag)
(if (keyword-t? tag)
(let ((ns (keyword-t-ns tag)))
(if (and ns (not (jolt-nil? ns))) (string-append ns "/" (keyword-t-name tag)) (keyword-t-name tag)))
(jolt-str-render-one tag)))
(define (register-tagged-methods! tag members)
(let* ((key (tag->method-key tag))
(h (or (hashtable-ref tagged-methods-tbl key #f)
(let ((nh (make-hashtable string-hash string=?)))
(hashtable-set! tagged-methods-tbl key nh) nh))))
(for-each (lambda (p) (hashtable-set! h (car p) (cdr p))) members)))
;; htable arm: dispatch (.method obj a*) through the table's tag method registry;
;; an unregistered method falls through (sorted colls are htables too).
(define %hs-rmd-htable record-method-dispatch)
(set! record-method-dispatch
(lambda (obj method-name rest-args)
(let ((tag (and (htable? obj) (hashtable-ref (htable-h obj) "jolt/type" #f))))
(let* ((mh (and tag (hashtable-ref tagged-methods-tbl (tag->method-key tag) #f)))
(f (and mh (hashtable-ref mh method-name #f))))
(if f
(apply f obj (if (jolt-nil? rest-args) '() (seq->list rest-args)))
(%hs-rmd-htable obj method-name rest-args))))))
(def-var! "clojure.core" "__register-class-methods!"
(lambda (tag members) (register-tagged-methods! tag (jmap->static-alist members)) jolt-nil))
;; Pluggable instance? — a library registers (fn [class-name-string val] -> true
;; | false | nil); nil means "not my class, fall through". First non-nil wins.
(define user-instance-checks '())
(register-instance-check-arm!
(lambda (type-sym val)
(let ((tname (symbol-t-name type-sym)))
(let loop ((fs user-instance-checks))
(if (null? fs)
'pass
(let ((r ((car fs) tname val)))
(if (jolt-nil? r) (loop (cdr fs)) (if (jolt-truthy? r) #t #f))))))))
(def-var! "clojure.core" "__register-instance-check!"
(lambda (f) (set! user-instance-checks (append user-instance-checks (list f))) jolt-nil))
;; (jolt.host/table? x) — is x a host tagged-table?
(def-var! "jolt.host" "table?" (lambda (x) (if (htable? x) #t #f)))

View file

@ -0,0 +1,288 @@
;; host-static-statics.ss — java.lang / java.util.* static methods and the
;; NumberFormat / Class registries. Continues host-static.ss (its registries +
;; jhost record + coercion helpers); loaded right after it.
;; ---- java.lang statics ------------------------------------------------------
;; java.lang.Math: sqrt/pow/floor/ceil/trig/log/exp always return a DOUBLE on the
;; JVM (Chez's sqrt/expt return EXACT for exact args, e.g. (sqrt 9) -> 3), so coerce
;; to flonum. round -> long (exact); abs/max/min preserve the argument's type.
(define (->dbl x) (exact->inexact x))
(register-class-statics! "Math"
(list (cons "sqrt" (lambda (x) (->dbl (sqrt x))))
(cons "pow" (lambda (a b) (->dbl (expt a b))))
(cons "floor" (lambda (x) (->dbl (floor x))))
(cons "ceil" (lambda (x) (->dbl (ceiling x))))
(cons "round" (lambda (x) (exact (floor (+ x 1/2))))) ; JVM round-half-up -> long
(cons "abs" (lambda (x) (abs x)))
(cons "sin" (lambda (x) (->dbl (sin x)))) (cons "cos" (lambda (x) (->dbl (cos x))))
(cons "tan" (lambda (x) (->dbl (tan x)))) (cons "asin" (lambda (x) (->dbl (asin x))))
(cons "acos" (lambda (x) (->dbl (acos x)))) (cons "atan" (lambda (x) (->dbl (atan x))))
(cons "log" (lambda (x) (->dbl (log x)))) (cons "log10" (lambda (x) (->dbl (/ (log x) (log 10)))))
(cons "exp" (lambda (x) (->dbl (exp x))))
(cons "max" (lambda (a b) (if (> a b) a b))) (cons "min" (lambda (a b) (if (< a b) a b)))
(cons "signum" (lambda (x) (cond ((< x 0) -1.0) ((> x 0) 1.0) (else 0.0))))
(cons "PI" (->dbl (* 4 (atan 1)))) (cons "E" (->dbl (exp 1)))
(cons "random" (lambda args (random 1.0)))))
;; Thread: real OS threads back futures/promises.
;; - sleep parks the calling thread for `ms` ms (a worker sleeping doesn't block
;; the parent).
;; - yield hands the CPU to another runnable thread (libc sched_yield).
;; - each thread carries an interrupt flag; interrupted (static) reads AND clears
;; the current thread's flag, matching the JVM. currentThread / .interrupt /
;; .isInterrupted are wired in io.ss, where the thread handle is built.
;; Per-thread interrupt flag, lazily allocated so each OS thread gets its own box.
;; A thread handle (from currentThread) captures this box, so .interrupt from
;; another thread sets the target thread's flag.
(define thread-interrupt-box (make-thread-parameter #f))
(define (current-interrupt-box)
(or (thread-interrupt-box)
(let ((b (box #f))) (thread-interrupt-box b) b)))
(define (clear-thread-interrupt!) (set-box! (current-interrupt-box) #f))
;; libc sched_yield, resolved once; fall back to a zero-length park if the symbol
;; isn't available.
(define thread-yield!
(let ((fp #f) (tried? #f))
(lambda ()
(unless tried?
(set! tried? #t)
(set! fp (guard (e (#t #f))
(load-shared-object #f)
(foreign-procedure "sched_yield" () int))))
(if fp (fp) (sleep (make-time 'time-duration 0 0)))
jolt-nil)))
(define thread-statics
(list (cons "sleep" (lambda (ms . _)
(let* ((ms* (exact (floor ms)))
(secs (quotient ms* 1000))
(nanos (* (remainder ms* 1000) 1000000)))
(sleep (make-time 'time-duration nanos secs)))
jolt-nil))
(cons "yield" (lambda _ (thread-yield!)))
(cons "interrupted" (lambda _ (let* ((b (current-interrupt-box)) (v (unbox b)))
(set-box! b #f) (and v #t))))))
(register-class-statics! "Thread" thread-statics)
(register-class-statics! "java.lang.Thread" thread-statics)
;; clojure.lang.LockingTransaction: jolt has no STM (no refs/dosync), so a
;; transaction is never running. isRunning -> false.
(register-class-statics! "LockingTransaction" (list (cons "isRunning" (lambda () #f))))
(register-class-statics! "clojure.lang.LockingTransaction" (list (cons "isRunning" (lambda () #f))))
;; clojure.lang.LazilyPersistentVector/createOwning: build a vector from an array
;; (malli's -vmap fills an object-array then hands it over). jolt has no array
;; ownership transfer, so copy the array's elements into a persistent vector.
(define (lpv-create-owning arr) (apply jolt-vector (seq->list (jolt-seq arr))))
(register-class-statics! "LazilyPersistentVector" (list (cons "createOwning" lpv-create-owning)))
(register-class-statics! "clojure.lang.LazilyPersistentVector" (list (cons "createOwning" lpv-create-owning)))
;; clojure.lang.PersistentArrayMap/createWithCheck: build a map from a [k v k v…]
;; array, throwing on a duplicate key. malli's eager entry parser relies on the
;; throw to report ::duplicate-keys, so a missing class would mis-fire on every
;; map. Build the map and signal if a key collapsed (count*2 < array length).
(define (pam-create-with-check arr)
(let ((items (seq->list (jolt-seq arr))))
(let loop ((xs items) (m (jolt-hash-map)))
(if (null? xs) m
(if (null? (cdr xs)) (error #f "PersistentArrayMap: odd key/value count")
(let ((k (car xs)))
(if (jolt-contains? m k) (error #f "Duplicate key")
(loop (cddr xs) (jolt-assoc m k (cadr xs))))))))))
(register-class-statics! "PersistentArrayMap" (list (cons "createWithCheck" pam-create-with-check)))
(register-class-statics! "clojure.lang.PersistentArrayMap" (list (cons "createWithCheck" pam-create-with-check)))
(define (now-millis)
(let ((t (current-time 'time-utc)))
(+ (* 1000 (time-second t)) (quotient (time-nanosecond t) 1000000))))
;; clojure.core/current-time-ms — epoch milliseconds; backs the `time` macro.
(def-var! "clojure.core" "current-time-ms" (lambda () (->num (now-millis))))
(register-class-statics! "System"
(list (cons "currentTimeMillis" (lambda () (->num (now-millis))))
(cons "nanoTime" (lambda () (->num (* 1000000 (now-millis)))))
(cons "exit" (lambda args (exit (if (null? args) 0 (jnum->exact (car args))))))
;; wrapped in lambdas: the helpers are defined below, resolved at call time.
(cons "getProperty" (lambda (k . d) (apply sys-get-property k d)))
(cons "setProperty" (lambda (k v) (sys-set-property k v)))
(cons "clearProperty" (lambda (k) (sys-clear-property k)))
(cons "getProperties" (lambda () (sys-properties-map)))
(cons "getenv" (lambda k (apply sys-getenv k)))))
(register-class-statics! "Long"
(list (cons "MAX_VALUE" (->num 9223372036854775807))
(cons "MIN_VALUE" (->num -9223372036854775808))
(cons "parseLong" (lambda (s . r) (parse-int-or-throw s (if (null? r) 10 (jnum->exact (car r))) "parseLong")))
(cons "valueOf" (lambda (s . r) (parse-int-or-throw s (if (null? r) 10 (jnum->exact (car r))) "valueOf")))))
(register-class-statics! "Integer"
(list (cons "MAX_VALUE" (->num 2147483647)) (cons "MIN_VALUE" (->num -2147483648))
(cons "valueOf" (lambda (x . r)
(if (number? x) (->num x)
(parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "valueOf"))))
(cons "parseInt" (lambda (x . r) (parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "parseInt")))))
(register-class-statics! "Boolean"
(list (cons "parseBoolean" (lambda (s) (string=? "true" (ascii-string-down (if (string? s) s (jolt-str-render-one s))))))
(cons "TRUE" #t) (cons "FALSE" #f)))
(register-class-ctor! "Double" ->double)
(register-class-ctor! "Float" ->double)
(register-class-statics! "Double"
(list (cons "parseDouble" parse-double-or-throw)
(cons "valueOf" ->double)
(cons "toString" (lambda (x) (jolt-str-render-one (->double x))))
(cons "isNaN" (lambda (x) (and (flonum? x) (nan? x))))
(cons "isInfinite" (lambda (x) (and (flonum? x) (infinite? x))))
(cons "MAX_VALUE" 1.7976931348623157e308) (cons "MIN_VALUE" 4.9e-324)
(cons "POSITIVE_INFINITY" +inf.0) (cons "NEGATIVE_INFINITY" -inf.0) (cons "NaN" +nan.0)))
(register-class-statics! "Float"
(list (cons "parseFloat" parse-double-or-throw) (cons "valueOf" ->double)))
;; Character: ASCII predicates (the engine is byte/ASCII oriented).
(register-class-statics! "Character"
(list (cons "isUpperCase" (lambda (c) (let ((n (char-code c))) (and (>= n 65) (<= n 90)))))
(cons "isLowerCase" (lambda (c) (let ((n (char-code c))) (and (>= n 97) (<= n 122)))))
(cons "isDigit" (lambda (c) (let ((n (char-code c))) (and (>= n 48) (<= n 57)))))
(cons "isWhitespace" (lambda (c) (char<=? (integer->char (char-code c)) #\space)))))
;; String/valueOf(Object): "null" for nil, else jolt's str semantics.
;; String/format(fmt args…) / (locale fmt args…) -> the clojure.core format engine.
(register-class-statics! "String"
(list (cons "valueOf" (lambda (x . _) (if (jolt-nil? x) "null" (jolt-str-render-one x))))
(cons "format" (lambda (a . rest)
(if (and (jhost? a) (string=? (jhost-tag a) "locale"))
(apply jolt-format (car rest) (cdr rest))
(apply jolt-format a rest))))))
;; ---- java.text.NumberFormat -------------------------------------------------
;; A grouping decimal formatter (selmer number-format / cuerdas). state:
;; #(grouping? min-frac max-frac). .format groups the integer part with commas.
(define (nf-make grouping? minf maxf) (make-jhost "numberformat" (vector grouping? minf maxf)))
(define (group-int-str s) ; "1234567" -> "1,234,567"
(let* ((neg (and (> (string-length s) 0) (char=? (string-ref s 0) #\-)))
(digs (if neg (substring s 1 (string-length s)) s))
(n (string-length digs)) (out '()))
(let loop ((i 0))
(when (< i n)
(when (and (> i 0) (= 0 (modulo (- n i) 3))) (set! out (cons #\, out)))
(set! out (cons (string-ref digs i) out)) (loop (+ i 1))))
(string-append (if neg "-" "") (list->string (reverse out)))))
(define (nf-format self x)
(let* ((grouping? (vector-ref (jhost-state self) 0))
(minf (vector-ref (jhost-state self) 1)) (maxf (vector-ref (jhost-state self) 2))
(neg (< x 0)) (ax (abs (exact->inexact x)))
(scale (expt 10 maxf))
(scaled (exact (round (* ax scale))))
(ipart (quotient scaled scale)) (fpart (remainder scaled scale))
(istr (number->string ipart))
(fstr0 (if (> maxf 0) (let ((s (number->string fpart)))
(string-append (make-string (max 0 (- maxf (string-length s))) #\0) s)) ""))
;; trim trailing zeros down to minf
(fstr (let loop ((s fstr0)) (if (and (> (string-length s) minf)
(char=? (string-ref s (- (string-length s) 1)) #\0))
(loop (substring s 0 (- (string-length s) 1))) s))))
(string-append (if neg "-" "") (if grouping? (group-int-str istr) istr)
(if (> (string-length fstr) 0) (string-append "." fstr) ""))))
(register-host-methods! "numberformat"
(list (cons "format" (lambda (self n) (nf-format self n)))
(cons "setMaximumFractionDigits" (lambda (self d) (vector-set! (jhost-state self) 2 (jnum->exact d)) jolt-nil))
(cons "setMinimumFractionDigits" (lambda (self d) (vector-set! (jhost-state self) 1 (jnum->exact d)) jolt-nil))
(cons "setGroupingUsed" (lambda (self b) (vector-set! (jhost-state self) 0 (jolt-truthy? b)) jolt-nil))))
(let ((nf-statics (list (cons "getInstance" (lambda _ (nf-make #t 0 3)))
(cons "getNumberInstance" (lambda _ (nf-make #t 0 3)))
(cons "getIntegerInstance" (lambda _ (nf-make #t 0 0))))))
(register-class-statics! "NumberFormat" nf-statics)
(register-class-statics! "java.text.NumberFormat" nf-statics))
(register-class-statics! "Class"
;; an array descriptor ("[C", "[I", …) is its own class token (so instance? and
;; class compare equal); other names become a class jhost.
(list (cons "forName" (lambda (nm)
(if (and (> (string-length nm) 0) (char=? (string-ref nm 0) #\[))
nm
(make-jhost "class" (list (cons 'name nm))))))))
;; ---- System helpers (defined before use above via top-level order) ----------
;; os.name reflects the actual platform (Chez's machine-type names it): a *osx
;; machine is macOS, otherwise Linux. Code that branches on the OS (socket struct
;; layout, path handling) needs the truth, not a fixed value.
(define (substring-index needle hay)
(let ((nl (string-length needle)) (hl (string-length hay)))
(let loop ((i 0)) (cond ((> (+ i nl) hl) #f)
((string=? (substring hay i (+ i nl)) needle) i)
(else (loop (+ i 1)))))))
(define sys-os-name
(let ((m (symbol->string (machine-type))))
(cond ((or (substring-index "osx" m) (substring-index "macos" m)) "Mac OS X")
((or (substring-index "nt" m) (substring-index "windows" m)) "Windows")
(else "Linux"))))
;; runtime-settable system properties (System/setProperty). A set value wins over
;; the built-in defaults below; clearProperty removes it.
(define sys-prop-table (make-hashtable string-hash string=?))
(define (sys-set-property k v)
(let ((prev (hashtable-ref sys-prop-table k jolt-nil)))
(hashtable-set! sys-prop-table k (if (string? v) v (jolt-str-render-one v)))
prev))
(define (sys-clear-property k)
(let ((prev (hashtable-ref sys-prop-table k jolt-nil)))
(hashtable-delete! sys-prop-table k) prev))
(define (sys-get-property k . dflt)
(let ((set-val (hashtable-ref sys-prop-table k #f)))
(cond (set-val set-val)
((string=? k "os.name") sys-os-name)
((string=? k "line.separator") "\n")
((string=? k "file.separator") "/")
((string=? k "path.separator") ":")
((string=? k "user.dir") (or (getenv "PWD") "."))
((string=? k "user.home") (or (getenv "HOME") ""))
((string=? k "java.io.tmpdir") (or (getenv "TMPDIR") "/tmp"))
((pair? dflt) (car dflt))
(else jolt-nil))))
(define (sys-properties-map)
(jolt-hash-map "os.name" sys-os-name "line.separator" "\n" "file.separator" "/"
"user.dir" (or (getenv "PWD") ".") "user.home" (or (getenv "HOME") "")
"java.io.tmpdir" (or (getenv "TMPDIR") "/tmp")))
;; full environment as an alist of (name . value), via spawning `env`.
(define (all-env-pairs)
(call-with-values
(lambda () (open-process-ports "env" (buffer-mode block) (native-transcoder)))
(lambda (stdin stdout stderr pid)
(let loop ((acc '()))
(let ((l (get-line stdout)))
(if (eof-object? l) (reverse acc)
(let ((eq (let scan ((i 0)) (cond ((= i (string-length l)) #f)
((char=? (string-ref l i) #\=) i)
(else (scan (+ i 1)))))))
(loop (if eq (cons (cons (substring l 0 eq) (substring l (+ eq 1) (string-length l))) acc) acc)))))))))
;; JOLT_BAKE_ENV_ALLOWLIST: when set, only the listed comma-separated
;; names are served; unset (the normal case) reads are live and unfiltered.
(define (env-allowlist)
(let ((a (getenv "JOLT_BAKE_ENV_ALLOWLIST")))
(and a (map str-trim (str-literal-split a ",")))))
(define (sys-getenv . k)
(let ((allow (env-allowlist)))
(if (null? k)
(apply jolt-hash-map
(let loop ((ps (all-env-pairs)) (acc '()))
(cond ((null? ps) (reverse acc))
((and allow (not (member (caar ps) allow))) (loop (cdr ps) acc))
(else (loop (cdr ps) (cons (cdar ps) (cons (caar ps) acc)))))))
(let ((name (car k)))
(if (and allow (not (member name allow))) jolt-nil
(let ((v (getenv name))) (if v v jolt-nil)))))))
;; ---- StringBuilder ----------------------------------------------------------
;; state: a box (1-vector) holding the accumulated string.
(define (sb-str self) (vector-ref (jhost-state self) 0))
(define (sb-set! self s) (vector-set! (jhost-state self) 0 s))
(define (render-piece x)
(cond ((jolt-nil? x) "null") ((char? x) (string x)) ((string? x) x)
(else (jolt-str-render-one x))))
;; (Object.) — a fresh value with distinct identity (libraries use it as a lock
;; or a unique sentinel). Each call returns a new jhost so identical?/= separate.
(register-class-ctor! "Object" (lambda _ (make-jhost "object" (vector))))

View file

@ -139,792 +139,3 @@
(if (string? s) s (jolt-str-render-one s)) "\""))))
(define (->double x) (if (number? x) (exact->inexact x) (parse-double-or-throw x)))
;; ---- java.lang statics ------------------------------------------------------
;; java.lang.Math: sqrt/pow/floor/ceil/trig/log/exp always return a DOUBLE on the
;; JVM (Chez's sqrt/expt return EXACT for exact args, e.g. (sqrt 9) -> 3), so coerce
;; to flonum. round -> long (exact); abs/max/min preserve the argument's type.
(define (->dbl x) (exact->inexact x))
(register-class-statics! "Math"
(list (cons "sqrt" (lambda (x) (->dbl (sqrt x))))
(cons "pow" (lambda (a b) (->dbl (expt a b))))
(cons "floor" (lambda (x) (->dbl (floor x))))
(cons "ceil" (lambda (x) (->dbl (ceiling x))))
(cons "round" (lambda (x) (exact (floor (+ x 1/2))))) ; JVM round-half-up -> long
(cons "abs" (lambda (x) (abs x)))
(cons "sin" (lambda (x) (->dbl (sin x)))) (cons "cos" (lambda (x) (->dbl (cos x))))
(cons "tan" (lambda (x) (->dbl (tan x)))) (cons "asin" (lambda (x) (->dbl (asin x))))
(cons "acos" (lambda (x) (->dbl (acos x)))) (cons "atan" (lambda (x) (->dbl (atan x))))
(cons "log" (lambda (x) (->dbl (log x)))) (cons "log10" (lambda (x) (->dbl (/ (log x) (log 10)))))
(cons "exp" (lambda (x) (->dbl (exp x))))
(cons "max" (lambda (a b) (if (> a b) a b))) (cons "min" (lambda (a b) (if (< a b) a b)))
(cons "signum" (lambda (x) (cond ((< x 0) -1.0) ((> x 0) 1.0) (else 0.0))))
(cons "PI" (->dbl (* 4 (atan 1)))) (cons "E" (->dbl (exp 1)))
(cons "random" (lambda args (random 1.0)))))
;; Thread: real OS threads back futures/promises.
;; - sleep parks the calling thread for `ms` ms (a worker sleeping doesn't block
;; the parent).
;; - yield hands the CPU to another runnable thread (libc sched_yield).
;; - each thread carries an interrupt flag; interrupted (static) reads AND clears
;; the current thread's flag, matching the JVM. currentThread / .interrupt /
;; .isInterrupted are wired in io.ss, where the thread handle is built.
;; Per-thread interrupt flag, lazily allocated so each OS thread gets its own box.
;; A thread handle (from currentThread) captures this box, so .interrupt from
;; another thread sets the target thread's flag.
(define thread-interrupt-box (make-thread-parameter #f))
(define (current-interrupt-box)
(or (thread-interrupt-box)
(let ((b (box #f))) (thread-interrupt-box b) b)))
(define (clear-thread-interrupt!) (set-box! (current-interrupt-box) #f))
;; libc sched_yield, resolved once; fall back to a zero-length park if the symbol
;; isn't available.
(define thread-yield!
(let ((fp #f) (tried? #f))
(lambda ()
(unless tried?
(set! tried? #t)
(set! fp (guard (e (#t #f))
(load-shared-object #f)
(foreign-procedure "sched_yield" () int))))
(if fp (fp) (sleep (make-time 'time-duration 0 0)))
jolt-nil)))
(define thread-statics
(list (cons "sleep" (lambda (ms . _)
(let* ((ms* (exact (floor ms)))
(secs (quotient ms* 1000))
(nanos (* (remainder ms* 1000) 1000000)))
(sleep (make-time 'time-duration nanos secs)))
jolt-nil))
(cons "yield" (lambda _ (thread-yield!)))
(cons "interrupted" (lambda _ (let* ((b (current-interrupt-box)) (v (unbox b)))
(set-box! b #f) (and v #t))))))
(register-class-statics! "Thread" thread-statics)
(register-class-statics! "java.lang.Thread" thread-statics)
;; clojure.lang.LockingTransaction: jolt has no STM (no refs/dosync), so a
;; transaction is never running. isRunning -> false.
(register-class-statics! "LockingTransaction" (list (cons "isRunning" (lambda () #f))))
(register-class-statics! "clojure.lang.LockingTransaction" (list (cons "isRunning" (lambda () #f))))
;; clojure.lang.LazilyPersistentVector/createOwning: build a vector from an array
;; (malli's -vmap fills an object-array then hands it over). jolt has no array
;; ownership transfer, so copy the array's elements into a persistent vector.
(define (lpv-create-owning arr) (apply jolt-vector (seq->list (jolt-seq arr))))
(register-class-statics! "LazilyPersistentVector" (list (cons "createOwning" lpv-create-owning)))
(register-class-statics! "clojure.lang.LazilyPersistentVector" (list (cons "createOwning" lpv-create-owning)))
;; clojure.lang.PersistentArrayMap/createWithCheck: build a map from a [k v k v…]
;; array, throwing on a duplicate key. malli's eager entry parser relies on the
;; throw to report ::duplicate-keys, so a missing class would mis-fire on every
;; map. Build the map and signal if a key collapsed (count*2 < array length).
(define (pam-create-with-check arr)
(let ((items (seq->list (jolt-seq arr))))
(let loop ((xs items) (m (jolt-hash-map)))
(if (null? xs) m
(if (null? (cdr xs)) (error #f "PersistentArrayMap: odd key/value count")
(let ((k (car xs)))
(if (jolt-contains? m k) (error #f "Duplicate key")
(loop (cddr xs) (jolt-assoc m k (cadr xs))))))))))
(register-class-statics! "PersistentArrayMap" (list (cons "createWithCheck" pam-create-with-check)))
(register-class-statics! "clojure.lang.PersistentArrayMap" (list (cons "createWithCheck" pam-create-with-check)))
(define (now-millis)
(let ((t (current-time 'time-utc)))
(+ (* 1000 (time-second t)) (quotient (time-nanosecond t) 1000000))))
;; clojure.core/current-time-ms — epoch milliseconds; backs the `time` macro.
(def-var! "clojure.core" "current-time-ms" (lambda () (->num (now-millis))))
(register-class-statics! "System"
(list (cons "currentTimeMillis" (lambda () (->num (now-millis))))
(cons "nanoTime" (lambda () (->num (* 1000000 (now-millis)))))
(cons "exit" (lambda args (exit (if (null? args) 0 (jnum->exact (car args))))))
;; wrapped in lambdas: the helpers are defined below, resolved at call time.
(cons "getProperty" (lambda (k . d) (apply sys-get-property k d)))
(cons "setProperty" (lambda (k v) (sys-set-property k v)))
(cons "clearProperty" (lambda (k) (sys-clear-property k)))
(cons "getProperties" (lambda () (sys-properties-map)))
(cons "getenv" (lambda k (apply sys-getenv k)))))
(register-class-statics! "Long"
(list (cons "MAX_VALUE" (->num 9223372036854775807))
(cons "MIN_VALUE" (->num -9223372036854775808))
(cons "parseLong" (lambda (s . r) (parse-int-or-throw s (if (null? r) 10 (jnum->exact (car r))) "parseLong")))
(cons "valueOf" (lambda (s . r) (parse-int-or-throw s (if (null? r) 10 (jnum->exact (car r))) "valueOf")))))
(register-class-statics! "Integer"
(list (cons "MAX_VALUE" (->num 2147483647)) (cons "MIN_VALUE" (->num -2147483648))
(cons "valueOf" (lambda (x . r)
(if (number? x) (->num x)
(parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "valueOf"))))
(cons "parseInt" (lambda (x . r) (parse-int-or-throw x (if (null? r) 10 (jnum->exact (car r))) "parseInt")))))
(register-class-statics! "Boolean"
(list (cons "parseBoolean" (lambda (s) (string=? "true" (ascii-string-down (if (string? s) s (jolt-str-render-one s))))))
(cons "TRUE" #t) (cons "FALSE" #f)))
(register-class-ctor! "Double" ->double)
(register-class-ctor! "Float" ->double)
(register-class-statics! "Double"
(list (cons "parseDouble" parse-double-or-throw)
(cons "valueOf" ->double)
(cons "toString" (lambda (x) (jolt-str-render-one (->double x))))
(cons "isNaN" (lambda (x) (and (flonum? x) (nan? x))))
(cons "isInfinite" (lambda (x) (and (flonum? x) (infinite? x))))
(cons "MAX_VALUE" 1.7976931348623157e308) (cons "MIN_VALUE" 4.9e-324)
(cons "POSITIVE_INFINITY" +inf.0) (cons "NEGATIVE_INFINITY" -inf.0) (cons "NaN" +nan.0)))
(register-class-statics! "Float"
(list (cons "parseFloat" parse-double-or-throw) (cons "valueOf" ->double)))
;; Character: ASCII predicates (the engine is byte/ASCII oriented).
(register-class-statics! "Character"
(list (cons "isUpperCase" (lambda (c) (let ((n (char-code c))) (and (>= n 65) (<= n 90)))))
(cons "isLowerCase" (lambda (c) (let ((n (char-code c))) (and (>= n 97) (<= n 122)))))
(cons "isDigit" (lambda (c) (let ((n (char-code c))) (and (>= n 48) (<= n 57)))))
(cons "isWhitespace" (lambda (c) (char<=? (integer->char (char-code c)) #\space)))))
;; String/valueOf(Object): "null" for nil, else jolt's str semantics.
;; String/format(fmt args…) / (locale fmt args…) -> the clojure.core format engine.
(register-class-statics! "String"
(list (cons "valueOf" (lambda (x . _) (if (jolt-nil? x) "null" (jolt-str-render-one x))))
(cons "format" (lambda (a . rest)
(if (and (jhost? a) (string=? (jhost-tag a) "locale"))
(apply jolt-format (car rest) (cdr rest))
(apply jolt-format a rest))))))
;; ---- java.text.NumberFormat -------------------------------------------------
;; A grouping decimal formatter (selmer number-format / cuerdas). state:
;; #(grouping? min-frac max-frac). .format groups the integer part with commas.
(define (nf-make grouping? minf maxf) (make-jhost "numberformat" (vector grouping? minf maxf)))
(define (group-int-str s) ; "1234567" -> "1,234,567"
(let* ((neg (and (> (string-length s) 0) (char=? (string-ref s 0) #\-)))
(digs (if neg (substring s 1 (string-length s)) s))
(n (string-length digs)) (out '()))
(let loop ((i 0))
(when (< i n)
(when (and (> i 0) (= 0 (modulo (- n i) 3))) (set! out (cons #\, out)))
(set! out (cons (string-ref digs i) out)) (loop (+ i 1))))
(string-append (if neg "-" "") (list->string (reverse out)))))
(define (nf-format self x)
(let* ((grouping? (vector-ref (jhost-state self) 0))
(minf (vector-ref (jhost-state self) 1)) (maxf (vector-ref (jhost-state self) 2))
(neg (< x 0)) (ax (abs (exact->inexact x)))
(scale (expt 10 maxf))
(scaled (exact (round (* ax scale))))
(ipart (quotient scaled scale)) (fpart (remainder scaled scale))
(istr (number->string ipart))
(fstr0 (if (> maxf 0) (let ((s (number->string fpart)))
(string-append (make-string (max 0 (- maxf (string-length s))) #\0) s)) ""))
;; trim trailing zeros down to minf
(fstr (let loop ((s fstr0)) (if (and (> (string-length s) minf)
(char=? (string-ref s (- (string-length s) 1)) #\0))
(loop (substring s 0 (- (string-length s) 1))) s))))
(string-append (if neg "-" "") (if grouping? (group-int-str istr) istr)
(if (> (string-length fstr) 0) (string-append "." fstr) ""))))
(register-host-methods! "numberformat"
(list (cons "format" (lambda (self n) (nf-format self n)))
(cons "setMaximumFractionDigits" (lambda (self d) (vector-set! (jhost-state self) 2 (jnum->exact d)) jolt-nil))
(cons "setMinimumFractionDigits" (lambda (self d) (vector-set! (jhost-state self) 1 (jnum->exact d)) jolt-nil))
(cons "setGroupingUsed" (lambda (self b) (vector-set! (jhost-state self) 0 (jolt-truthy? b)) jolt-nil))))
(register-class-statics! "NumberFormat"
(list (cons "getInstance" (lambda _ (nf-make #t 0 3)))
(cons "getNumberInstance" (lambda _ (nf-make #t 0 3)))
(cons "getIntegerInstance" (lambda _ (nf-make #t 0 0)))))
(register-class-statics! "java.text.NumberFormat"
(list (cons "getInstance" (lambda _ (nf-make #t 0 3)))
(cons "getNumberInstance" (lambda _ (nf-make #t 0 3)))
(cons "getIntegerInstance" (lambda _ (nf-make #t 0 0)))))
(register-class-statics! "Class"
;; an array descriptor ("[C", "[I", …) is its own class token (so instance? and
;; class compare equal); other names become a class jhost.
(list (cons "forName" (lambda (nm)
(if (and (> (string-length nm) 0) (char=? (string-ref nm 0) #\[))
nm
(make-jhost "class" (list (cons 'name nm))))))))
;; ---- System helpers (defined before use above via top-level order) ----------
;; os.name reflects the actual platform (Chez's machine-type names it): a *osx
;; machine is macOS, otherwise Linux. Code that branches on the OS (socket struct
;; layout, path handling) needs the truth, not a fixed value.
(define (substring-index needle hay)
(let ((nl (string-length needle)) (hl (string-length hay)))
(let loop ((i 0)) (cond ((> (+ i nl) hl) #f)
((string=? (substring hay i (+ i nl)) needle) i)
(else (loop (+ i 1)))))))
(define sys-os-name
(let ((m (symbol->string (machine-type))))
(cond ((or (substring-index "osx" m) (substring-index "macos" m)) "Mac OS X")
((or (substring-index "nt" m) (substring-index "windows" m)) "Windows")
(else "Linux"))))
;; runtime-settable system properties (System/setProperty). A set value wins over
;; the built-in defaults below; clearProperty removes it.
(define sys-prop-table (make-hashtable string-hash string=?))
(define (sys-set-property k v)
(let ((prev (hashtable-ref sys-prop-table k jolt-nil)))
(hashtable-set! sys-prop-table k (if (string? v) v (jolt-str-render-one v)))
prev))
(define (sys-clear-property k)
(let ((prev (hashtable-ref sys-prop-table k jolt-nil)))
(hashtable-delete! sys-prop-table k) prev))
(define (sys-get-property k . dflt)
(let ((set-val (hashtable-ref sys-prop-table k #f)))
(cond (set-val set-val)
((string=? k "os.name") sys-os-name)
((string=? k "line.separator") "\n")
((string=? k "file.separator") "/")
((string=? k "path.separator") ":")
((string=? k "user.dir") (or (getenv "PWD") "."))
((string=? k "user.home") (or (getenv "HOME") ""))
((string=? k "java.io.tmpdir") (or (getenv "TMPDIR") "/tmp"))
((pair? dflt) (car dflt))
(else jolt-nil))))
(define (sys-properties-map)
(jolt-hash-map "os.name" sys-os-name "line.separator" "\n" "file.separator" "/"
"user.dir" (or (getenv "PWD") ".") "user.home" (or (getenv "HOME") "")
"java.io.tmpdir" (or (getenv "TMPDIR") "/tmp")))
;; full environment as an alist of (name . value), via spawning `env`.
(define (all-env-pairs)
(call-with-values
(lambda () (open-process-ports "env" (buffer-mode block) (native-transcoder)))
(lambda (stdin stdout stderr pid)
(let loop ((acc '()))
(let ((l (get-line stdout)))
(if (eof-object? l) (reverse acc)
(let ((eq (let scan ((i 0)) (cond ((= i (string-length l)) #f)
((char=? (string-ref l i) #\=) i)
(else (scan (+ i 1)))))))
(loop (if eq (cons (cons (substring l 0 eq) (substring l (+ eq 1) (string-length l))) acc) acc)))))))))
;; JOLT_BAKE_ENV_ALLOWLIST: when set, only the listed comma-separated
;; names are served; unset (the normal case) reads are live and unfiltered.
(define (env-allowlist)
(let ((a (getenv "JOLT_BAKE_ENV_ALLOWLIST")))
(and a (map str-trim (str-literal-split a ",")))))
(define (sys-getenv . k)
(let ((allow (env-allowlist)))
(if (null? k)
(apply jolt-hash-map
(let loop ((ps (all-env-pairs)) (acc '()))
(cond ((null? ps) (reverse acc))
((and allow (not (member (caar ps) allow))) (loop (cdr ps) acc))
(else (loop (cdr ps) (cons (cdar ps) (cons (caar ps) acc)))))))
(let ((name (car k)))
(if (and allow (not (member name allow))) jolt-nil
(let ((v (getenv name))) (if v v jolt-nil)))))))
;; ---- StringBuilder ----------------------------------------------------------
;; state: a box (1-vector) holding the accumulated string.
(define (sb-str self) (vector-ref (jhost-state self) 0))
(define (sb-set! self s) (vector-set! (jhost-state self) 0 s))
(define (render-piece x)
(cond ((jolt-nil? x) "null") ((char? x) (string x)) ((string? x) x)
(else (jolt-str-render-one x))))
;; (Object.) — a fresh value with distinct identity (libraries use it as a lock
;; or a unique sentinel). Each call returns a new jhost so identical?/= separate.
(register-class-ctor! "Object" (lambda _ (make-jhost "object" (vector))))
;; ---- java.util.ArrayList ----------------------------------------------------
;; A mutable list backed by a Scheme list in a box. medley's stateful transducers
;; (window / partition-between) build one with .add / .size / .toArray / .clear /
;; .remove. (ArrayList.) | (ArrayList. n) | (ArrayList. coll).
(define (al-list self) (vector-ref (jhost-state self) 0))
(define (al-set! self xs) (vector-set! (jhost-state self) 0 xs))
(define (make-arraylist xs) (make-jhost "arraylist" (vector xs)))
(register-class-ctor! "ArrayList"
(lambda args
(cond ((null? args) (make-arraylist '()))
((number? (car args)) (make-arraylist '())) ; initial capacity, ignored
(else (make-arraylist (seq->list (jolt-seq (car args))))))))
(register-class-ctor! "java.util.ArrayList"
(lambda args
(cond ((null? args) (make-arraylist '()))
((number? (car args)) (make-arraylist '()))
(else (make-arraylist (seq->list (jolt-seq (car args))))))))
(define (al-remove-at xs i)
(let loop ((xs xs) (i i) (acc '()))
(cond ((null? xs) (reverse acc))
((= i 0) (append (reverse acc) (cdr xs)))
(else (loop (cdr xs) (- i 1) (cons (car xs) acc))))))
(register-host-methods! "arraylist"
(list
(cons "add" (lambda (self . a)
;; (.add x) -> append+true; (.add i x) -> insert at i, returns nil.
(if (= 1 (length a))
(begin (al-set! self (append (al-list self) (list (car a)))) #t)
(let ((i (jnum->exact (car a))) (x (cadr a)) (xs (al-list self)))
(al-set! self (append (list-head xs i) (list x) (list-tail xs i))) jolt-nil))))
(cons "add!" (lambda (self x) (al-set! self (append (al-list self) (list x))) #t))
(cons "get" (lambda (self i) (list-ref (al-list self) (jnum->exact i))))
(cons "set" (lambda (self i x)
(let* ((xs (al-list self)) (idx (jnum->exact i)) (old (list-ref xs idx)))
(al-set! self (append (list-head xs idx) (list x) (list-tail xs (+ idx 1)))) old)))
(cons "size" (lambda (self) (->num (length (al-list self)))))
(cons "isEmpty" (lambda (self) (null? (al-list self))))
(cons "remove" (lambda (self i)
(let* ((xs (al-list self)) (idx (jnum->exact i)) (old (list-ref xs idx)))
(al-set! self (al-remove-at xs idx)) old)))
(cons "clear" (lambda (self) (al-set! self '()) jolt-nil))
(cons "contains" (lambda (self x) (and (memp (lambda (e) (jolt=2 e x)) (al-list self)) #t)))
(cons "toArray" (lambda (self . _) (apply jolt-vector (al-list self))))
(cons "iterator" (lambda (self) (make-jiterator (list->cseq (al-list self)))))
(cons "toString" (lambda (self) (jolt-pr-str (list->cseq (al-list self)))))))
(register-class-ctor! "StringBuilder"
(lambda args (make-jhost "string-builder"
;; a numeric first arg is a CAPACITY hint, not content.
(vector (if (and (pair? args) (not (number? (car args)))) (render-piece (car args)) "")))))
(register-host-methods! "string-builder"
(list (cons "append" (lambda (self x) (sb-set! self (string-append (sb-str self) (render-piece x))) self))
(cons "toString" (lambda (self) (sb-str self)))
(cons "length" (lambda (self) (->num (string-length (sb-str self)))))
(cons "charAt" (lambda (self i) (string-ref (sb-str self) (jnum->exact i))))
(cons "setLength" (lambda (self n)
(let ((cur (sb-str self)) (n (jnum->exact n)))
(sb-set! self (if (< n (string-length cur))
(substring cur 0 n)
(string-append cur (make-string (- n (string-length cur)) #\nul)))))
jolt-nil))))
;; ---- StringWriter -----------------------------------------------------------
;; Writer.write(int) writes the CHAR for that code; append(char) appends the char.
(define (writer-piece x) (if (number? x) (string (integer->char (jnum->exact x))) (render-piece x)))
(register-class-ctor! "StringWriter" (lambda args (make-jhost "writer" (vector ""))))
(register-host-methods! "writer"
(list (cons "write" (lambda (self x) (sb-set! self (string-append (sb-str self) (writer-piece x))) jolt-nil))
(cons "append" (lambda (self x) (sb-set! self (string-append (sb-str self) (render-piece x))) self))
(cons "flush" (lambda (self) jolt-nil))
(cons "close" (lambda (self) jolt-nil))
(cons "toString" (lambda (self) (sb-str self)))))
;; a file-backed writer (clojure.java.io/writer of a File/path): accumulates like
;; StringWriter, then persists to the path on flush/close, so
;; (with-open [w (io/writer "f")] (.write w …)) writes the file. State #(path buf).
(define (fw-path self) (vector-ref (jhost-state self) 0))
(define (fw-buf self) (vector-ref (jhost-state self) 1))
(define (fw-append! self s) (vector-set! (jhost-state self) 1 (string-append (fw-buf self) s)))
(define (fw-flush! self) (jolt-spit (fw-path self) (fw-buf self))) ; jolt-spit: io.ss
(register-host-methods! "file-writer"
(list (cons "write" (lambda (self x) (fw-append! self (writer-piece x)) jolt-nil))
(cons "append" (lambda (self x) (fw-append! self (render-piece x)) self))
(cons "flush" (lambda (self) (fw-flush! self) jolt-nil))
(cons "close" (lambda (self) (fw-flush! self) jolt-nil))
(cons "toString" (lambda (self) (fw-buf self)))))
;; a writer over a real Chez port — the values *out* / *err* hold. write/append
;; push to the port (so (.write *out* s) and (binding [*out* *err*] …) work);
;; it isn't a buffer, so toString is empty. Lets libraries that touch *out*/*err*
;; (tools.logging, selmer) compile and run.
(register-host-methods! "port-writer"
(list (cons "write" (lambda (self x) (display (writer-piece x) (vector-ref (jhost-state self) 0)) jolt-nil))
(cons "append" (lambda (self x) (display (render-piece x) (vector-ref (jhost-state self) 0)) self))
(cons "flush" (lambda (self) (flush-output-port (vector-ref (jhost-state self) 0)) jolt-nil))
(cons "close" (lambda (self) jolt-nil))
(cons "toString" (lambda (self) ""))))
(def-var! "clojure.core" "*out*" (make-jhost "port-writer" (vector (current-output-port))))
(def-var! "clojure.core" "*err*" (make-jhost "port-writer" (vector (current-error-port))))
;; ---- java.util.HashMap ------------------------------------------------------
;; A mutable map keyed by jolt values (jolt-hash / jolt=2). State #(chez-hashtable).
;; Constructors: () | (capacity) | (capacity load-factor) [sizing args ignored] |
;; (Map m) [copy]. Enough of the Map surface for libraries that build a fast lookup
;; (malli's fast-registry: (doto (HashMap. 1024 0.25) (.putAll m)) then .get).
(define (hm-hash k) (let ((h (jolt-hash k)))
(bitwise-and (if (and (integer? h) (exact? h)) (abs h) 0) #x3FFFFFFF)))
(define (hm-tbl self) (vector-ref (jhost-state self) 0))
(define (hm-hashmap? x) (and (jhost? x) (string=? (jhost-tag x) "hashmap")))
(define (hm-copy-into! ht src) ; src: a jolt map or another hashmap
(if (hm-hashmap? src)
(vector-for-each (lambda (k) (hashtable-set! ht k (hashtable-ref (hm-tbl src) k jolt-nil)))
(hashtable-keys (hm-tbl src)))
(for-each (lambda (e) (hashtable-set! ht (jolt-nth e 0) (jolt-nth e 1)))
(seq->list (jolt-seq src)))))
(register-class-ctor! "HashMap"
(lambda args
(let ((ht (make-hashtable hm-hash jolt=2)))
(when (and (pair? args) (or (pmap? (car args)) (hm-hashmap? (car args))))
(hm-copy-into! ht (car args)))
(make-jhost "hashmap" (vector ht)))))
(define (hm->pmap self)
(let ((m (jolt-hash-map)))
(vector-for-each (lambda (k) (set! m (jolt-assoc m k (hashtable-ref (hm-tbl self) k jolt-nil))))
(hashtable-keys (hm-tbl self)))
m))
(register-host-methods! "hashmap"
(list (cons "put" (lambda (self k v) (let ((old (hashtable-ref (hm-tbl self) k jolt-nil)))
(hashtable-set! (hm-tbl self) k v) old)))
(cons "get" (lambda (self k) (hashtable-ref (hm-tbl self) k jolt-nil)))
(cons "getOrDefault" (lambda (self k d) (hashtable-ref (hm-tbl self) k d)))
(cons "containsKey" (lambda (self k) (if (hashtable-contains? (hm-tbl self) k) #t #f)))
(cons "containsValue" (lambda (self v)
(let ((found #f))
(vector-for-each (lambda (k) (when (jolt=2 v (hashtable-ref (hm-tbl self) k jolt-nil)) (set! found #t)))
(hashtable-keys (hm-tbl self))) found)))
(cons "size" (lambda (self) (hashtable-size (hm-tbl self))))
(cons "isEmpty" (lambda (self) (= 0 (hashtable-size (hm-tbl self)))))
(cons "remove" (lambda (self k) (let ((old (hashtable-ref (hm-tbl self) k jolt-nil)))
(hashtable-delete! (hm-tbl self) k) old)))
(cons "clear" (lambda (self) (hashtable-clear! (hm-tbl self)) jolt-nil))
(cons "putAll" (lambda (self m) (hm-copy-into! (hm-tbl self) m) jolt-nil))
(cons "keySet" (lambda (self) (apply jolt-hash-set (vector->list (hashtable-keys (hm-tbl self))))))
(cons "values" (lambda (self) (apply jolt-vector
(map (lambda (k) (hashtable-ref (hm-tbl self) k jolt-nil))
(vector->list (hashtable-keys (hm-tbl self)))))))
(cons "entrySet" (lambda (self) (jolt-seq (hm->pmap self))))
(cons "toString" (lambda (self) (jolt-pr-str (hm->pmap self))))))
;; ---- StringReader -----------------------------------------------------------
;; state: a vector #(string pos marked).
(register-class-ctor! "StringReader"
;; src is a String or a char[] ((StringReader. (char-array s)) — selmer's parser
;; reads templates this way); a char-array becomes the string of its chars.
(lambda (src . _)
(make-jhost "string-reader"
(vector (cond ((string? src) src)
((jolt-array? src) (apply string-append (map jolt-str-render-one (seq->list (jolt-seq src)))))
(else (jolt-str-render-one src)))
0 0))))
(define (sr-s self) (vector-ref (jhost-state self) 0))
(define (sr-pos self) (vector-ref (jhost-state self) 1))
(define (sr-pos! self p) (vector-set! (jhost-state self) 1 p))
(register-host-methods! "string-reader"
(list (cons "read" (lambda (self)
(let ((s (sr-s self)) (p (sr-pos self)))
(if (>= p (string-length s)) -1 ; EOF -> exact int -1 (= JVM)
(begin (sr-pos! self (+ p 1)) (->num (char->integer (string-ref s p))))))))
(cons "mark" (lambda (self . _) (vector-set! (jhost-state self) 2 (sr-pos self)) jolt-nil))
(cons "reset" (lambda (self) (sr-pos! self (vector-ref (jhost-state self) 2)) jolt-nil))
(cons "skip" (lambda (self n) (let ((n (jnum->exact n)))
(sr-pos! self (min (string-length (sr-s self)) (+ (sr-pos self) n))) (->num n))))
;; readLine: the next line without its terminator (\n or \r\n), nil at EOF —
;; what line-seq drives over a BufferedReader.
(cons "readLine"
(lambda (self)
(let ((s (sr-s self)) (p (sr-pos self)) (len (string-length (sr-s self))))
(if (>= p len) jolt-nil
(let scan ((i p))
(cond
((>= i len) (sr-pos! self len) (substring s p len))
((char=? (string-ref s i) #\newline)
(sr-pos! self (+ i 1))
(substring s p (if (and (> i p) (char=? (string-ref s (- i 1)) #\return)) (- i 1) i)))
(else (scan (+ i 1)))))))))
(cons "close" (lambda (self) jolt-nil))))
;; ---- PushbackReader ---------------------------------------------------------
;; state: a vector #(wrapped-reader pushed-list)
(register-class-ctor! "PushbackReader"
(lambda (rdr . _) (make-jhost "pushback-reader" (vector rdr '()))))
(define (read-unit r) ; read one code unit (flonum) from any reader, -1 at EOF
(record-method-dispatch r "read" jolt-nil))
(register-host-methods! "pushback-reader"
(list (cons "read" (lambda (self)
(let ((pushed (vector-ref (jhost-state self) 1)))
(if (pair? pushed)
(begin (vector-set! (jhost-state self) 1 (cdr pushed)) (car pushed))
(read-unit (vector-ref (jhost-state self) 0))))))
(cons "unread" (lambda (self ch)
(vector-set! (jhost-state self) 1
(cons (if (char? ch) (->num (char->integer ch)) ch) (vector-ref (jhost-state self) 1)))
jolt-nil))
(cons "close" (lambda (self) jolt-nil))))
;; ---- HashMap ----------------------------------------------------------------
;; state: a box holding an alist of (k . v), jolt= keyed.
(define (hm-alist self) (vector-ref (jhost-state self) 0))
(define (hm-set! self al) (vector-set! (jhost-state self) 0 al))
(define (hm-assoc al k v)
(let loop ((ps al) (acc '()) (hit #f))
(cond ((null? ps) (reverse (if hit acc (cons (cons k v) acc))))
((jolt=2 (caar ps) k) (loop (cdr ps) (cons (cons k v) acc) #t))
(else (loop (cdr ps) (cons (car ps) acc) hit)))))
(define (hm-get al k) (let loop ((ps al)) (cond ((null? ps) jolt-nil) ((jolt=2 (caar ps) k) (cdar ps)) (else (loop (cdr ps))))))
(define (coll->pairs m)
(if (jolt-nil? m) '()
(let loop ((s (jolt-seq m)) (acc '()))
(if (jolt-nil? s) (reverse acc)
(let ((e (seq-first s))) (loop (jolt-seq (seq-more s)) (cons (cons (jolt-nth e 0) (jolt-nth e 1)) acc)))))))
(register-class-ctor! "HashMap"
(lambda args
(let ((init (and (pair? args) (car args))))
(make-jhost "hashmap" (vector (if (and init (not (number? init))) (coll->pairs init) '()))))))
(register-host-methods! "hashmap"
(list (cons "get" (lambda (self k) (hm-get (hm-alist self) k)))
(cons "put" (lambda (self k v) (hm-set! self (hm-assoc (hm-alist self) k v)) v))
(cons "putAll" (lambda (self m) (for-each (lambda (p) (hm-set! self (hm-assoc (hm-alist self) (car p) (cdr p)))) (coll->pairs m)) jolt-nil))
(cons "containsKey" (lambda (self k) (not (jolt-nil? (hm-get (hm-alist self) k)))))
(cons "size" (lambda (self) (->num (length (hm-alist self)))))))
;; ---- StringTokenizer --------------------------------------------------------
;; state: a vector #(tokens-list pos)
(define (tokenize s delims)
(let ((dset (string->list delims)))
(let loop ((chars (string->list s)) (cur '()) (toks '()))
(cond ((null? chars) (reverse (if (null? cur) toks (cons (list->string (reverse cur)) toks))))
((memv (car chars) dset)
(loop (cdr chars) '() (if (null? cur) toks (cons (list->string (reverse cur)) toks))))
(else (loop (cdr chars) (cons (car chars) cur) toks))))))
(register-class-ctor! "StringTokenizer"
(lambda (s . delims) (make-jhost "string-tokenizer"
(vector (tokenize (if (string? s) s (jolt-str-render-one s))
(if (null? delims) " \t\n\r\f" (car delims))) 0))))
(register-host-methods! "string-tokenizer"
(list (cons "hasMoreTokens" (lambda (self) (< (vector-ref (jhost-state self) 1) (length (vector-ref (jhost-state self) 0)))))
(cons "countTokens" (lambda (self) (->num (- (length (vector-ref (jhost-state self) 0)) (vector-ref (jhost-state self) 1)))))
(cons "nextToken" (lambda (self)
(let ((toks (vector-ref (jhost-state self) 0)) (p (vector-ref (jhost-state self) 1)))
(if (< p (length toks))
(begin (vector-set! (jhost-state self) 1 (+ p 1)) (list-ref toks p))
(error #f "NoSuchElementException")))))))
;; ---- String / BigInteger / MapEntry constructors ----------------------------
;; (String. bytes [charset]) decodes bytes (a bytevector OR a jolt byte-array)
;; with the named charset (UTF-8 default; ISO-8859-1/latin1/ascii = one byte per
;; char); else stringify. clj-http-lite's body coercion is (String. ^[B body cs).
(define (string-charset-name rest)
(if (pair? rest)
(let ((c (car rest)))
(cond ((string? c) c)
((and (jhost? c) (string=? (jhost-tag c) "charset"))
(let ((p (assq 'name (jhost-state c)))) (if p (jolt-str-render-one (cdr p)) "UTF-8")))
(else "UTF-8")))
"UTF-8"))
(define (decode-bytevector bv rest)
(let ((cs (ascii-string-down (string-charset-name rest))))
(cond
((or (string=? cs "utf-8") (string=? cs "utf8")) (utf8->string bv))
((or (string=? cs "iso-8859-1") (string=? cs "latin1") (string=? cs "iso8859-1")
(string=? cs "us-ascii") (string=? cs "ascii"))
(list->string (map integer->char (bytevector->u8-list bv))))
((or (string=? cs "utf-16") (string=? cs "utf16") (string=? cs "utf-16be") (string=? cs "unicode"))
(utf16->string bv (endianness big))) ; respects a leading BOM
((string=? cs "utf-16le") (utf16->string bv (endianness little)))
((or (string=? cs "utf-32") (string=? cs "utf32") (string=? cs "utf-32be"))
(utf32->string bv (endianness big)))
((string=? cs "utf-32le") (utf32->string bv (endianness little)))
(else (guard (e (#t (list->string (map integer->char (bytevector->u8-list bv))))) (utf8->string bv))))))
(register-class-ctor! "String"
(lambda (x . rest)
(cond ((bytevector? x) (decode-bytevector x rest))
((and (jolt-array? x) (eq? (jolt-array-kind x) 'byte)) (decode-bytevector (na-bytearray->bv x) rest))
((string? x) x)
(else (jolt-str-render-one x)))))
(register-class-ctor! "BigInteger"
(lambda (v) (parse-int-or-throw v 10 "BigInteger")))
(register-class-ctor! "MapEntry" (lambda (k v) (make-map-entry k v)))
;; JVM exception ctors -> a typed host throwable carrying the canonical :jolt/class
;; (so class / instance? / getMessage / ex-message reflect the real type) and the
;; message. Supports (E. msg), (E. msg cause), (E. cause), and (E.).
(for-each
(lambda (nm)
(let ((canonical (or (resolve-class-hint nm) nm)))
(register-class-ctor! nm
(lambda args
(let* ((a0 (if (pair? args) (car args) jolt-nil))
(rest (if (pair? args) (cdr args) '()))
(cause (if (pair? rest) (car rest) jolt-nil)))
(cond
((string? a0) (jolt-host-throwable canonical a0 cause))
((jolt-nil? a0) (jolt-host-throwable canonical jolt-nil))
;; (E. cause): a lone throwable arg is the cause, message nil.
((and (null? rest) (ex-info-map? a0)) (jolt-host-throwable canonical jolt-nil a0))
(else (jolt-host-throwable canonical (jolt-str-render-one a0) cause))))))))
'("Throwable" "Exception" "RuntimeException" "IllegalArgumentException" "IllegalStateException"
"InterruptedException" "UnsupportedOperationException" "IOException" "NumberFormatException"
"ArithmeticException" "NullPointerException" "ClassCastException" "IndexOutOfBoundsException"
"FileNotFoundException" "UnsupportedEncodingException"))
;; ---- URLEncoder / URLDecoder (www-form-urlencoded) --------------------------
(define (url-unreserved? b)
(or (and (>= b 48) (<= b 57)) (and (>= b 65) (<= b 90)) (and (>= b 97) (<= b 122))
(= b 46) (= b 42) (= b 95) (= b 45)))
(define hex-digits "0123456789ABCDEF")
(define (url-encode s . _)
(let ((bs (string->utf8 (if (string? s) s (jolt-str-render-one s)))) (out '()))
(let loop ((i 0))
(if (= i (bytevector-length bs)) (list->string (reverse out))
(let ((b (bytevector-u8-ref bs i)))
(cond ((url-unreserved? b) (set! out (cons (integer->char b) out)))
((= b 32) (set! out (cons #\+ out)))
(else (set! out (cons (string-ref hex-digits (bitwise-and b 15))
(cons (string-ref hex-digits (bitwise-arithmetic-shift-right b 4))
(cons #\% out))))))
(loop (+ i 1)))))))
(define (hexv c)
(cond ((and (char<=? #\0 c) (char<=? c #\9)) (- (char->integer c) 48))
((and (char<=? #\A c) (char<=? c #\F)) (- (char->integer c) 55))
((and (char<=? #\a c) (char<=? c #\f)) (- (char->integer c) 87))
(else (error #f "URLDecoder: malformed escape"))))
(define (url-decode s . _)
(let* ((str (if (string? s) s (jolt-str-render-one s))) (n (string-length str)) (out '()))
(let loop ((i 0))
(if (>= i n) (utf8->string (u8-list->bytevector (reverse out)))
(let ((c (string-ref str i)))
(cond ((char=? c #\+) (set! out (cons 32 out)) (loop (+ i 1)))
((char=? c #\%)
(set! out (cons (+ (* 16 (hexv (string-ref str (+ i 1)))) (hexv (string-ref str (+ i 2)))) out))
(loop (+ i 3)))
(else (set! out (cons (char->integer c) out)) (loop (+ i 1)))))))))
(define (u8-list->bytevector lst)
(let ((bv (make-bytevector (length lst))))
(let loop ((l lst) (i 0)) (if (null? l) bv (begin (bytevector-u8-set! bv i (car l)) (loop (cdr l) (+ i 1)))))))
(register-class-statics! "URLEncoder" (list (cons "encode" url-encode)))
(register-class-statics! "URLDecoder" (list (cons "decode" url-decode)))
;; Charset/forName yields the canonical name STRING (not an opaque object) so it
;; threads straight into (.getBytes s cs) / (String. bytes cs), which take a name.
(register-class-statics! "Charset" (list (cons "forName" (lambda (nm) (jolt-str-render-one nm)))))
;; ---- Base64 (RFC 4648) ------------------------------------------------------
(define b64-alphabet "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
(define (->bytevector x)
(cond ((bytevector? x) x)
((and (jolt-array? x) (eq? (jolt-array-kind x) 'byte)) (na-bytearray->bv x))
((string? x) (string->utf8 x))
(else (string->utf8 (jolt-str-render-one x)))))
(define (b64-encode x)
(let* ((bs (->bytevector x)) (n (bytevector-length bs)) (out '()))
(let loop ((i 0))
(if (>= i n) (list->string (reverse out))
(let* ((b0 (bytevector-u8-ref bs i))
(b1 (if (< (+ i 1) n) (bytevector-u8-ref bs (+ i 1)) #f))
(b2 (if (< (+ i 2) n) (bytevector-u8-ref bs (+ i 2)) #f)))
(set! out (cons (string-ref b64-alphabet (bitwise-arithmetic-shift-right b0 2)) out))
(set! out (cons (string-ref b64-alphabet (bitwise-ior (bitwise-arithmetic-shift-left (bitwise-and b0 3) 4)
(bitwise-arithmetic-shift-right (or b1 0) 4))) out))
(set! out (cons (if b1 (string-ref b64-alphabet (bitwise-ior (bitwise-arithmetic-shift-left (bitwise-and b1 15) 2)
(bitwise-arithmetic-shift-right (or b2 0) 6))) #\=) out))
(set! out (cons (if b2 (string-ref b64-alphabet (bitwise-and b2 63)) #\=) out))
(loop (+ i 3)))))))
(define (b64-char-val c)
(let loop ((i 0)) (cond ((= i 64) (error #f "Base64: illegal character")) ((char=? (string-ref b64-alphabet i) c) i) (else (loop (+ i 1))))))
(define (b64-decode x)
(let* ((str (let ((s (if (string? x) x (utf8->string (->bytevector x)))))
(list->string (filter (lambda (c) (not (char=? c #\=))) (string->list s)))))
(out '()) (acc 0) (bits 0))
(for-each (lambda (c)
(set! acc (bitwise-ior (bitwise-arithmetic-shift-left acc 6) (b64-char-val c)))
(set! bits (+ bits 6))
(when (>= bits 8)
(set! bits (- bits 8))
(set! out (cons (bitwise-and (bitwise-arithmetic-shift-right acc bits) 255) out))))
(string->list str))
(u8-list->bytevector (reverse out))))
(register-host-methods! "b64-encoder"
(list (cons "encode" (lambda (self bs) (string->utf8 (b64-encode bs))))
(cons "encodeToString" (lambda (self bs) (b64-encode bs)))))
(register-host-methods! "b64-decoder"
(list (cons "decode" (lambda (self s) (b64-decode s)))))
(register-class-statics! "Base64"
(list (cons "getEncoder" (lambda () (make-jhost "b64-encoder" '())))
(cons "getDecoder" (lambda () (make-jhost "b64-decoder" '())))))
;; ---- java.util.regex.Pattern ------------------------------------------------
;; Pattern/compile returns a jolt-regex value (regex-t), so str/replace, re-find,
;; .split etc. accept it transparently.
(define pattern-multiline 8.0)
(define (pattern-quote s)
(let ((meta "\\.[]{}()*+-?^$|&") (s (if (string? s) s (jolt-str-render-one s))) (out '()))
(let loop ((i 0))
(if (= i (string-length s)) (list->string (reverse out))
(let ((c (string-ref s i)))
(when (memv c (string->list meta)) (set! out (cons #\\ out)))
(set! out (cons c out))
(loop (+ i 1)))))))
(register-class-statics! "Pattern"
(list (cons "compile" (lambda (s . flags)
(if (and (pair? flags) (= (bitwise-and (jnum->exact (car flags)) 8) 8))
(jolt-regex (string-append "(?m)" s))
(jolt-regex s))))
(cons "quote" (lambda (s) (pattern-quote s)))
(cons "MULTILINE" pattern-multiline)))
;; record-method-dispatch already routes string? -> jolt-string-method. Add a
;; regex-t arm (Pattern .split / .matcher-less surface used by corpus) by wrapping
;; once more — a regex-t isn't a jhost.
(define %hs-rmd2 record-method-dispatch)
(set! record-method-dispatch
(lambda (obj method-name rest-args)
(if (regex-t? obj)
(let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args))))
(cond ((string=? method-name "split")
;; .split returns a String[] — a seq (prints
;; (a b c), not a vector). re-split with no limit; drop trailing
;; empties (JVM default).
(let ((parts (re-split (regex-t-irx obj) (car rest) #f)))
(list->cseq (str-split-drop-trailing parts))))
((string=? method-name "pattern") (regex-t-source obj))
(else (error #f (string-append "No method " method-name " on Pattern")))))
(%hs-rmd2 obj method-name rest-args))))
;; ---- def-var! the registry entry points so emit can also reach them ---------
(def-var! "clojure.core" "host-static-ref" host-static-ref)
(def-var! "clojure.core" "host-static-call" (lambda (c m . a) (apply host-static-call c m a)))
(def-var! "clojure.core" "host-new" (lambda (c . a) (apply host-new c a)))
;; Clojure-visible class-registration hooks. A host shim (e.g. reitit.trie-jolt,
;; which mirrors the reitit.Trie Java class) registers a constructor proc or a
;; map of static members against a class token so (Class. args) / (Class/member
;; args) resolve to it. The statics argument is a jolt map {member-name -> val}.
(define (jmap->static-alist m)
(let loop ((s (jolt-seq m)) (acc '()))
(if (jolt-nil? s) acc
(let ((e (jolt-first s)))
(loop (jolt-seq (jolt-rest s)) (cons (cons (jolt-nth e 0) (jolt-nth e 1)) acc))))))
(def-var! "clojure.core" "__register-class-ctor!"
(lambda (name proc) (register-class-ctor! name proc) jolt-nil))
(def-var! "clojure.core" "__register-class-statics!"
(lambda (name members) (register-class-statics! name (jmap->static-alist members)) jolt-nil))
;; ---- tagged-table method dispatch + pluggable instance? --------------------
;; A jolt library can build stateful host objects with (jolt.host/tagged-table
;; tag) and dispatch (.method obj ...) to handlers registered here, keyed by the
;; table's "jolt/type" tag — the htable analogue of the jhost method registry
;; above. jolt-lang/http-client uses this to emulate java.net URL /
;; HttpURLConnection / java.io byte streams so clj-http-lite runs unchanged.
(define tagged-methods-tbl (make-hashtable string-hash string=?)) ; tag-key -> (method-ht)
(define (tag->method-key tag)
(if (keyword-t? tag)
(let ((ns (keyword-t-ns tag)))
(if (and ns (not (jolt-nil? ns))) (string-append ns "/" (keyword-t-name tag)) (keyword-t-name tag)))
(jolt-str-render-one tag)))
(define (register-tagged-methods! tag members)
(let* ((key (tag->method-key tag))
(h (or (hashtable-ref tagged-methods-tbl key #f)
(let ((nh (make-hashtable string-hash string=?)))
(hashtable-set! tagged-methods-tbl key nh) nh))))
(for-each (lambda (p) (hashtable-set! h (car p) (cdr p))) members)))
;; htable arm: dispatch (.method obj a*) through the table's tag method registry;
;; an unregistered method falls through (sorted colls are htables too).
(define %hs-rmd-htable record-method-dispatch)
(set! record-method-dispatch
(lambda (obj method-name rest-args)
(let ((tag (and (htable? obj) (hashtable-ref (htable-h obj) "jolt/type" #f))))
(let* ((mh (and tag (hashtable-ref tagged-methods-tbl (tag->method-key tag) #f)))
(f (and mh (hashtable-ref mh method-name #f))))
(if f
(apply f obj (if (jolt-nil? rest-args) '() (seq->list rest-args)))
(%hs-rmd-htable obj method-name rest-args))))))
(def-var! "clojure.core" "__register-class-methods!"
(lambda (tag members) (register-tagged-methods! tag (jmap->static-alist members)) jolt-nil))
;; Pluggable instance? — a library registers (fn [class-name-string val] -> true
;; | false | nil); nil means "not my class, fall through". First non-nil wins.
(define user-instance-checks '())
(define %hs-instance-check instance-check)
(set! instance-check
(lambda (type-sym val)
(let ((tname (symbol-t-name type-sym)))
(let loop ((fs user-instance-checks))
(if (null? fs)
(%hs-instance-check type-sym val)
(let ((r ((car fs) tname val)))
(if (jolt-nil? r) (loop (cdr fs)) (if (jolt-truthy? r) #t #f))))))))
(def-var! "clojure.core" "instance-check" instance-check)
(def-var! "clojure.core" "__register-instance-check!"
(lambda (f) (set! user-instance-checks (append user-instance-checks (list f))) jolt-nil))
;; (jolt.host/table? x) — is x a host tagged-table?
(def-var! "jolt.host" "table?" (lambda (x) (if (htable? x) #t #f)))

View file

@ -160,8 +160,7 @@
(set! jolt-pr-readable (lambda (x) (if (htable-sorted? x) (sorted-render x jolt-pr-readable) (%h-pr-readable x))))
(define %h-pr-str jolt-pr-str)
(set! jolt-pr-str (lambda (x) (if (htable-sorted? x) (sorted-render x jolt-pr-str) (%h-pr-str x))))
(define %h-str-render-one jolt-str-render-one)
(set! jolt-str-render-one (lambda (x) (if (htable-sorted? x) (sorted-render x jolt-str-render-one) (%h-str-render-one x))))
(register-str-render! htable-sorted? (lambda (x) (sorted-render x jolt-str-render-one)))
;; --- protocol dispatch over builtins (extend-protocol Map/Set on sorted) ------
;; value-host-tags (records.ss) drives extend-protocol on host values; a

View file

@ -93,16 +93,16 @@
(define (pad2 n) (if (< n 10) (string-append "0" (number->string n)) (number->string n)))
(define (pad4 n) (let ((s (number->string n))) (string-append (make-string (max 0 (- 4 (string-length s))) #\0) s)))
(define (pad3 n) (let ((s (number->string n))) (string-append (make-string (max 0 (- 3 (string-length s))) #\0) s)))
(define (floor-div a b) (let ((q (quotient a b)) (r (remainder a b))) (if (and (not (= r 0)) (< (* a b) 0)) (- q 1) q)))
(define (floor-mod a b) (- a (* (floor-div a b) b)))
(define (inst-floor-div a b) (let ((q (quotient a b)) (r (remainder a b))) (if (and (not (= r 0)) (< (* a b) 0)) (- q 1) q)))
(define (inst-floor-mod a b) (- a (* (inst-floor-div a b) b)))
(define (inst-fields ms) ; -> list (y mo d hh mm ss frac dow)
(let* ((total-s (floor-div (exact (truncate ms)) 1000))
(let* ((total-s (inst-floor-div (exact (truncate ms)) 1000))
(frac (- (exact (truncate ms)) (* total-s 1000)))
(days (floor-div total-s 86400))
(sod (floor-mod total-s 86400))
(days (inst-floor-div total-s 86400))
(sod (inst-floor-mod total-s 86400))
(hh (quotient sod 3600)) (mm (quotient (remainder sod 3600) 60)) (ss (remainder sod 60))
(dow (floor-mod (+ days 4) 7))) ; 1970-01-01 = Thursday; 0=Sunday
(dow (inst-floor-mod (+ days 4) 7))) ; 1970-01-01 = Thursday; 0=Sunday
(call-with-values (lambda () (civil-from-days days))
(lambda (y mo d) (list y mo d hh mm ss frac dow)))))
@ -255,8 +255,7 @@
(set! jolt-pr-str (lambda (x) (if (jinst? x) (inst-pr x) (%it-pr-str x))))
(define %it-pr-readable jolt-pr-readable)
(set! jolt-pr-readable (lambda (x) (if (jinst? x) (inst-pr x) (%it-pr-readable x))))
(define %it-str-render jolt-str-render-one)
(set! jolt-str-render-one (lambda (x) (if (jinst? x) (inst-rfc3339 x) (%it-str-render x))))
(register-str-render! jinst? inst-rfc3339)
(define %it-type jolt-type)
(set! jolt-type (lambda (x) (if (jinst? x) inst-type-kw (%it-type x))))
@ -266,8 +265,7 @@
;; matching jhost tag. The instance? macro passes the class-name symbol.
(define (class-short tn) (let loop ((i (- (string-length tn) 1)))
(cond ((< i 0) tn) ((char=? (string-ref tn i) #\.) (substring tn (+ i 1) (string-length tn))) (else (loop (- i 1))))))
(define %it-instance-check instance-check)
(set! instance-check
(register-instance-check-arm!
(lambda (type-sym val)
(let ((tn (class-short (symbol-t-name type-sym))))
(cond
@ -275,11 +273,10 @@
;; (on the JVM a Date is not a Timestamp), so answer Timestamp explicitly #f.
((jinst? val) (cond ((string=? tn "Date") #t)
((string=? tn "Timestamp") #f)
(else (%it-instance-check type-sym val))))
((and (jhost? val) (string=? (jhost-tag val) "instant")) (if (string=? tn "Instant") #t (%it-instance-check type-sym val)))
((and (jhost? val) (string=? (jhost-tag val) "local-dt")) (if (string=? tn "LocalDateTime") #t (%it-instance-check type-sym val)))
(else (%it-instance-check type-sym val))))))
(def-var! "clojure.core" "instance-check" instance-check)
(else 'pass)))
((and (jhost? val) (string=? (jhost-tag val) "instant")) (if (string=? tn "Instant") #t 'pass))
((and (jhost? val) (string=? (jhost-tag val) "local-dt")) (if (string=? tn "LocalDateTime") #t 'pass))
(else 'pass)))))
;; inst-ms* is a seed native (the overlay inst-ms reads (get x :ms), now answered).
(def-var! "clojure.core" "inst-ms*" (lambda (i) (jinst-ms i)))

View file

@ -221,9 +221,7 @@
;; --- str / type / instance? integration ------------------------------------
;; str of a jfile is its path (Clojure's File.toString).
(define %io-str-render jolt-str-render-one)
(set! jolt-str-render-one
(lambda (v) (if (jfile? v) (jfile-path v) (%io-str-render v))))
(register-str-render! jfile? jfile-path)
;; stdin line seam: the clojure.core *in* reader (50-io.clj) drives read-line /
;; read / read+string through __stdin-read-line. Return the next line (newline
@ -241,16 +239,14 @@
;; (instance? java.io.File f): the instance? macro passes the class-name symbol;
;; match "File" / "java.io.File" (and any *.File) against a jfile.
(define %io-instance-check instance-check)
(set! instance-check
(register-instance-check-arm!
(lambda (type-sym val)
(let ((tname (symbol-t-name type-sym)))
(if (and (jfile? val)
(or (string=? tname "File") (string=? tname "java.io.File")
(string=? (path-last-segment tname) "File")))
#t
(%io-instance-check type-sym val)))))
(def-var! "clojure.core" "instance-check" instance-check)
'pass))))
;; --- def-var! the native names the overlay file-seq + str/slurp use ----
(def-var! "clojure.core" "__make-file" jolt-make-file)
@ -261,18 +257,6 @@
(def-var! "clojure.core" "spit" jolt-spit)
(def-var! "clojure.core" "flush" jolt-flush)
;; --- char-array: a seq of chars over a string (the JVM char[]). io/reader's
;; char[] branch + selmer's (char-array template) feed on this.
;; char-array (string -> chars). A leaf array native; lives here as io/reader
;; is its only Chez consumer so far.
(define (jolt-char-array a . rest)
(cond
((string? a) (list->cseq (string->list a)))
((number? a) (list->cseq (make-list (exact (truncate a)) #\nul)))
(else (list->cseq (map (lambda (c) (if (char? c) c (integer->char (exact (truncate c)))))
(seq->list a))))))
(def-var! "clojure.core" "char-array" jolt-char-array)
;; --- with-open's close seam (__close): a map-like value closes via its :close
;; fn; a jhost reader/writer/file via its .close method (a no-op here); anything
;; else is an error.
@ -323,7 +307,8 @@
;; --- clojure.java.io ns -----------------------------------------------------
(def-var! "clojure.java.io" "file" jolt-make-file)
(def-var! "clojure.java.io" "as-file" (lambda (x) (if (jfile? x) x (make-jfile (file-path-of x)))))
(def-var! "clojure.java.io" "reader" jolt-io-reader)
;; "reader" is bound by natives-array.ss (loaded later) so a char[] argument is
;; handled; that binding delegates here via jolt-io-reader for everything else.
(def-var! "clojure.java.io" "writer" jolt-io-writer)
(def-var! "clojure.java.io" "input-stream" jolt-io-reader)
(def-var! "clojure.java.io" "output-stream" jolt-io-writer)
@ -486,9 +471,8 @@
(cons "equals" (lambda (u o) (and (jhost? o) (string=? (jhost-tag o) "uri")
(string=? (uri-field u 'string) (uri-field o 'string)))))))
;; str / pr-str of a uri -> its string form.
(define %uri-str-render-one jolt-str-render-one)
(set! jolt-str-render-one
(lambda (x) (if (and (jhost? x) (string=? (jhost-tag x) "uri")) (uri-field x 'string) (%uri-str-render-one x))))
(register-str-render! (lambda (x) (and (jhost? x) (string=? (jhost-tag x) "uri")))
(lambda (x) (uri-field x 'string)))
(define %uri-pr-readable jolt-pr-readable)
(set! jolt-pr-readable
(lambda (x) (if (and (jhost? x) (string=? (jhost-tag x) "uri"))

View file

@ -69,8 +69,7 @@
(set! jolt-pr-str (lambda (x) (if (jolt-lazyseq? x) (%ls-pr-str (jolt-seq x)) (%ls-pr-str x))))
(define %ls-pr-readable jolt-pr-readable)
(set! jolt-pr-readable (lambda (x) (if (jolt-lazyseq? x) (%ls-pr-readable (jolt-seq x)) (%ls-pr-readable x))))
(define %ls-str-render-one jolt-str-render-one)
(set! jolt-str-render-one (lambda (x) (if (jolt-lazyseq? x) (%ls-str-render-one (jolt-seq x)) (%ls-str-render-one x))))
(register-str-render! jolt-lazyseq? (lambda (x) (jolt-str-render-one (jolt-seq x))))
;; seq? — a lazy seq IS a seq (predicates.ss's jolt-seq? predates the lazyseq
;; record). Unlike the native-op dispatchers above (called via a direct top-level

View file

@ -35,15 +35,17 @@
(loop (cdr cs) '() (cons (list->string (reverse seg)) segs)))
(else (loop (cdr cs) (cons (car cs) seg) segs)))))
(define (find-ns-file name)
(let ((rel (ns-name->rel name)))
(let loop ((roots source-roots))
(if (null? roots) #f
(let ((clj (string-append (car roots) "/" rel ".clj"))
(cljc (string-append (car roots) "/" rel ".cljc")))
(cond ((file-exists? clj) clj)
((file-exists? cljc) cljc)
(else (loop (cdr roots)))))))))
;; First existing <root>/rel.clj or <root>/rel.cljc on the search roots, else #f.
(define (resolve-on-roots rel)
(let loop ((roots source-roots))
(if (null? roots) #f
(let ((clj (string-append (car roots) "/" rel ".clj"))
(cljc (string-append (car roots) "/" rel ".cljc")))
(cond ((file-exists? clj) clj)
((file-exists? cljc) cljc)
(else (loop (cdr roots))))))))
(define (find-ns-file name) (resolve-on-roots (ns-name->rel name)))
;; --- the loaded set ---------------------------------------------------------
;; Seeded with every namespace that already has vars at load time — the baked
@ -170,15 +172,10 @@
(for-each
(lambda (p)
(let* ((rel (if (and (> (string-length p) 0) (char=? (string-ref p 0) #\/))
(substring p 1 (string-length p)) p)))
(let loop ((roots source-roots))
(if (null? roots)
(error #f "Could not locate resource on source roots" p)
(let ((clj (string-append (car roots) "/" rel ".clj"))
(cljc (string-append (car roots) "/" rel ".cljc")))
(cond ((file-exists? clj) (load-jolt-file clj))
((file-exists? cljc) (load-jolt-file cljc))
(else (loop (cdr roots)))))))))
(substring p 1 (string-length p)) p))
(f (resolve-on-roots rel)))
(if f (load-jolt-file f)
(error #f "Could not locate resource on source roots" p))))
paths)
jolt-nil)
(def-var! "clojure.core" "load" jolt-load)

View file

@ -158,21 +158,18 @@
(set! jolt-type (lambda (x) (if (jolt-array? x) (na-array-class-name x) (%na-type x))))
(def-var! "clojure.core" "type" jolt-type)
;; instance? over an array class token ([I, [C, …). The token reaches us as a
;; string (Class/forName "[C") or symbol; normalize, and pass a non-array string
;; token on as a symbol so the inner wrappers' symbol-t-name doesn't choke.
(define %na-instance-check instance-check)
(set! instance-check
;; instance? over an array class token ([I, [C, …). An array token reaches us as
;; a string ("[C", from (Class/forName "[C")) — the dispatcher leaves it a string
;; (non-array string tokens are already normalized to symbols there); decide it
;; here, deferring everything else.
(register-instance-check-arm!
(lambda (type-sym val)
(let ((tname (cond ((string? type-sym) type-sym)
((symbol-t? type-sym) (symbol-t-name type-sym))
(else #f))))
(cond
((and tname (> (string-length tname) 0) (char=? (string-ref tname 0) #\[))
(and (jolt-array? val) (string=? (na-array-class-name val) tname)))
((string? type-sym) (%na-instance-check (jolt-symbol #f type-sym) val))
(else (%na-instance-check type-sym val))))))
(def-var! "clojure.core" "instance-check" instance-check)
(if (and tname (> (string-length tname) 0) (char=? (string-ref tname 0) #\[))
(and (jolt-array? val) (string=? (na-array-class-name val) tname))
'pass))))
;; clojure.java.io/reader over a char-array reads its chars (the JVM char[] branch).
(def-var! "clojure.java.io" "reader"

View file

@ -51,8 +51,7 @@
;; the prelude would clobber a def-var! here — they're asserted in post-prelude.ss.
;; str of a uuid -> the bare 36-char string; pr-str -> #uuid "…".
(define %m-str-render-one jolt-str-render-one)
(set! jolt-str-render-one (lambda (x) (if (juuid? x) (juuid-s x) (%m-str-render-one x))))
(register-str-render! juuid? juuid-s)
(define (juuid-pr u) (string-append "#uuid \"" (juuid-s u) "\""))
(define %m-pr-str jolt-pr-str)
(set! jolt-pr-str (lambda (x) (if (juuid? x) (juuid-pr x) (%m-pr-str x))))

View file

@ -46,15 +46,13 @@
(define (jolt-seq-or-empty x) (let ((s (jolt-seq x))) (if (jolt-nil? s) jolt-empty-list s)))
(define %q-pr-readable jolt-pr-readable)
(set! jolt-pr-readable (lambda (x) (if (jolt-queue? x) (%q-pr-readable (jolt-seq-or-empty x)) (%q-pr-readable x))))
(define %q-str-render-one jolt-str-render-one)
(set! jolt-str-render-one (lambda (x) (if (jolt-queue? x) (%q-str-render-one (jolt-seq-or-empty x)) (%q-str-render-one x))))
(register-str-render! jolt-queue? (lambda (x) (jolt-str-render-one (jolt-seq-or-empty x))))
;; class / type / instance? recognize a queue.
(define %q-class jolt-class)
(set! jolt-class (lambda (x) (if (jolt-queue? x) "clojure.lang.PersistentQueue" (%q-class x))))
(def-var! "clojure.core" "class" jolt-class)
(define %q-instance-check instance-check)
(set! instance-check
(register-instance-check-arm!
(lambda (type-sym val)
(if (jolt-queue? val)
(let ((tn (cond ((string? type-sym) type-sym)
@ -62,8 +60,7 @@
(and (member (last-dot tn)
'("PersistentQueue" "IPersistentCollection" "Sequential" "Collection" "Object"))
#t))
(%q-instance-check type-sym val))))
(def-var! "clojure.core" "instance-check" instance-check)
'pass)))
;; clojure.lang.PersistentQueue/EMPTY + a queue? predicate.
(register-class-statics! "PersistentQueue" (list (cons "EMPTY" jolt-queue-empty)))

View file

@ -196,13 +196,10 @@
(if (fx<? i 0) jolt-nil i)))
;; (str-join coll [sep]) -> stringify each element (Clojure str), join by sep.
;; str-join-strs (defined below) does the join; here we just render each element.
(define (str-join coll . opt)
(let ((sep (if (pair? opt) (jolt-str-render-one (car opt)) ""))
(items (map jolt-str-render-one (seq->list coll))))
(let loop ((xs items) (first #t) (acc '()))
(cond ((null? xs) (apply string-append (reverse acc)))
(first (loop (cdr xs) #f (cons (car xs) acc)))
(else (loop (cdr xs) #f (cons (car xs) (cons sep acc))))))))
(let ((sep (if (pair? opt) (jolt-str-render-one (car opt)) "")))
(str-join-strs (map jolt-str-render-one (seq->list coll)) sep)))
;; (re-split irx s limit) -> parts, splitting at each match. Keeps interior AND
;; trailing empty strings (the clojure.string wrapper drops trailing for limit 0);

View file

@ -348,5 +348,4 @@
(set! jolt-pr-str (lambda (x) (if (jns? x) (jns-name x) (%ns-pr-str x))))
(define %ns-pr-readable jolt-pr-readable)
(set! jolt-pr-readable (lambda (x) (if (jns? x) (jns-name x) (%ns-pr-readable x))))
(define %ns-str-render-one jolt-str-render-one)
(set! jolt-str-render-one (lambda (x) (if (jns? x) (jns-name x) (%ns-str-render-one x))))
(register-str-render! jns? jns-name)

View file

@ -0,0 +1,115 @@
;; records-interop.ss — JVM-emulation taxonomy split out of records.ss: the
;; ex-info class accessors, the exception supertype hierarchy, and instance-check
;; / case-string (the (instance? Class x) decision table). Loaded right after
;; records.ss; instance-check forward-refs nothing in records.ss at load time.
;; pmap? guard: ex-info maps are plain hash-maps, never sorted-map htables — and a
;; bare jolt-get on a sorted-map would invoke its comparator on :jolt/type and throw.
(define (ex-info-map? v)
(and (pmap? v) (jolt=2 (jolt-get v jolt-kw-ex-type jolt-nil) jolt-kw-ex-info)))
(define (ex-info-class v)
(let ((c (jolt-get v jolt-kw-class jolt-nil)))
(if (string? c) c "clojure.lang.ExceptionInfo")))
;; immediate-parent chain of the JVM exception hierarchy (simple names). Drives
;; instance? across exception supertypes — (instance? Throwable (ex-info …)) etc.
(define exception-parent
'(("ExceptionInfo" . "RuntimeException")
("RuntimeException" . "Exception")
("IllegalArgumentException" . "RuntimeException")
("NumberFormatException" . "IllegalArgumentException")
("IllegalStateException" . "RuntimeException")
("UnsupportedOperationException" . "RuntimeException")
("ArithmeticException" . "RuntimeException")
("NullPointerException" . "RuntimeException")
("ClassCastException" . "RuntimeException")
("IndexOutOfBoundsException" . "RuntimeException")
("ConcurrentModificationException" . "RuntimeException")
("NoSuchElementException" . "RuntimeException")
("UncheckedIOException" . "RuntimeException")
("InterruptedException" . "Exception")
("IOException" . "Exception")
("FileNotFoundException" . "IOException")
("UnsupportedEncodingException" . "IOException")
("UnknownHostException" . "IOException")
("SocketException" . "IOException")
("ConnectException" . "IOException")
("SocketTimeoutException" . "IOException")
("MalformedURLException" . "IOException")
("SSLException" . "IOException")
("Exception" . "Throwable")
("Error" . "Throwable")
("AssertionError" . "Error")
("Throwable" . "Object")))
;; Is `wanted` (simple name) `cls` or a supertype of it? ExceptionInfo also
;; implements the IExceptionInfo interface.
(define (exception-isa? cls wanted)
(let loop ((c cls))
(cond ((not c) #f)
((string=? c wanted) #t)
((and (string=? c "ExceptionInfo") (string=? wanted "IExceptionInfo")) #t)
(else (let ((p (assoc c exception-parent))) (loop (and p (cdr p))))))))
;; instance-check: (type-sym val) — type/protocol membership. Host shims loaded
;; later (io, inst-time, natives-array, natives-queue, host-static-objects)
;; register an arm with register-instance-check-arm! instead of set!-wrapping
;; instance-check; an arm returns #t/#f to decide or 'pass to defer to the next.
;; Newest arm is checked first (matches the old outermost-wins set! order).
;; instance-check-base is the JVM taxonomy fallback when no arm decides.
(define instance-check-registry '())
(define (register-instance-check-arm! f) ; f: (type-sym val) -> #t | #f | 'pass
(set! instance-check-registry (cons f instance-check-registry)))
(define (instance-check-base type-sym val)
(let ((tname (symbol-t-name type-sym)))
(cond
((jrec? val)
(let ((tag (jrec-tag val)))
(or (string=? tag tname)
(and (> (string-length tag) (string-length tname))
(string=? (substring tag (- (string-length tag) (string-length tname)) (string-length tag)) tname)))))
((jreify? val) (let ((short (last-dot tname)))
(and (memp (lambda (p) (string=? (last-dot p) short)) (jreify-protos val)) #t)))
((ex-info-map? val) (exception-isa? (last-dot (ex-info-class val)) (last-dot tname)))
(else (case-string tname val)))))
(define (instance-check type-sym val)
;; normalize a bare (non-array) string class token to a symbol so every arm and
;; the base table can read its name; array tokens ("[I") stay strings for the
;; natives-array arm.
(let ((ts (if (and (string? type-sym)
(or (= 0 (string-length type-sym))
(not (char=? (string-ref type-sym 0) #\[))))
(jolt-symbol #f type-sym)
type-sym)))
(let loop ((rs instance-check-registry))
(if (null? rs)
(instance-check-base ts val)
(let ((r ((car rs) ts val)))
(if (eq? r 'pass) (loop (cdr rs)) r))))))
(define (case-string tname val)
(cond
((member tname '("Number" "java.lang.Number")) (number? val))
((member tname '("Long" "java.lang.Long" "Integer" "java.lang.Integer"))
(and (number? val) (exact? val) (integer? val)))
((member tname '("Double" "java.lang.Double" "Float" "java.lang.Float")) (and (number? val) (flonum? val)))
((member tname '("Ratio" "clojure.lang.Ratio")) (and (number? val) (exact? val) (rational? val) (not (integer? val))))
((member tname '("String" "java.lang.String" "CharSequence" "java.lang.CharSequence")) (string? val))
((member tname '("Boolean" "java.lang.Boolean")) (boolean? val))
((member tname '("Character" "java.lang.Character")) (char? val))
((member tname '("Keyword" "clojure.lang.Keyword")) (keyword? val))
((member tname '("Symbol" "clojure.lang.Symbol")) (jolt-symbol? val))
((member tname '("Atom" "clojure.lang.Atom")) (jolt-atom? val))
((member tname '("IFn" "clojure.lang.IFn" "Fn" "clojure.lang.Fn")) (procedure? val))
((member tname '("Pattern" "java.util.regex.Pattern")) (regex-t? val))
((member tname '("URI" "java.net.URI"))
(and (jhost? val) (string=? (jhost-tag val) "uri")))
((member tname '("File" "java.io.File")) (jfile? val))
((member tname '("UUID" "java.util.UUID")) (juuid? val))
(else #f)))
;; str of a record uses a custom (Object toString) impl if the type defines one
;; (deftype with no default toString relies on this); otherwise the map form
;; without the leading # (Clojure's record .toString). converters.ss loads before
;; records.ss, so this set! sees the registry — forward refs resolve at call time.
(def-var! "clojure.core" "instance-check" instance-check)

View file

@ -174,7 +174,9 @@
(keyword #f "name") (jolt-symbol jolt-nil name-str)
(keyword #f "methods") methods))
;; register-protocol-methods!: a no-op for Chez dispatch.
;; register-protocol-methods!: intentional no-op. Chez dispatches a protocol method
;; by the receiver's type tag at call time, so there is no method table to register;
;; this binding exists only because defprotocol-emitted code calls it.
(define (register-protocol-methods! proto-name method-names) jolt-nil)
;; register-method: extend-type/extend register an impl. Host type names keep a
@ -329,7 +331,7 @@
((string=? method-name "toString") (condition->message-string obj))
((string=? method-name "getCause") jolt-nil)
;; java.sql.SQLException chaining — jolt errors don't chain (nil).
((or (string=? method-name "getNextException") (string=? method-name "getCause")) jolt-nil)
((string=? method-name "getNextException") jolt-nil)
((string=? method-name "getStackTrace") (jolt-vector))
((string=? method-name "printStackTrace") jolt-nil)
(else (error #f (string-append "No method " method-name " on Throwable")))))
@ -408,98 +410,11 @@
;; jolt exception values (ex-info + host-constructed throwables) are ex-info-shaped
;; maps tagged :jolt/type :jolt/ex-info; (class …)/instance? read the JVM class off
;; the optional :jolt/class key, defaulting to clojure.lang.ExceptionInfo.
;; pmap? guard: ex-info maps are plain hash-maps, never sorted-map htables — and a
;; bare jolt-get on a sorted-map would invoke its comparator on :jolt/type and throw.
(define (ex-info-map? v)
(and (pmap? v) (jolt=2 (jolt-get v jolt-kw-ex-type jolt-nil) jolt-kw-ex-info)))
(define (ex-info-class v)
(let ((c (jolt-get v jolt-kw-class jolt-nil)))
(if (string? c) c "clojure.lang.ExceptionInfo")))
;; immediate-parent chain of the JVM exception hierarchy (simple names). Drives
;; instance? across exception supertypes — (instance? Throwable (ex-info …)) etc.
(define exception-parent
'(("ExceptionInfo" . "RuntimeException")
("RuntimeException" . "Exception")
("IllegalArgumentException" . "RuntimeException")
("NumberFormatException" . "IllegalArgumentException")
("IllegalStateException" . "RuntimeException")
("UnsupportedOperationException" . "RuntimeException")
("ArithmeticException" . "RuntimeException")
("NullPointerException" . "RuntimeException")
("ClassCastException" . "RuntimeException")
("IndexOutOfBoundsException" . "RuntimeException")
("ConcurrentModificationException" . "RuntimeException")
("NoSuchElementException" . "RuntimeException")
("UncheckedIOException" . "RuntimeException")
("InterruptedException" . "Exception")
("IOException" . "Exception")
("FileNotFoundException" . "IOException")
("UnsupportedEncodingException" . "IOException")
("UnknownHostException" . "IOException")
("SocketException" . "IOException")
("ConnectException" . "IOException")
("SocketTimeoutException" . "IOException")
("MalformedURLException" . "IOException")
("SSLException" . "IOException")
("Exception" . "Throwable")
("Error" . "Throwable")
("AssertionError" . "Error")
("Throwable" . "Object")))
;; Is `wanted` (simple name) `cls` or a supertype of it? ExceptionInfo also
;; implements the IExceptionInfo interface.
(define (exception-isa? cls wanted)
(let loop ((c cls))
(cond ((not c) #f)
((string=? c wanted) #t)
((and (string=? c "ExceptionInfo") (string=? wanted "IExceptionInfo")) #t)
(else (let ((p (assoc c exception-parent))) (loop (and p (cdr p))))))))
;; instance-check: (type-sym val) — type/protocol membership.
(define (instance-check type-sym val)
(let ((tname (symbol-t-name type-sym)))
(cond
((jrec? val)
(let ((tag (jrec-tag val)))
(or (string=? tag tname)
(and (> (string-length tag) (string-length tname))
(string=? (substring tag (- (string-length tag) (string-length tname)) (string-length tag)) tname)))))
((jreify? val) (let ((short (last-dot tname)))
(and (memp (lambda (p) (string=? (last-dot p) short)) (jreify-protos val)) #t)))
((ex-info-map? val) (exception-isa? (last-dot (ex-info-class val)) (last-dot tname)))
(else (case-string tname val)))))
(define (case-string tname val)
(cond
((member tname '("Number" "java.lang.Number")) (number? val))
((member tname '("Long" "java.lang.Long" "Integer" "java.lang.Integer"))
(and (number? val) (exact? val) (integer? val)))
((member tname '("Double" "java.lang.Double" "Float" "java.lang.Float")) (and (number? val) (flonum? val)))
((member tname '("Ratio" "clojure.lang.Ratio")) (and (number? val) (exact? val) (rational? val) (not (integer? val))))
((member tname '("String" "java.lang.String" "CharSequence" "java.lang.CharSequence")) (string? val))
((member tname '("Boolean" "java.lang.Boolean")) (boolean? val))
((member tname '("Character" "java.lang.Character")) (char? val))
((member tname '("Keyword" "clojure.lang.Keyword")) (keyword? val))
((member tname '("Symbol" "clojure.lang.Symbol")) (jolt-symbol? val))
((member tname '("Atom" "clojure.lang.Atom")) (jolt-atom? val))
((member tname '("IFn" "clojure.lang.IFn" "Fn" "clojure.lang.Fn")) (procedure? val))
((member tname '("Pattern" "java.util.regex.Pattern")) (regex-t? val))
((member tname '("URI" "java.net.URI"))
(and (jhost? val) (string=? (jhost-tag val) "uri")))
((member tname '("File" "java.io.File")) (jfile? val))
((member tname '("UUID" "java.util.UUID")) (juuid? val))
(else #f)))
;; str of a record uses a custom (Object toString) impl if the type defines one
;; (deftype with no default toString relies on this); otherwise the map form
;; without the leading # (Clojure's record .toString). converters.ss loads before
;; records.ss, so this set! sees the registry — forward refs resolve at call time.
(define %r-str-render-one jolt-str-render-one)
(set! jolt-str-render-one
(register-str-render! jrec?
(lambda (v)
(if (jrec? v)
(let ((f (find-protocol-method (jrec-tag v) "Object" "toString")))
(if f (jolt-invoke f v)
(let ((s (jrec-pr v))) (substring s 1 (string-length s)))))
(%r-str-render-one v))))
(let ((f (find-protocol-method (jrec-tag v) "Object" "toString")))
(if f (jolt-invoke f v)
(let ((s (jrec-pr v))) (substring s 1 (string-length s)))))))
;; `type` lives in natives-meta.ss: it needs jolt-meta for the :type
;; override and a total value->taxonomy mapping, so it sits with meta — a record
@ -514,6 +429,5 @@
(def-var! "clojure.core" "protocol-dispatch" (lambda (pn mn obj rest) (protocol-dispatch pn mn obj rest)))
(def-var! "clojure.core" "satisfies?" jolt-satisfies?)
(def-var! "clojure.core" "extenders" extenders)
(def-var! "clojure.core" "instance-check" instance-check)
(def-var! "clojure.core" "make-reified" (lambda (mm . rest) (apply make-reified mm rest)))
(def-var! "clojure.core" "record-method-dispatch" (lambda (obj m rest) (record-method-dispatch obj m rest)))

View file

@ -237,6 +237,7 @@
;; the dispatchers/printers it wraps (collections/seq/values/converters/printing/
;; transients).
(load "host/chez/records.ss")
(load "host/chez/records-interop.ss") ; exception hierarchy + instance-check taxonomy
;; metadata: meta / with-meta over an identity-keyed
;; side-table. After records.ss (jrec) + the collection ctors it copies.
@ -303,7 +304,9 @@
;; host-static-call/host-new + the jhost method registry. Loads LAST — it extends
;; record-method-dispatch (records.ss) and reuses natives-str helpers (str-trim,
;; ascii-string-down, re-split, str-split-drop-trailing) + the regex-t accessors.
(load "host/chez/host-static.ss")
(load "host/chez/host-static.ss") ; registries + jhost + coercion helpers
(load "host/chez/host-static-statics.ss") ; java.lang/util static methods
(load "host/chez/host-static-objects.ss") ; host object classes + instance? hook
;; generic dot-form dispatch: field access + map/vector member access
;; for the `.` / `.-field` desugar. Loads after host-static.ss so it wraps every

106
host/chez/run-infer.ss Normal file
View file

@ -0,0 +1,106 @@
;; run-infer.ss — inference / success-type-checking gate (jolt.passes.types).
;;
;; The corpus and unit gates compile through run-passes' const-fold-only branch,
;; so the inference walk (jolt.passes.types) runs only under `jolt build --opt` —
;; buildsmoke exercises it on one trivial app and asserts stdout only. This gate
;; drives the pass DIRECTLY: analyze a source string to IR, then call the public
;; checker/driver entry points (check-form, infer-body, the set-*! registries) and
;; assert their observable output (diagnostic counts, collected calls/escapes). It
;; pins the behavior the inference walk's internal state produces, so a refactor of
;; that state is gate-validatable.
;;
;; chez --script host/chez/run-infer.ss
(import (chezscheme))
(load "host/chez/rt.ss")
(set-chez-ns! "clojure.core")
(load "host/chez/seed/prelude.ss")
(load "host/chez/post-prelude.ss")
(set-chez-ns! "user")
(load "host/chez/host-contract.ss")
(load "host/chez/seed/image.ss")
(load "host/chez/compile-eval.ss")
(define analyze (var-deref "jolt.analyzer" "analyze"))
(define check-form (var-deref "jolt.passes.types" "check-form"))
(define infer-body (var-deref "jolt.passes.types" "infer-body"))
(define reset-escapes! (var-deref "jolt.passes.types" "reset-escapes!"))
(define collected-escapes (var-deref "jolt.passes.types" "collected-escapes"))
(define set-record-shapes! (var-deref "jolt.passes.types" "set-record-shapes!"))
(define set-vtypes! (var-deref "jolt.passes.types" "set-vtypes!"))
(define set-check-mode! (var-deref "jolt.passes.types" "set-check-mode!"))
(define run-inference (var-deref "jolt.passes.types" "run-inference"))
(define take-diags! (var-deref "jolt.passes.types" "take-diags!"))
;; analyze a source string to its IR node (fresh ctx, ns "user", no passes).
(define (anode src) (analyze (make-analyze-ctx "user") (jolt-ce-read src)))
;; number of success-type diagnostics check-form produces for src.
(define (diags src strict?) (jolt-count (check-form (anode src) strict?)))
(define fails 0)
(define total 0)
(define (check label actual expected)
(set! total (+ total 1))
(unless (equal? actual expected)
(set! fails (+ fails 1))
(printf " FAIL ~a: got ~s expected ~s\n" label actual expected)))
;; --- core error-domain checking (strict not required) -----------------------
(check "num-op on keyword" (diags "(+ 1 :k)" #f) 1)
(check "num-op all numbers" (diags "(+ 1 2)" #f) 0)
(check "count on number" (diags "(count 5)" #f) 1)
(check "count on vector" (diags "(count [1 2])" #f) 0)
(check "lenient (:k 5)" (diags "(:k 5)" #f) 0)
(check "call a number" (diags "(5 1)" #f) 1)
(check "nested count return type" (diags "(+ 1 (count :k))" #f) 1)
;; --- walk arms thread the type env ------------------------------------------
(check "let binds kw" (diags "(let [x :k] (+ x 1))" #f) 1)
(check "let binds ok" (diags "(let [x 1] (+ x 1))" #f) 0)
(check "if then branch error" (diags "(if true (+ 1 :k) 2)" #f) 1)
(check "do statement error" (diags "(do (+ 1 :k) 2)" #f) 1)
(check "mapv seeds element type" (diags "(mapv (fn [x] (+ x 1)) [:a :b])" #f) 1)
(check "mapv ok element type" (diags "(mapv (fn [x] (+ x 1)) [1 2])" #f) 0)
(check "reduce seeds element" (diags "(reduce (fn [acc x] (+ acc x)) 0 [:a])" #f) 1)
;; --- strict user-function domains (checking-box / diag-memo / user-sig) ------
(check "user wrong arg type" (diags "(do (defn f [x] (+ x 1)) (f :k))" #t) 1)
(check "user wrong arity" (diags "(do (defn g [x] x) (g 1 2))" #t) 1)
(check "user call ok" (diags "(do (defn h [x] (+ x 1)) (h 3))" #t) 0)
(check "user domains off w/o strict" (diags "(do (defn f [x] (+ x 1)) (f :k))" #f) 0)
;; recursive user fn terminates (cycle guard) and still flags the bad arg
(check "user recursive terminates"
(diags "(do (defn rf [x] (+ x (rf x))) (rf :k))" #t) 1)
;; --- infer-body collects calls + escapes ------------------------------------
(reset-escapes!)
(let ((r (infer-body (anode "(do (foo 1) (bar 2) (map inc [1]))") (jolt-hash-map))))
(check "infer-body calls" (jolt-count (jolt-nth r 2)) 3) ; foo, bar, map
(check "infer-body escapes" (jolt-count (collected-escapes)) 1)) ; inc (value position)
;; --- the record-shapes registry feeds call-result types --------------------
;; without shapes a (->P …) call result is :any (accepted); with the registry it
;; types as a struct, so an arithmetic op over it is provably not-a-number.
(check "ctor result :any w/o shapes" (diags "(+ (->P 1) 1)" #f) 0)
(set-record-shapes!
(jolt-hash-map "user/->P"
(jolt-hash-map (keyword #f "fields") (jolt-vector (keyword #f "x"))
(keyword #f "tags") (jolt-vector jolt-nil)
(keyword #f "type") "user.P")))
(check "ctor result struct w/ shapes" (diags "(+ (->P 1) 1)" #f) 1)
(set-record-shapes! (jolt-hash-map))
;; --- the opt-path checker: run-inference emits, take-diags! drains -----------
;; (set-check-mode! on strict?) arms checking during the next run-inference; the
;; diagnostics are stashed for take-diags! to drain once.
(set-check-mode! #t #f)
(run-inference (anode "(+ 1 :k)"))
(check "take-diags drains run-inference" (jolt-count (take-diags!)) 1)
(check "take-diags re-drained empty" (jolt-count (take-diags!)) 0)
(set-check-mode! #f #f)
(run-inference (anode "(+ 1 :k)"))
(check "no diags when check-mode off" (jolt-count (take-diags!)) 0)
(if (= fails 0)
(begin (printf "infer gate: ~a/~a passed\n" total total) (exit 0))
(begin (printf "infer gate: ~a/~a passed (~a failed)\n" (- total fails) total fails) (exit 1)))

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -206,7 +206,13 @@
(if (jolt-reduced? r) (jolt-reduced-val r) r))))
(else (reduce-seq f init (jolt-seq coll)))))))
(define (jolt-into to from) (reduce-seq (lambda (acc x) (jolt-conj1 acc x)) to (jolt-seq from)))
;; Fold through a transient so a pvec/pmap/pset target is built in O(n): a
;; persistent pvec-conj copies its whole backing vector each step, making a naive
;; fold O(n^2) (and into/vec/mapv/filterv all route here). jolt-transient-new
;; falls back to a copy-on-write wrapper for other targets (lists, sorted colls,
;; nil), so those keep the old per-step jolt-conj behaviour.
(define (jolt-into to from)
(jolt-persistent! (reduce-seq (lambda (t x) (jolt-conj! t x)) (jolt-transient-new to) (jolt-seq from))))
(define (range-from n) (cseq-lazy n (lambda () (range-from (+ n 1)))))
(define (range-bounded n end step)

View file

@ -45,8 +45,7 @@
(set! jolt-pr-str (lambda (x) (if (var-cell? x) (var->str x) (%v-pr-str x))))
(define %v-pr-readable jolt-pr-readable)
(set! jolt-pr-readable (lambda (x) (if (var-cell? x) (var->str x) (%v-pr-readable x))))
(define %v-str-render-one jolt-str-render-one)
(set! jolt-str-render-one (lambda (x) (if (var-cell? x) (var->str x) (%v-str-render-one x))))
(register-str-render! var-cell? var->str)
;; bound? — native (the overlay's (get v :root) is nil on a var-cell record).
(define (jolt-var-bound-one? v) (and (var-cell? v) (not (eq? (var-cell-root v) jolt-unbound))))

View file

@ -188,13 +188,15 @@
(defn replicate [n x] (map (fn [_] x) (range n)))
;; Returns a seq (JVM does), nil when n<=0 or coll is empty.
(defn take-last [n coll]
(let [c (vec coll) len (count c)]
(when (pos? len) (subvec c (max 0 (- len n))))))
(when (pos? len) (seq (subvec c (max 0 (- len n)))))))
;; The JVM definition: a lazy seq (() when empty), not a vector.
(defn drop-last
([coll] (drop-last 1 coll))
([n coll] (let [c (vec coll)] (subvec c 0 (max 0 (- (count c) n))))))
([n coll] (map (fn [x _] x) coll (drop n coll))))
(defn distinct?
([x] true)
@ -453,671 +455,3 @@
(defn println-str [& xs] (__with-out-str (fn* [] (apply println xs))))
(defn prn-str [& xs] (__with-out-str (fn* [] (apply prn xs))))
;; --- leaves over the rand / sort host seams ----------------------------------
;; Canonical truncation toward zero via int (the kernel fn floored, which is
;; wrong for a negative n).
(defn rand-int [n] (int (rand n)))
;; Pure-functional Fisher-Yates over vector assoc; returns a vector, as in
;; Clojure. Collections only — a string is seqable but not shuffleable, as on
;; the JVM (Collections/shuffle wants a Collection).
(defn shuffle [coll]
(when-not (coll? coll)
(throw (ex-info (str "shuffle requires a collection, got: " coll) {})))
(loop [v (vec coll) i (dec (count v))]
(if (pos? i)
(let [j (rand-int (inc i))
t (nth v i)]
(recur (assoc (assoc v i (nth v j)) j t) (dec i)))
v)))
;; Canonical sort-by: the default comparator is compare (so nil sorts first,
;; like Clojure — the kernel fn used host ordering, which put nil last); the
;; comparator compares KEYS and may be 3-way or a boolean predicate (the host
;; sort seam normalizes).
(defn sort-by
([keyfn coll] (sort-by keyfn compare coll))
([keyfn comp coll]
(sort (fn [x y] (comp (keyfn x) (keyfn y))) coll)))
;; parse-uuid: nil unless s is a canonical 8-4-4-4-12 hex UUID string; throws
;; on a non-string (Clojure 1.11). __make-uuid is the host constructor for the
;; tagged value (overlay source can't write :jolt/type map literals — the
;; reader treats them as tagged forms).
(defn parse-uuid [s]
(if (string? s)
(when (re-matches
#"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" s)
(__make-uuid s))
(throw (str "parse-uuid requires a string, got: " s))))
;; Version-4 UUID (RFC 4122): zero-padded hex groups 8-4-4-4-12, version
;; nibble 4, variant 8-b — built over rand-int and validated by parse-uuid.
(defn random-uuid []
(let [hx4 (fn [] (format "%04x" (rand-int 0x10000)))
hx3 (fn [] (format "%03x" (rand-int 0x1000)))]
(parse-uuid (str (hx4) (hx4) "-" (hx4) "-4" (hx3)
"-" (format "%x" (+ 8 (rand-int 4))) (hx3)
"-" (hx4) (hx4) (hx4)))))
;; The char escape/name tables, as char-keyed maps (Clojure's shape).
(def ^:private char-escape-strings
{\newline "\\n" \tab "\\t" \return "\\r" \formfeed "\\f"
\backspace "\\b" \" "\\\"" \\ "\\\\"})
(defn char-escape-string [c] (get char-escape-strings c))
(def ^:private char-name-strings
{\newline "newline" \tab "tab" \return "return" \formfeed "formfeed"
\backspace "backspace" \space "space"})
(defn char-name-string [c] (get char-name-strings c))
;; Random selection over the host rand primitives.
(defn rand-nth [coll]
(let [v (vec coll)] (nth v (rand-int (count v)))))
(defn random-sample
([prob] (filter (fn [_] (< (rand) prob))))
([prob coll] (filter (fn [_] (< (rand) prob)) coll)))
(defn comparator [pred]
(fn [a b] (cond (pred a b) -1 (pred b a) 1 :else 0)))
;; Lazy: the running accumulators, one at a time (matches Clojure).
(defn reductions
([f coll]
(lazy-seq
(let [s (seq coll)]
(if s
(reductions f (first s) (rest s))
(list (f))))))
([f init coll]
(cons init
(lazy-seq
(when-let [s (seq coll)]
(reductions f (f init (first s)) (rest s)))))))
;; Lazy pre-order DFS (matches Clojure): node, then its children's walks spliced
;; via the (now lazy) mapcat.
(defn tree-seq [branch? children root]
(let [walk (fn walk [node]
(lazy-seq
(cons node
(when (branch? node)
(mapcat walk (children node))))))]
(walk root)))
;; file-seq: the tree of paths under root (root included), directories walked
;; via the host dir primitives. Paths (strings), not File objects. (Lives below
;; tree-seq: forward references are analysis errors.)
(defn file-seq [root]
(if (__file? root)
;; java.io.File tree: walk via the File method surface so leaves are File
;; values callers can invoke .isFile/.getName/slurp on.
(tree-seq (fn [f] (.isDirectory f)) (fn [f] (seq (.listFiles f))) root)
(tree-seq __dir? __list-dir root)))
;; Canonical flatten via tree-seq: the leaves (non-sequential nodes) in order.
;; Flattens lists too (sequential?), matching Clojure/CLJS.
(defn flatten [coll]
(filter (complement sequential?) (rest (tree-seq sequential? seq coll))))
;; xml-seq: tree-seq over XML element trees. Elements are maps with :content.
(defn xml-seq [root]
(tree-seq (complement string?) (comp seq :content) root))
;; Lazy interleave: round-robin one element from each coll until any exhausts.
(defn interleave
([] ())
([c1] (lazy-seq c1))
([c1 c2]
(lazy-seq
(let [s1 (seq c1) s2 (seq c2)]
(when (and s1 s2)
(cons (first s1)
(cons (first s2)
(interleave (rest s1) (rest s2))))))))
([c1 c2 & cs]
(lazy-seq
(let [ss (map seq (list* c1 c2 cs))]
(when (every? identity ss)
(concat (map first ss)
(apply interleave (map rest ss))))))))
;; No ratio type on Jolt, so rationalize is identity.
(defn rationalize [x] x)
;; 0-arg: a stateful transducer (tracks [seen? prev] in a volatile, so no sentinel
;; value is needed). 1-arg: eager dedupe of consecutive equal elements.
(defn dedupe
([]
(fn [rf]
(let [pv (volatile! [false nil])]
(fn
([] (rf))
([result] (rf result))
([result input]
(let [[seen prior] @pv]
(vreset! pv [true input])
(if (and seen (= prior input)) result (rf result input))))))))
([coll]
(let [step (fn step [s prev]
(make-lazy-seq
(fn* []
(let [s (seq s)]
(if s
(let [x (first s)]
(if (= x prev)
(coll->cells (step (rest s) prev))
(coll->cells (cons x (step (rest s) x)))))
nil)))))]
(let [s (seq coll)]
(if s
(make-lazy-seq
(fn* [] (coll->cells (cons (first s) (step (rest s) (first s))))))
())))))
;; Internal helper for {:keys [...]} destructuring over a seq of k/v pairs —
;; canonical Clojure 1.11 shape (core.clj seq-to-map-for-destructuring):
;; even pairs build a map (later keys win, as createAsIfByAssoc), a SINGLE
;; element is returned as-is (the trailing-map calling convention), and an
;; unpaired key past pairs throws.
(defn seq-to-map-for-destructuring [s]
(if (next s)
(loop [m {} xs (seq s)]
(if xs
(if (next xs)
(recur (assoc m (first xs) (second xs)) (nnext xs))
(throw (str "No value supplied for key: " (first xs))))
m))
(if (seq s) (first s) {})))
;; Host-coupled fns that are pure logic over existing core primitives, so they
;; need no new jolt.host surface.
;; vary-meta: f applied to obj's metadata (+ extra args), reattached. meta and
;; with-meta are the irreducible host primitives; vary-meta is just their compose.
(defn vary-meta [obj f & args]
(with-meta obj (apply f (meta obj) args)))
;; namespace-munge: Clojure namespace name -> legal Java package name (- -> _).
(defn namespace-munge [s]
(apply str (map (fn [c] (if (= c \-) \_ c)) (seq (str s)))))
;; reduce-kv over a map (k v) or vector (index v). Both branches go through reduce,
;; so reduced short-circuits — and the vector path indexes correctly. nil folds
;; to init, matching Clojure.
(defn reduce-kv [f init coll]
(cond
(vector? coll) (reduce (fn [acc i] (f acc i (nth coll i))) init (range (count coll)))
(map? coll) (reduce (fn [acc k] (f acc k (get coll k))) init (keys coll))
(nil? coll) init
:else (throw (str "reduce-kv not supported on: " coll))))
;; ex-info accessors. The constructor (ex-info) stays native — it builds the tagged
;; value and wires into throw — but the value exposes :jolt/type/:message/:data/
;; :cause via get, so the accessors are pure over get. A thrown non-ex-info arrives
;; wrapped as {:jolt/type :jolt/exception :value v}; unwrap that first.
(defn- ex-info-val? [x] (= (get x :jolt/type) :jolt/ex-info))
(defn- ex-unwrap [e]
(if (= (get e :jolt/type) :jolt/exception) (get e :value) e))
(defn ex-data [e]
(let [e (ex-unwrap e)] (if (ex-info-val? e) (get e :data) nil)))
(defn ex-message [e]
(let [e (ex-unwrap e)]
(cond (ex-info-val? e) (get e :message)
:else nil)))
(defn ex-cause [e]
(let [e (ex-unwrap e)] (if (ex-info-val? e) (get e :cause) nil)))
;; inst-ms: epoch milliseconds of an instant; throws on a non-inst (Clojure
;; protocol behavior).
(defn inst-ms [x]
(if (inst? x) (get x :ms) (throw (str "inst-ms requires an inst, got: " x))))
;; Clojure 1.11 map transformers. PHM base so transformed keys canonicalize
;; (collisions: last entry in seq order wins, matching the reference).
(defn update-keys [m f]
(reduce-kv (fn [acc k v] (assoc acc (f k) v)) (hash-map) m))
(defn update-vals [m f]
(reduce-kv (fn [acc k v] (assoc acc k (f v))) (hash-map) m))
;; Vector-returning partition variants (1.11): lazy seqs OF vectors.
(defn partitionv
([n coll] (map vec (partition n coll)))
([n step coll] (map vec (partition n step coll)))
([n step pad coll] (map vec (partition n step pad coll))))
;; partition-all is a lazy-tier fn (40-lazy) — declared so partitionv-all
;; compiles; bound by the time anything calls it.
(declare partition-all)
(defn partitionv-all
([n coll] (map vec (partition-all n coll)))
([n step coll] (map vec (partition-all n step coll))))
;; First part a vector, rest a seq — matching the reference implementation.
(defn splitv-at [n coll]
[(vec (take n coll)) (drop n coll)])
;; with-redefs-fn: temporarily set each var's root to the mapped value, run
;; the thunk, restore the saved roots even on throw. The with-redefs macro
;; (30-macros) builds the {var val} map from names.
(defn with-redefs-fn [binding-map func]
(let [vars (vec (keys binding-map))
saved (mapv var-get vars)]
(doseq [v vars] (var-set v (get binding-map v)))
(try
(func)
(finally
;; loop/recur, not dotimes: dotimes is a 30-macros macro and this tier
;; compiles before it exists (a forward ref would resolve to the macro
;; fn at runtime and mis-apply it).
(loop [i 0]
(when (< i (count vars))
(var-set (nth vars i) (nth saved i))
(recur (inc i))))))))
;; Jolt has no chunked seqs, so this is always false.
(defn chunked-seq? [x] false)
;; Atom peripheral operations. atom/swap!/reset!/deref stay native — the compiler
;; depends on them and they're hot. swap-vals!/reset-vals!/compare-and-set! compose
;; the native ops (which already validate and notify watches); get-validator reads a
;; slot; add-watch/remove-watch/set-validator! mutate the atom (or its watches
;; sub-table) through the one host primitive jolt.host/ref-put! — the minimal
;; mutation kernel the overlay can't express over core fns (a nil value removes the
;; key). compare-and-set! compares by value.
(defn swap-vals! [a f & args]
(let [old (deref a)] [old (apply swap! a f args)]))
(defn reset-vals! [a newval]
(let [old (deref a)] (reset! a newval) [old newval]))
(defn compare-and-set! [a oldval newval]
(if (= oldval (deref a)) (do (reset! a newval) true) false))
(defn get-validator [a] (get a :validator))
(defn add-watch [a key f]
(jolt.host/ref-put! (get a :watches) key f) a)
(defn remove-watch [a key]
(jolt.host/ref-put! (get a :watches) key nil) a)
(defn set-validator! [a f]
(jolt.host/ref-put! a :validator f) nil)
;; vreset!/vswap! live in the seq tier (10-seq.clj): its transducers use them.
;; Future status predicates — pure reads of the future's :cached/:cancelled slots.
;; future? stays native (deref/future-cancel/realized? call it); future-call and
;; future-cancel stay native too (OS threads).
(defn future-done? [x]
(if (future? x) (boolean (get x :cached)) (throw "future-done? requires a future")))
(defn future-cancelled? [x]
(and (future? x) (boolean (get x :cancelled))))
;; ns-name: a namespace object's :name as a symbol. Pure over get + symbol.
(defn ns-name [ns]
(let [nm (get ns :name)] (if nm (symbol (str nm)) nil)))
;; Java-array element access. Jolt arrays are mutable backing arrays; aget/alength
;; read them (nth/count) and aset writes a slot through ref-put!. Both handle the
;; multi-dimensional form (aget a i j ... / aset a i j ... v) by walking. The array
;; constructors (object-array/make-array/to-array/...) stay native — they build the
;; mutable backing.
(defn aget [arr & idxs]
(reduce (fn [v i] (nth v i)) arr idxs))
(defn alength [arr] (count arr))
(defn aset [arr & idxs+val]
(let [n (count idxs+val)
val (nth idxs+val (dec n))
target (reduce (fn [t k] (nth t k)) arr (take (- n 2) idxs+val))]
(jolt.host/ref-put! target (nth idxs+val (- n 2)) val)
val))
;; --- fn combinators + host-free stubs ----------------------------------------
(defn complement
"Takes a fn f and returns a fn that takes the same arguments as f, has the
same effects, if any, and returns the opposite truth value."
[f]
(fn [& args] (not (apply f args))))
;; Canonical Clojure fnil: patches only the FIRST 1-3 arguments.
(defn fnil
([f x]
(fn [a & args] (apply f (if (nil? a) x a) args)))
([f x y]
(fn [a b & args] (apply f (if (nil? a) x a) (if (nil? b) y b) args)))
([f x y z]
(fn [a b c & args]
(apply f (if (nil? a) x a) (if (nil? b) y b) (if (nil? c) z c) args))))
(defn clojure-version [] "1.11.0-jolt")
;; bigdec is a host fn (host/chez/bigdec.ss) — a real BigDecimal value type.
(defn numerator [x] (throw (ex-info "numerator requires a ratio (Jolt has no ratios)" {})))
(defn denominator [x] (throw (ex-info "denominator requires a ratio (Jolt has no ratios)" {})))
;; No class hierarchy on this host.
(defn supers [x] #{})
;; Like Clojure's munge: rewrite dashes to underscores, preserving the argument's
;; type — a symbol munges to a symbol, anything else to a string. (jolt only
;; rewrites dashes, not the full Compiler CHAR_MAP.)
(defn munge [s]
(let [m (str-replace-all "-" "_" (str s))]
(if (symbol? s) (symbol m) m)))
(defn test
"Calls the :test fn from v's metadata; :ok if it runs, :no-test if absent."
[v]
(let [t (:test (meta v))]
(if t (do (t) :ok) :no-test)))
;; --- canonical Clojure ports -------------------------------------------------
;; key/val/find first — merge-with and memoize below use them.
;; Strict, as in Clojure: an entry is what (seq m) yields (a host tuple), NOT
;; a plain vector — (key [1 2]) throws.
;; key/val moved above the hierarchies section (underive uses them).
;; find was previously missing from jolt entirely. Presence (contains?), not
;; value, decides — so (find {:a nil} :a) is [:a nil]. Works on vectors by
;; index. The result must be a REAL entry (key/val are strict), so it is
;; minted as the first entry of a one-entry map — nil values survive (the
;; map builder switches to a phm when nil is involved).
(defn find [m k]
(when (contains? m k) (first {k (get m k)})))
;; some? lives in the top leaf block now (forward refs are errors).
(defn true? [x] (= true x))
(defn false? [x] (= false x))
;; Presence-preserving: a key with a nil value is kept ((hash-map) base keeps
;; nil values and canonicalizes collection keys).
(defn select-keys [map keyseq]
(reduce (fn [m k] (if (contains? map k) (assoc m k (get map k)) m))
(hash-map) keyseq))
(defn zipmap [keys vals]
(loop [m (hash-map) ks (seq keys) vs (seq vals)]
(if (and ks vs)
(recur (assoc m (first ks) (first vs)) (next ks) (next vs))
m)))
;; conj semantics per entry arg (a map merges, a [k v] pair adds); nil args are
;; no-ops; all-nil (or no args) is nil.
(defn merge [& maps]
(when (some identity maps)
(reduce (fn [acc m] (if (nil? m) acc (conj (or acc (hash-map)) m)))
maps)))
(defn merge-with [f & maps]
(when (some identity maps)
(let [merge-entry (fn [m e]
(let [k (key e) v (val e)]
;; presence — not nil-of-value — decides combination
(if (contains? m k)
(assoc m k (f (get m k) v))
(assoc m k v))))
merge2 (fn [m1 m2]
(reduce merge-entry (or m1 (hash-map)) (seq m2)))]
(reduce merge2 maps))))
(defn get-in
([m ks] (reduce get m ks))
([m ks not-found]
;; a fresh table is its own identity — a present-but-nil step is
;; distinguished from a missing one
(let [sentinel (hash-map)]
(loop [m m ks (seq ks)]
(if ks
(let [nxt (get m (first ks) sentinel)]
(if (identical? sentinel nxt)
not-found
(recur nxt (next ks))))
m)))))
;; find-based, so nil RESULTS are cached too; args canonicalize as a collection key.
(defn memoize [f]
(let [mem (atom (hash-map))]
(fn [& args]
;; plain let/if, not if-let: this tier loads before 30-macros defines it
(let [e (find (deref mem) args)]
(if e
(val e)
(let [ret (apply f args)]
(swap! mem assoc args ret)
ret))))))
(defn partial
([f] f)
([f a] (fn [& args] (apply f a args)))
([f a b] (fn [& args] (apply f a b args)))
([f a b c] (fn [& args] (apply f a b c args)))
([f a b c & more] (fn [& args] (apply f a b c (concat more args)))))
(defn trampoline
([f] (let [ret (f)] (if (fn? ret) (trampoline ret) ret)))
([f & args] (trampoline (fn [] (apply f args)))))
;; Canonical pairwise max/min: > / < throw on non-numbers, and the NaN
;; behavior is Clojure's by construction.
(defn max
([x] x)
([x y] (if (> x y) x y))
([x y & more] (reduce max (max x y) more)))
(defn min
([x] x)
([x y] (if (< x y) x y))
([x y & more] (reduce min (min x y) more)))
(defn reverse [coll] (reduce conj (list) coll))
;; An empty coll of the same category; sorted colls keep their comparator (the
;; value's own :empty op). Strings and scalars are nil, as in Clojure; a lazy
;; seq empties to ().
(defn empty [coll]
(cond
(nil? coll) nil
(sorted? coll) ((get (jolt.host/ref-get coll :ops) :empty) coll)
(map? coll) {}
(set? coll) #{}
(vector? coll) []
(coll? coll) ()
:else nil))
(defn assoc-in [m [k & ks] v]
(if ks
(assoc m k (assoc-in (get m k) ks v))
(assoc m k v)))
(defn update-in [m ks f & args]
(let [up (fn up [m ks f args]
(let [[k & ks] ks]
(if ks
(assoc m k (up (get m k) ks f args))
(assoc m k (apply f (get m k) args)))))]
(up m ks f args)))
;; jolt keywords have no intern table (any keyword "exists"), so find-keyword
;; always finds — babashka makes the same call.
(defn find-keyword
([nm] (keyword nm))
([ns nm] (keyword ns nm)))
;; The raw Inst protocol method; jolt insts have one representation, so it is
;; inst-ms itself.
(defn inst-ms* [i] (inst-ms i))
;; Canonical comp — here rather than a host primitive so each stage is invoked with
;; jolt call semantics: (comp seq :content) works because the keyword stage
;; goes through IFn dispatch.
(defn comp
([] identity)
([f] f)
([f g]
;; fixed arities first (Clojure's own shape): the 1-arg path — every
;; map/filter stage — is two direct calls, no rest-seq, no apply.
(fn
([] (f (g)))
([x] (f (g x)))
([x y] (f (g x y)))
([x y z] (f (g x y z)))
([x y z & args] (f (apply g x y z args)))))
([f g & fs] (reduce comp (comp f g) fs)))
;; Canonical IFn set: fns, keywords, symbols, maps (sorted incl.),
;; sets, vectors, and vars — NOT lists ((ifn? '(1 2)) is false in Clojure).
(defn ifn? [x]
(or (fn? x) (keyword? x) (symbol? x) (map? x) (set? x) (vector? x) (var? x)))
;; Auto-promoting (') and unchecked arithmetic. Jolt numbers don't overflow,
;; so all of these are the checked ops; fixed arities mirror Clojure's
;; signatures. unchecked-divide-int goes through quot, so dividing by zero
;; throws as on the JVM.
(def +' +)
(def -' -)
(def *' *)
(def inc' inc)
(def dec' dec)
(defn unchecked-add [x y] (+ x y))
(defn unchecked-subtract [x y] (- x y))
(defn unchecked-multiply [x y] (* x y))
(defn unchecked-negate [x] (- x))
(defn unchecked-inc [x] (+ x 1))
(defn unchecked-dec [x] (- x 1))
(def unchecked-add-int unchecked-add)
(def unchecked-subtract-int unchecked-subtract)
(def unchecked-multiply-int unchecked-multiply)
(def unchecked-negate-int unchecked-negate)
(def unchecked-inc-int unchecked-inc)
(def unchecked-dec-int unchecked-dec)
(defn unchecked-divide-int [x y] (quot x y))
(defn unchecked-remainder-int [x y] (rem x y))
(defn unchecked-int [x] (int x))
(def unchecked-long unchecked-int)
;; int? is integer? on jolt: one number type, so fixed-precision and
;; arbitrary-precision integers coincide.
(defn int? [x] (integer? x))
;; num: Clojure coerces to java.lang.Number; jolt just checks.
(defn num [x]
(if (number? x) x (throw (str "num requires a number, got: " x))))
;; == numeric equality: 1-arity is trivially true without inspecting the value
;; (Clojure's shape); 2+ args must be numbers, as Numbers.equiv throws.
(defn ==
([x] true)
([x y]
(if (and (number? x) (number? y))
(= x y)
(throw (str "Cannot cast to number: " (if (number? x) y x)))))
([x y & more]
(if (== x y)
(apply == y more)
false)))
;; ensure-reduced / halt-when: canonical Clojure. halt-when smuggles the halt
;; value through reduce in a ::halt-keyed map and unwraps it in the completion
;; arity, so the halt REPLACES the whole reduction result.
(defn ensure-reduced [x] (if (reduced? x) x (reduced x)))
(defn halt-when
([pred] (halt-when pred nil))
([pred retf]
(fn [rf]
(fn
([] (rf))
([result]
(if (and (map? result) (contains? result ::halt))
(get result ::halt)
(rf result)))
([result input]
(if (pred input)
(reduced (hash-map ::halt (if retf (retf (rf result) input) input)))
(rf result input)))))))
;; parse-boolean: exact "true"/"false" only; nil on anything else, throw on a
;; non-string (Clojure 1.11).
(defn parse-boolean [s]
(if (string? s)
(cond (= s "true") true (= s "false") false :else nil)
(throw (str "parse-boolean requires a string, got: " s))))
(defn newline [] (print "\n") nil)
;; seque: jolt is single-threaded eager here — the queue is a no-op and the
;; coll passes through.
(defn seque
([s] s)
([n-or-q s] s))
(defn array-seq [arr & _] (seq arr))
(defn to-array-2d [coll] (to-array (map to-array coll)))
;; Masking integer coercions (not aliases): byte/short wrap to their width.
;; unchecked-byte/short truncate to a number; unchecked-char returns a char (as on
;; the JVM). int handles chars, so (unchecked-byte \a) works.
(defn unchecked-byte [x] (bit-and (int x) 0xff))
(defn unchecked-short [x] (bit-and (int x) 0xffff))
(defn unchecked-char [x] (char (bit-and (int x) 0xffff)))
(defn unchecked-float [x] (double x))
(defn unchecked-double [x] (double x))
;; --- transduce / into / eduction ---------------------------------------------
;; Canonical transduce: build the stacked rf once, reduce (which honors
;; `reduced` and steps lazy seqs incrementally), then run the completion arity.
(defn transduce
([xform f coll] (transduce xform f (f) coll))
([xform f init coll]
(let [xf (xform f)]
(xf (reduce xf init coll)))))
;; into stays a host primitive: it's perf-wall hot (the into-vec bench pays ~11%
;; through the overlay call layers — same lesson as even?/odd?).
;; eduction is EAGER on jolt (documented divergence): the composed
;; xforms applied to coll, realized into a vector.
(defn eduction [& args]
(let [coll (last args)
xforms (butlast args)]
(if xforms
(into [] (apply comp xforms) coll)
(into [] coll))))
(defn ->Eduction [xform coll] (into [] xform coll))
;; --- JVM-shape stubs and trivial shells --------------------------------------
;; Pure compositions or documented jolt stubs; the host keeps nothing.
(defn enumeration-seq [e] (seq e))
(defn iterator-seq [i] (seq i))
;; jolt is single-threaded: a promise is an atom, deref never blocks
;; ((deref undelivered) is nil rather than a hang).
(defn promise [] (atom nil))
(defn deliver [p v] (reset! p v) p)
(defn bean [x] (if (map? x) x {}))
(defn uri? [x] false)
;; An EVALUATED set of quoted symbols — a quoted set literal ('#{if ...})
;; stays an unevaluated reader form on jolt and contains? can't see into it.
(def ^:private special-syms
#{'if 'do 'let* 'fn* 'quote 'var 'def 'loop* 'recur 'throw 'try 'catch
'finally 'new 'set! '. 'monitor-enter 'monitor-exit})
(defn special-symbol? [s] (contains? special-syms s))
;; print-method / print-dup are real multimethods in the io tier (50-io.clj).
;; JVM proxies don't exist on this host: the read-only surface is inert,
;; the constructive surface throws.
(defn proxy-mappings [p] {})
(defn proxy-call-with-super [f p meth] (f))
(defn init-proxy [p mappings] p)
(defn update-proxy [p mappings] p)
(defn proxy-super [& args] (throw "proxy-super: JVM proxies are not supported in Jolt"))
(defn construct-proxy [c & args] (throw "construct-proxy: not supported in Jolt"))
(defn get-proxy-class [& interfaces] (throw "get-proxy-class: not supported in Jolt"))

View file

@ -0,0 +1,362 @@
;; clojure.core — collection tier, part 2 (rand/sort host seams, the
;; clojure.test runner, fn combinators). Continues 20-coll.clj; same constraints
;; (pure, eager, no macros), loaded in the 20 slot before 25-sorted.
;; --- leaves over the rand / sort host seams ----------------------------------
;; Canonical truncation toward zero via int (the kernel fn floored, which is
;; wrong for a negative n).
(defn rand-int [n] (int (rand n)))
;; Pure-functional Fisher-Yates over vector assoc; returns a vector, as in
;; Clojure. Collections only — a string is seqable but not shuffleable, as on
;; the JVM (Collections/shuffle wants a Collection).
(defn shuffle [coll]
(when-not (coll? coll)
(throw (ex-info (str "shuffle requires a collection, got: " coll) {})))
(loop [v (vec coll) i (dec (count v))]
(if (pos? i)
(let [j (rand-int (inc i))
t (nth v i)]
(recur (assoc (assoc v i (nth v j)) j t) (dec i)))
v)))
;; Canonical sort-by: the default comparator is compare (so nil sorts first,
;; like Clojure — the kernel fn used host ordering, which put nil last); the
;; comparator compares KEYS and may be 3-way or a boolean predicate (the host
;; sort seam normalizes).
(defn sort-by
([keyfn coll] (sort-by keyfn compare coll))
([keyfn comp coll]
(sort (fn [x y] (comp (keyfn x) (keyfn y))) coll)))
;; parse-uuid: nil unless s is a canonical 8-4-4-4-12 hex UUID string; throws
;; on a non-string (Clojure 1.11). __make-uuid is the host constructor for the
;; tagged value (overlay source can't write :jolt/type map literals — the
;; reader treats them as tagged forms).
(defn parse-uuid [s]
(if (string? s)
(when (re-matches
#"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" s)
(__make-uuid s))
(throw (str "parse-uuid requires a string, got: " s))))
;; Version-4 UUID (RFC 4122): zero-padded hex groups 8-4-4-4-12, version
;; nibble 4, variant 8-b — built over rand-int and validated by parse-uuid.
(defn random-uuid []
(let [hx4 (fn [] (format "%04x" (rand-int 0x10000)))
hx3 (fn [] (format "%03x" (rand-int 0x1000)))]
(parse-uuid (str (hx4) (hx4) "-" (hx4) "-4" (hx3)
"-" (format "%x" (+ 8 (rand-int 4))) (hx3)
"-" (hx4) (hx4) (hx4)))))
;; The char escape/name tables, as char-keyed maps (Clojure's shape).
(def ^:private char-escape-strings
{\newline "\\n" \tab "\\t" \return "\\r" \formfeed "\\f"
\backspace "\\b" \" "\\\"" \\ "\\\\"})
(defn char-escape-string [c] (get char-escape-strings c))
(def ^:private char-name-strings
{\newline "newline" \tab "tab" \return "return" \formfeed "formfeed"
\backspace "backspace" \space "space"})
(defn char-name-string [c] (get char-name-strings c))
;; Random selection over the host rand primitives.
(defn rand-nth [coll]
(let [v (vec coll)] (nth v (rand-int (count v)))))
(defn random-sample
([prob] (filter (fn [_] (< (rand) prob))))
([prob coll] (filter (fn [_] (< (rand) prob)) coll)))
(defn comparator [pred]
(fn [a b] (cond (pred a b) -1 (pred b a) 1 :else 0)))
;; Lazy: the running accumulators, one at a time (matches Clojure).
(defn reductions
([f coll]
(lazy-seq
(let [s (seq coll)]
(if s
(reductions f (first s) (rest s))
(list (f))))))
([f init coll]
(cons init
(lazy-seq
(when-let [s (seq coll)]
(reductions f (f init (first s)) (rest s)))))))
;; Lazy pre-order DFS (matches Clojure): node, then its children's walks spliced
;; via the (now lazy) mapcat.
(defn tree-seq [branch? children root]
(let [walk (fn walk [node]
(lazy-seq
(cons node
(when (branch? node)
(mapcat walk (children node))))))]
(walk root)))
;; file-seq: the tree of paths under root (root included), directories walked
;; via the host dir primitives. Paths (strings), not File objects. (Lives below
;; tree-seq: forward references are analysis errors.)
(defn file-seq [root]
(if (__file? root)
;; java.io.File tree: walk via the File method surface so leaves are File
;; values callers can invoke .isFile/.getName/slurp on.
(tree-seq (fn [f] (.isDirectory f)) (fn [f] (seq (.listFiles f))) root)
(tree-seq __dir? __list-dir root)))
;; Canonical flatten via tree-seq: the leaves (non-sequential nodes) in order.
;; Flattens lists too (sequential?), matching Clojure/CLJS.
(defn flatten [coll]
(filter (complement sequential?) (rest (tree-seq sequential? seq coll))))
;; xml-seq: tree-seq over XML element trees. Elements are maps with :content.
(defn xml-seq [root]
(tree-seq (complement string?) (comp seq :content) root))
;; Lazy interleave: round-robin one element from each coll until any exhausts.
(defn interleave
([] ())
([c1] (lazy-seq c1))
([c1 c2]
(lazy-seq
(let [s1 (seq c1) s2 (seq c2)]
(when (and s1 s2)
(cons (first s1)
(cons (first s2)
(interleave (rest s1) (rest s2))))))))
([c1 c2 & cs]
(lazy-seq
(let [ss (map seq (list* c1 c2 cs))]
(when (every? identity ss)
(concat (map first ss)
(apply interleave (map rest ss))))))))
;; No ratio type on Jolt, so rationalize is identity.
(defn rationalize [x] x)
;; 0-arg: a stateful transducer (tracks [seen? prev] in a volatile, so no sentinel
;; value is needed). 1-arg: eager dedupe of consecutive equal elements.
(defn dedupe
([]
(fn [rf]
(let [pv (volatile! [false nil])]
(fn
([] (rf))
([result] (rf result))
([result input]
(let [[seen prior] @pv]
(vreset! pv [true input])
(if (and seen (= prior input)) result (rf result input))))))))
([coll]
(let [step (fn step [s prev]
(make-lazy-seq
(fn* []
(let [s (seq s)]
(if s
(let [x (first s)]
(if (= x prev)
(coll->cells (step (rest s) prev))
(coll->cells (cons x (step (rest s) x)))))
nil)))))]
(let [s (seq coll)]
(if s
(make-lazy-seq
(fn* [] (coll->cells (cons (first s) (step (rest s) (first s))))))
())))))
;; Internal helper for {:keys [...]} destructuring over a seq of k/v pairs —
;; canonical Clojure 1.11 shape (core.clj seq-to-map-for-destructuring):
;; even pairs build a map (later keys win, as createAsIfByAssoc), a SINGLE
;; element is returned as-is (the trailing-map calling convention), and an
;; unpaired key past pairs throws.
(defn seq-to-map-for-destructuring [s]
(if (next s)
(loop [m {} xs (seq s)]
(if xs
(if (next xs)
(recur (assoc m (first xs) (second xs)) (nnext xs))
(throw (str "No value supplied for key: " (first xs))))
m))
(if (seq s) (first s) {})))
;; Host-coupled fns that are pure logic over existing core primitives, so they
;; need no new jolt.host surface.
;; vary-meta: f applied to obj's metadata (+ extra args), reattached. meta and
;; with-meta are the irreducible host primitives; vary-meta is just their compose.
(defn vary-meta [obj f & args]
(with-meta obj (apply f (meta obj) args)))
;; namespace-munge: Clojure namespace name -> legal Java package name (- -> _).
(defn namespace-munge [s]
(apply str (map (fn [c] (if (= c \-) \_ c)) (seq (str s)))))
;; reduce-kv over a map (k v) or vector (index v). Both branches go through reduce,
;; so reduced short-circuits — and the vector path indexes correctly. nil folds
;; to init, matching Clojure.
(defn reduce-kv [f init coll]
(cond
(vector? coll) (reduce (fn [acc i] (f acc i (nth coll i))) init (range (count coll)))
(map? coll) (reduce (fn [acc k] (f acc k (get coll k))) init (keys coll))
(nil? coll) init
:else (throw (str "reduce-kv not supported on: " coll))))
;; ex-info accessors. The constructor (ex-info) stays native — it builds the tagged
;; value and wires into throw — but the value exposes :jolt/type/:message/:data/
;; :cause via get, so the accessors are pure over get. A thrown non-ex-info arrives
;; wrapped as {:jolt/type :jolt/exception :value v}; unwrap that first.
(defn- ex-info-val? [x] (= (get x :jolt/type) :jolt/ex-info))
(defn- ex-unwrap [e]
(if (= (get e :jolt/type) :jolt/exception) (get e :value) e))
(defn ex-data [e]
(let [e (ex-unwrap e)] (if (ex-info-val? e) (get e :data) nil)))
(defn ex-message [e]
(let [e (ex-unwrap e)]
(cond (ex-info-val? e) (get e :message)
:else nil)))
(defn ex-cause [e]
(let [e (ex-unwrap e)] (if (ex-info-val? e) (get e :cause) nil)))
;; inst-ms: epoch milliseconds of an instant; throws on a non-inst (Clojure
;; protocol behavior).
(defn inst-ms [x]
(if (inst? x) (get x :ms) (throw (str "inst-ms requires an inst, got: " x))))
;; Clojure 1.11 map transformers. PHM base so transformed keys canonicalize
;; (collisions: last entry in seq order wins, matching the reference).
(defn update-keys [m f]
(reduce-kv (fn [acc k v] (assoc acc (f k) v)) (hash-map) m))
(defn update-vals [m f]
(reduce-kv (fn [acc k v] (assoc acc k (f v))) (hash-map) m))
;; Vector-returning partition variants (1.11): lazy seqs OF vectors.
(defn partitionv
([n coll] (map vec (partition n coll)))
([n step coll] (map vec (partition n step coll)))
([n step pad coll] (map vec (partition n step pad coll))))
;; partition-all is a lazy-tier fn (40-lazy) — declared so partitionv-all
;; compiles; bound by the time anything calls it.
(declare partition-all)
(defn partitionv-all
([n coll] (map vec (partition-all n coll)))
([n step coll] (map vec (partition-all n step coll))))
;; First part a vector, rest a seq — matching the reference implementation.
(defn splitv-at [n coll]
[(vec (take n coll)) (drop n coll)])
;; with-redefs-fn: temporarily set each var's root to the mapped value, run
;; the thunk, restore the saved roots even on throw. The with-redefs macro
;; (30-macros) builds the {var val} map from names.
(defn with-redefs-fn [binding-map func]
(let [vars (vec (keys binding-map))
saved (mapv var-get vars)]
(doseq [v vars] (var-set v (get binding-map v)))
(try
(func)
(finally
;; loop/recur, not dotimes: dotimes is a 30-macros macro and this tier
;; compiles before it exists (a forward ref would resolve to the macro
;; fn at runtime and mis-apply it).
(loop [i 0]
(when (< i (count vars))
(var-set (nth vars i) (nth saved i))
(recur (inc i))))))))
;; Jolt has no chunked seqs, so this is always false.
(defn chunked-seq? [x] false)
;; Atom peripheral operations. atom/swap!/reset!/deref stay native — the compiler
;; depends on them and they're hot. swap-vals!/reset-vals!/compare-and-set! compose
;; the native ops (which already validate and notify watches); get-validator reads a
;; slot; add-watch/remove-watch/set-validator! mutate the atom (or its watches
;; sub-table) through the one host primitive jolt.host/ref-put! — the minimal
;; mutation kernel the overlay can't express over core fns (a nil value removes the
;; key). compare-and-set! compares by value.
(defn swap-vals! [a f & args]
(let [old (deref a)] [old (apply swap! a f args)]))
(defn reset-vals! [a newval]
(let [old (deref a)] (reset! a newval) [old newval]))
(defn compare-and-set! [a oldval newval]
(if (= oldval (deref a)) (do (reset! a newval) true) false))
(defn get-validator [a] (get a :validator))
(defn add-watch [a key f]
(jolt.host/ref-put! (get a :watches) key f) a)
(defn remove-watch [a key]
(jolt.host/ref-put! (get a :watches) key nil) a)
(defn set-validator! [a f]
(jolt.host/ref-put! a :validator f) nil)
;; vreset!/vswap! live in the seq tier (10-seq.clj): its transducers use them.
;; Future status predicates — pure reads of the future's :cached/:cancelled slots.
;; future? stays native (deref/future-cancel/realized? call it); future-call and
;; future-cancel stay native too (OS threads).
(defn future-done? [x]
(if (future? x) (boolean (get x :cached)) (throw "future-done? requires a future")))
(defn future-cancelled? [x]
(and (future? x) (boolean (get x :cancelled))))
;; ns-name: a namespace object's :name as a symbol. Pure over get + symbol.
(defn ns-name [ns]
(let [nm (get ns :name)] (if nm (symbol (str nm)) nil)))
;; Java-array element access. Jolt arrays are mutable backing arrays; aget/alength
;; read them (nth/count) and aset writes a slot through ref-put!. Both handle the
;; multi-dimensional form (aget a i j ... / aset a i j ... v) by walking. The array
;; constructors (object-array/make-array/to-array/...) stay native — they build the
;; mutable backing.
(defn aget [arr & idxs]
(reduce (fn [v i] (nth v i)) arr idxs))
(defn alength [arr] (count arr))
(defn aset [arr & idxs+val]
(let [n (count idxs+val)
val (nth idxs+val (dec n))
target (reduce (fn [t k] (nth t k)) arr (take (- n 2) idxs+val))]
(jolt.host/ref-put! target (nth idxs+val (- n 2)) val)
val))
;; --- fn combinators + host-free stubs ----------------------------------------
(defn complement
"Takes a fn f and returns a fn that takes the same arguments as f, has the
same effects, if any, and returns the opposite truth value."
[f]
(fn [& args] (not (apply f args))))
;; Canonical Clojure fnil: patches only the FIRST 1-3 arguments.
(defn fnil
([f x]
(fn [a & args] (apply f (if (nil? a) x a) args)))
([f x y]
(fn [a b & args] (apply f (if (nil? a) x a) (if (nil? b) y b) args)))
([f x y z]
(fn [a b c & args]
(apply f (if (nil? a) x a) (if (nil? b) y b) (if (nil? c) z c) args))))
(defn clojure-version [] "1.11.0-jolt")
;; bigdec is a host fn (host/chez/bigdec.ss) — a real BigDecimal value type.
(defn numerator [x] (throw (ex-info "numerator requires a ratio (Jolt has no ratios)" {})))
(defn denominator [x] (throw (ex-info "denominator requires a ratio (Jolt has no ratios)" {})))
;; No class hierarchy on this host.
(defn supers [x] #{})
;; Like Clojure's munge: rewrite dashes to underscores, preserving the argument's
;; type — a symbol munges to a symbol, anything else to a string. (jolt only
;; rewrites dashes, not the full Compiler CHAR_MAP.)
(defn munge [s]
(let [m (str-replace-all "-" "_" (str s))]
(if (symbol? s) (symbol m) m)))
(defn test
"Calls the :test fn from v's metadata; :ok if it runs, :no-test if absent."
[v]
(let [t (:test (meta v))]
(if t (do (t) :ok) :no-test)))

View file

@ -0,0 +1,314 @@
;; clojure.core — collection tier, part 3 (canonical Clojure ports: key/val/find,
;; merge-with, memoize, group-by, frequencies, transduce/into/eduction, and the
;; JVM-shape stubs). Continues 21-coll.clj; same constraints.
;; --- canonical Clojure ports -------------------------------------------------
;; key/val/find first — merge-with and memoize below use them.
;; Strict, as in Clojure: an entry is what (seq m) yields (a host tuple), NOT
;; a plain vector — (key [1 2]) throws.
;; key/val moved above the hierarchies section (underive uses them).
;; find was previously missing from jolt entirely. Presence (contains?), not
;; value, decides — so (find {:a nil} :a) is [:a nil]. Works on vectors by
;; index. The result must be a REAL entry (key/val are strict), so it is
;; minted as the first entry of a one-entry map — nil values survive (the
;; map builder switches to a phm when nil is involved).
(defn find [m k]
(when (contains? m k) (first {k (get m k)})))
;; some? lives in the top leaf block now (forward refs are errors).
(defn true? [x] (= true x))
(defn false? [x] (= false x))
;; Presence-preserving: a key with a nil value is kept ((hash-map) base keeps
;; nil values and canonicalizes collection keys).
(defn select-keys [map keyseq]
(reduce (fn [m k] (if (contains? map k) (assoc m k (get map k)) m))
(hash-map) keyseq))
(defn zipmap [keys vals]
(loop [m (hash-map) ks (seq keys) vs (seq vals)]
(if (and ks vs)
(recur (assoc m (first ks) (first vs)) (next ks) (next vs))
m)))
;; conj semantics per entry arg (a map merges, a [k v] pair adds); nil args are
;; no-ops; all-nil (or no args) is nil.
(defn merge [& maps]
(when (some identity maps)
(reduce (fn [acc m] (if (nil? m) acc (conj (or acc (hash-map)) m)))
maps)))
(defn merge-with [f & maps]
(when (some identity maps)
(let [merge-entry (fn [m e]
(let [k (key e) v (val e)]
;; presence — not nil-of-value — decides combination
(if (contains? m k)
(assoc m k (f (get m k) v))
(assoc m k v))))
merge2 (fn [m1 m2]
(reduce merge-entry (or m1 (hash-map)) (seq m2)))]
(reduce merge2 maps))))
(defn get-in
([m ks] (reduce get m ks))
([m ks not-found]
;; a fresh table is its own identity — a present-but-nil step is
;; distinguished from a missing one
(let [sentinel (hash-map)]
(loop [m m ks (seq ks)]
(if ks
(let [nxt (get m (first ks) sentinel)]
(if (identical? sentinel nxt)
not-found
(recur nxt (next ks))))
m)))))
;; find-based, so nil RESULTS are cached too; args canonicalize as a collection key.
(defn memoize [f]
(let [mem (atom (hash-map))]
(fn [& args]
;; plain let/if, not if-let: this tier loads before 30-macros defines it
(let [e (find (deref mem) args)]
(if e
(val e)
(let [ret (apply f args)]
(swap! mem assoc args ret)
ret))))))
(defn partial
([f] f)
([f a] (fn [& args] (apply f a args)))
([f a b] (fn [& args] (apply f a b args)))
([f a b c] (fn [& args] (apply f a b c args)))
([f a b c & more] (fn [& args] (apply f a b c (concat more args)))))
(defn trampoline
([f] (let [ret (f)] (if (fn? ret) (trampoline ret) ret)))
([f & args] (trampoline (fn [] (apply f args)))))
;; Canonical pairwise max/min: > / < throw on non-numbers, and the NaN
;; behavior is Clojure's by construction.
(defn max
([x] x)
([x y] (if (> x y) x y))
([x y & more] (reduce max (max x y) more)))
(defn min
([x] x)
([x y] (if (< x y) x y))
([x y & more] (reduce min (min x y) more)))
(defn reverse [coll] (reduce conj (list) coll))
;; An empty coll of the same category; sorted colls keep their comparator (the
;; value's own :empty op). Strings and scalars are nil, as in Clojure; a lazy
;; seq empties to ().
(defn empty [coll]
(cond
(nil? coll) nil
(sorted? coll) ((get (jolt.host/ref-get coll :ops) :empty) coll)
(map? coll) {}
(set? coll) #{}
(vector? coll) []
(coll? coll) ()
:else nil))
(defn assoc-in [m [k & ks] v]
(if ks
(assoc m k (assoc-in (get m k) ks v))
(assoc m k v)))
(defn update-in [m ks f & args]
(let [up (fn up [m ks f args]
(let [[k & ks] ks]
(if ks
(assoc m k (up (get m k) ks f args))
(assoc m k (apply f (get m k) args)))))]
(up m ks f args)))
;; jolt keywords have no intern table (any keyword "exists"), so find-keyword
;; always finds — babashka makes the same call.
(defn find-keyword
([nm] (keyword nm))
([ns nm] (keyword ns nm)))
;; The raw Inst protocol method; jolt insts have one representation, so it is
;; inst-ms itself.
(defn inst-ms* [i] (inst-ms i))
;; Canonical comp — here rather than a host primitive so each stage is invoked with
;; jolt call semantics: (comp seq :content) works because the keyword stage
;; goes through IFn dispatch.
(defn comp
([] identity)
([f] f)
([f g]
;; fixed arities first (Clojure's own shape): the 1-arg path — every
;; map/filter stage — is two direct calls, no rest-seq, no apply.
(fn
([] (f (g)))
([x] (f (g x)))
([x y] (f (g x y)))
([x y z] (f (g x y z)))
([x y z & args] (f (apply g x y z args)))))
([f g & fs] (reduce comp (comp f g) fs)))
;; Canonical IFn set: fns, keywords, symbols, maps (sorted incl.),
;; sets, vectors, and vars — NOT lists ((ifn? '(1 2)) is false in Clojure).
(defn ifn? [x]
(or (fn? x) (keyword? x) (symbol? x) (map? x) (set? x) (vector? x) (var? x)))
;; Auto-promoting (') and unchecked arithmetic. Jolt numbers don't overflow,
;; so all of these are the checked ops; fixed arities mirror Clojure's
;; signatures. unchecked-divide-int goes through quot, so dividing by zero
;; throws as on the JVM.
(def +' +)
(def -' -)
(def *' *)
(def inc' inc)
(def dec' dec)
(defn unchecked-add [x y] (+ x y))
(defn unchecked-subtract [x y] (- x y))
(defn unchecked-multiply [x y] (* x y))
(defn unchecked-negate [x] (- x))
(defn unchecked-inc [x] (+ x 1))
(defn unchecked-dec [x] (- x 1))
(def unchecked-add-int unchecked-add)
(def unchecked-subtract-int unchecked-subtract)
(def unchecked-multiply-int unchecked-multiply)
(def unchecked-negate-int unchecked-negate)
(def unchecked-inc-int unchecked-inc)
(def unchecked-dec-int unchecked-dec)
(defn unchecked-divide-int [x y] (quot x y))
(defn unchecked-remainder-int [x y] (rem x y))
(defn unchecked-int [x] (int x))
(def unchecked-long unchecked-int)
;; int? is integer? on jolt: one number type, so fixed-precision and
;; arbitrary-precision integers coincide.
(defn int? [x] (integer? x))
;; num: Clojure coerces to java.lang.Number; jolt just checks.
(defn num [x]
(if (number? x) x (throw (str "num requires a number, got: " x))))
;; == numeric equality: 1-arity is trivially true without inspecting the value
;; (Clojure's shape); 2+ args must be numbers, as Numbers.equiv throws.
(defn ==
([x] true)
([x y]
(if (and (number? x) (number? y))
(= x y)
(throw (str "Cannot cast to number: " (if (number? x) y x)))))
([x y & more]
(if (== x y)
(apply == y more)
false)))
;; ensure-reduced / halt-when: canonical Clojure. halt-when smuggles the halt
;; value through reduce in a ::halt-keyed map and unwraps it in the completion
;; arity, so the halt REPLACES the whole reduction result.
(defn ensure-reduced [x] (if (reduced? x) x (reduced x)))
(defn halt-when
([pred] (halt-when pred nil))
([pred retf]
(fn [rf]
(fn
([] (rf))
([result]
(if (and (map? result) (contains? result ::halt))
(get result ::halt)
(rf result)))
([result input]
(if (pred input)
(reduced (hash-map ::halt (if retf (retf (rf result) input) input)))
(rf result input)))))))
;; parse-boolean: exact "true"/"false" only; nil on anything else, throw on a
;; non-string (Clojure 1.11).
(defn parse-boolean [s]
(if (string? s)
(cond (= s "true") true (= s "false") false :else nil)
(throw (str "parse-boolean requires a string, got: " s))))
(defn newline [] (print "\n") nil)
;; seque: jolt is single-threaded eager here — the queue is a no-op and the
;; coll passes through.
(defn seque
([s] s)
([n-or-q s] s))
(defn array-seq [arr & _] (seq arr))
(defn to-array-2d [coll] (to-array (map to-array coll)))
;; Masking integer coercions (not aliases): byte/short wrap to their width.
;; unchecked-byte/short truncate to a number; unchecked-char returns a char (as on
;; the JVM). int handles chars, so (unchecked-byte \a) works.
(defn unchecked-byte [x] (bit-and (int x) 0xff))
(defn unchecked-short [x] (bit-and (int x) 0xffff))
(defn unchecked-char [x] (char (bit-and (int x) 0xffff)))
(defn unchecked-float [x] (double x))
(defn unchecked-double [x] (double x))
;; --- transduce / into / eduction ---------------------------------------------
;; Canonical transduce: build the stacked rf once, reduce (which honors
;; `reduced` and steps lazy seqs incrementally), then run the completion arity.
(defn transduce
([xform f coll] (transduce xform f (f) coll))
([xform f init coll]
(let [xf (xform f)]
(xf (reduce xf init coll)))))
;; into stays a host primitive: it's perf-wall hot (the into-vec bench pays ~11%
;; through the overlay call layers — same lesson as even?/odd?).
;; eduction is EAGER on jolt (documented divergence): the composed
;; xforms applied to coll, realized into a vector.
(defn eduction [& args]
(let [coll (last args)
xforms (butlast args)]
(if xforms
(into [] (apply comp xforms) coll)
(into [] coll))))
(defn ->Eduction [xform coll] (into [] xform coll))
;; --- JVM-shape stubs and trivial shells --------------------------------------
;; Pure compositions or documented jolt stubs; the host keeps nothing.
(defn enumeration-seq [e] (seq e))
(defn iterator-seq [i] (seq i))
;; jolt is single-threaded: a promise is an atom, deref never blocks
;; ((deref undelivered) is nil rather than a hang).
(defn promise [] (atom nil))
(defn deliver [p v] (reset! p v) p)
(defn bean [x] (if (map? x) x {}))
(defn uri? [x] false)
;; An EVALUATED set of quoted symbols — a quoted set literal ('#{if ...})
;; stays an unevaluated reader form on jolt and contains? can't see into it.
(def ^:private special-syms
#{'if 'do 'let* 'fn* 'quote 'var 'def 'loop* 'recur 'throw 'try 'catch
'finally 'new 'set! '. 'monitor-enter 'monitor-exit})
(defn special-symbol? [s] (contains? special-syms s))
;; print-method / print-dup are real multimethods in the io tier (50-io.clj).
;; JVM proxies don't exist on this host: the read-only surface is inert,
;; the constructive surface throws.
(defn proxy-mappings [p] {})
(defn proxy-call-with-super [f p meth] (f))
(defn init-proxy [p mappings] p)
(defn update-proxy [p mappings] p)
(defn proxy-super [& args] (throw "proxy-super: JVM proxies are not supported in Jolt"))
(defn construct-proxy [c & args] (throw "construct-proxy: not supported in Jolt"))
(defn get-proxy-class [& interfaces] (throw "get-proxy-class: not supported in Jolt"))

View file

@ -76,13 +76,11 @@
(mapi 0 coll))))
;; --- cycle ---
;; Lazy, like the JVM: never counts coll, so it terminates on a lazy/infinite
;; argument instead of forcing it.
(defn cycle [coll]
(if-let [vals (seq coll)]
(let [n (count vals)]
(letfn [(cstep [i]
(lazy-seq
(cons (nth vals (mod i n)) (cstep (inc i)))))]
(cstep 0)))
(if (seq coll)
(lazy-seq (concat coll (cycle coll)))
()))
;; --- repeatedly --- ((f) throws on a non-fn; (take n …) throws on a non-number

View file

@ -32,6 +32,10 @@
(declare analyze)
;; Special forms analyze-special has a dispatch arm for — the subset of the host
;; contract's reserved words (jolt.host/form-special?) the analyzer lowers itself.
;; The two differ deliberately (e.g. interop heads like `new`/`.` are reserved but
;; analyzed in analyze-list), so keep them in sync by intent, not by equality.
(def ^:private handled
#{"quote" "if" "do" "def" "fn*" "let*" "loop*" "recur" "throw" "try"
"syntax-quote" "var" "letfn" "set!" "defmacro"})
@ -251,6 +255,65 @@
{:op :let :letrec true :bindings binds
:body (analyze-seq ctx (drop 2 items) env*)}))
;; A `.-field` head: `(.-field target)` is field access (the dash signals field
;; access to the host-call dispatcher). Defined above analyze-special so its set!
;; arm and analyze-list both reach it without a forward reference.
(defn- field-head? [nm]
(and (> (count nm) 2) (= ".-" (subs nm 0 2))))
(defn- analyze-def [ctx items env]
(let [name-sym (nth items 1)]
;; ^{:map} metadata reads as (def (with-meta name m) v): the metadata is a
;; runtime expression, so the interpreter evaluates the whole def.
(when-not (form-sym? name-sym)
(uncompilable "def name with map metadata"))
(if (< (count items) 3)
;; (def name) with no init (declare): intern + reserve the cell so a forward
;; reference resolves; the back end keys on :no-init.
(let [nm (form-sym-name name-sym) cur (compile-ns ctx)]
(host-intern! ctx cur nm)
{:op :def :ns cur :name nm :no-init true})
;; (def name docstring value): docstring is form 2, value form 3 — matching
;; the interpreter, else the docstring is taken as the value.
(let [nm (form-sym-name name-sym)
cur (compile-ns ctx)
has-doc (and (> (count items) 3) (string? (nth items 2)))
val-form (nth items (if has-doc 3 2))
base0 (or (form-sym-meta name-sym) {})
;; resolve a ^Type hint to its canonical class name at def time, as the
;; JVM compiler does (^String -> java.lang.String); unknown hints pass.
tag (get base0 :tag)
tag-name (cond (form-sym? tag) (form-sym-name tag)
(string? tag) tag
:else nil)
base-meta (if tag-name
(let [c (resolve-class-hint tag-name)]
(if c (assoc base0 :tag c) base0))
base0)
node-meta (if has-doc (assoc base-meta :doc (nth items 2)) base-meta)]
(host-intern! ctx cur nm)
(def-node cur nm (analyze ctx val-form env) node-meta)))))
;; (set! (.-field obj) v) mutates a deftype instance field in place; (set! *var* v)
;; sets the var's innermost thread binding, else its root. A local target (jolt
;; binds fields immutably) or any other shape is uncompilable.
(defn- analyze-set! [ctx items env]
(let [target (nth items 1)
val-node (analyze ctx (nth items 2) env)
ti (when (form-list? target) (vec (form-elements target)))
thead (when (and ti (pos? (count ti)) (form-sym? (first ti)))
(form-sym-name (first ti)))]
(cond
(and thead (field-head? thead))
{:op :set-field :obj (analyze ctx (nth ti 1) env)
:field (subs thead 2) :val val-node}
(form-sym? target)
(do (when (local? env (form-sym-name target)) (uncompilable "set! of a local"))
(let [r (resolve-global ctx target)]
(when-not (= :var (:kind r)) (uncompilable "set! of a non-var"))
{:op :set-var :the-var (the-var (:ns r) (:name r)) :val val-node}))
:else (uncompilable "set! of an unsupported target"))))
(defn- analyze-special [ctx op items env]
(case op
"quote" (quote-node (second items))
@ -265,42 +328,7 @@
(const nil))))
"do" (analyze-seq ctx (rest items) env)
"throw" (throw-node (analyze ctx (nth items 1) env))
"def" (let [name-sym (nth items 1)]
;; ^{:map} metadata reads as (def (with-meta name m) v) — the
;; metadata is a runtime expression, so the interpreter evaluates
;; the whole def (it unwraps the name and merges the meta).
(when-not (form-sym? name-sym)
(uncompilable "def name with map metadata"))
(if (< (count items) 3)
;; (def name) with no init (declare): intern + reserve the cell so a
;; forward reference resolves. The back end keys on :no-init — Chez
;; def-var!s an unbound placeholder; the interpreter interns a
;; genuinely-unbound var.
(let [nm (form-sym-name name-sym) cur (compile-ns ctx)]
(host-intern! ctx cur nm)
{:op :def :ns cur :name nm :no-init true})
(let [nm (form-sym-name name-sym)
cur (compile-ns ctx)
;; (def name docstring value): docstring is form 2, value form 3.
;; Matches the interpreter; otherwise the docstring is taken as
;; the value and the real init dropped.
has-doc (and (> (count items) 3) (string? (nth items 2)))
val-form (nth items (if has-doc 3 2))
base0 (or (form-sym-meta name-sym) {})
;; resolve a ^Type hint to its canonical class name at def
;; time, as the JVM compiler does: ^String ->
;; java.lang.String. A record/unknown hint is left untouched.
tag (get base0 :tag)
tag-name (cond (form-sym? tag) (form-sym-name tag)
(string? tag) tag
:else nil)
base-meta (if tag-name
(let [c (resolve-class-hint tag-name)]
(if c (assoc base0 :tag c) base0))
base0)
node-meta (if has-doc (assoc base-meta :doc (nth items 2)) base-meta)]
(host-intern! ctx cur nm)
(def-node cur nm (analyze ctx val-form env) node-meta))))
"def" (analyze-def ctx items env)
"let*" (let [bvec (vec (form-vec-items (nth items 1)))
r (analyze-bindings ctx bvec env)]
(let-node (first r) (analyze-seq ctx (drop 2 items) (second r))))
@ -345,24 +373,7 @@
(host-intern! ctx cur nm)
{:op :defmacro :ns cur :name nm
:fn (analyze ctx fn-form env)})
"set!" (let [target (nth items 1)
val-node (analyze ctx (nth items 2) env)
ti (when (form-list? target) (vec (form-elements target)))
thead (when (and ti (pos? (count ti)) (form-sym? (first ti)))
(form-sym-name (first ti)))]
(cond
;; (set! (.-field obj) v): mutate a deftype instance field in place.
;; A deftype method's (set! mutable-field v) lowers to this shape.
(and thead (field-head? thead))
{:op :set-field :obj (analyze ctx (nth ti 1) env)
:field (subs thead 2) :val val-node}
;; (set! *var* v): set the var's innermost binding, else its root.
(form-sym? target)
(do (when (local? env (form-sym-name target)) (uncompilable "set! of a local"))
(let [r (resolve-global ctx target)]
(when-not (= :var (:kind r)) (uncompilable "set! of a non-var"))
{:op :set-var :the-var (the-var (:ns r) (:name r)) :val val-node}))
:else (uncompilable "set! of an unsupported target")))
"set!" (analyze-set! ctx items env)
(uncompilable (str "special form " op))))
;; Host interop method call. `(.method target arg*)` — a head that
@ -433,11 +444,6 @@
(invoke (analyze ctx member env) [(analyze ctx (nth items 1) env)])
:else (uncompilable "special form . (non-symbol member)"))))
;; A `.-field` head: `(.-field target)` is field access. Lowers to a :host-call
;; with the "-field" method (the dash signals field access to the dispatcher).
(defn- field-head? [nm]
(and (> (count nm) 2) (= ".-" (subs nm 0 2))))
(defn- analyze-field [ctx hname items env]
(when (< (count items) 2)
(throw (str "Malformed (.-field target) form")))

View file

@ -210,10 +210,12 @@
(str/join " " (mapcat (fn [p] [(emit-quoted (nth p 0)) (emit-quoted (nth p 1))]) pairs))
")"))
(defn- emit-quoted-map-value [m]
;; a jolt map VALUE (def/symbol metadata is a value, not a reader form)
(str "(jolt-hash-map "
(str/join " " (mapcat (fn [k] [(emit-quoted k) (emit-quoted (get m k))]) (keys m)))
")"))
;; A jolt map VALUE (def/symbol metadata is a value, not a reader form). (keys m)
;; iterates in host-hash order, which is not stable across Chez versions, so emit
;; the pairs sorted by their emitted Scheme text — keeps the seed byte-fixed
;; regardless of the host hash (jolt-8479).
(let [pairs (sort (map (fn [k] (str (emit-quoted k) " " (emit-quoted (get m k)))) (keys m)))]
(str "(jolt-hash-map " (str/join " " pairs) ")")))
;; emit-quoted reconstructs both raw reader forms (from :quote) AND plain jolt
;; values (def/symbol :meta). Reader forms are walked via the jolt.host form-*
;; contract; the native-predicate branches below catch genuine jolt collection
@ -230,14 +232,16 @@
(str "(jolt-symbol/meta " (if sns (chez-str-lit sns) "#f") " " (chez-str-lit nm) " "
(emit-quoted m) ")")
(str "(jolt-symbol " (if sns (chez-str-lit sns) "#f") " " (chez-str-lit nm) ")")))
(form-set? form) (str "(jolt-hash-set " (str/join " " (map emit-quoted (form-set-items form))) ")")
;; sort items by emitted text: a set has no source order, and host-hash order
;; is not stable across Chez versions (jolt-8479).
(form-set? form) (str "(jolt-hash-set " (str/join " " (sort (map emit-quoted (form-set-items form)))) ")")
(form-list? form) (str "(jolt-list " (str/join " " (map emit-quoted (form-elements form))) ")")
(form-vec? form) (str "(jolt-vector " (str/join " " (map emit-quoted (form-vec-items form))) ")")
(form-map? form) (emit-quoted-map (form-map-pairs form))
;; plain jolt VALUES (metadata maps and anything nested in them)
(map? form) (emit-quoted-map-value form)
(vector? form) (str "(jolt-vector " (str/join " " (map emit-quoted form)) ")")
(set? form) (str "(jolt-hash-set " (str/join " " (map emit-quoted form)) ")")
(set? form) (str "(jolt-hash-set " (str/join " " (sort (map emit-quoted form))) ")")
(seq? form) (str "(jolt-list " (str/join " " (map emit-quoted form)) ")")
:else (throw (ex-info (str "emit-quoted: unsupported quoted form " (pr-str form)) {}))))

View file

@ -83,22 +83,26 @@
declarations from every dep's deps.edn. `base-dir` resolves :local/root and is
replaced by a dep's own root as the walk descends."
[deps base-dir]
;; queue grows by appending children at the tail; an index cursor walks it so
;; each dequeue is O(1) (was (subvec (vec queue) 1) per pop -> O(n^2)).
(loop [queue (mapv (fn [[c s]] [c s base-dir]) (seq deps))
i 0
seen #{}
roots []
natives []]
(if (empty? queue)
(if (>= i (count queue))
{:roots (distinct roots) :natives natives}
(let [[coord spec bd] (first queue)
queue (subvec (vec queue) 1)]
(let [[coord spec bd] (nth queue i)
i (inc i)]
(if (contains? seen coord)
(recur queue seen roots natives)
(recur queue i seen roots natives)
(let [root (coord-root coord spec bd)]
(if (nil? root)
(recur queue (conj seen coord) roots natives)
(recur queue i (conj seen coord) roots natives)
(let [edn (read-edn (str root "/deps.edn"))
child (mapv (fn [[c s]] [c s root]) (seq (:deps edn)))]
(recur (into queue child)
i
(conj seen coord)
(into roots (dep-source-roots root))
(into natives (:jolt/native edn)))))))))))

View file

@ -20,9 +20,6 @@
;; end emits the embedded var cell so `binding`'s thread-binding frame can key on it.
(defn the-var [ns name] {:op :the-var :ns ns :name name})
;; A runtime primitive (cons, +, get, apply, …) the back end maps to the host RT.
(defn rt [name] {:op :rt :name name})
;; A name that resolves only via the host's own environment (e.g. + or int?) —
;; the back end emits a host-appropriate reference.
(defn host-ref [name] {:op :host :name name})
@ -69,8 +66,6 @@
(defn quote-node [form] {:op :quote :form form})
(defn throw-node [expr] {:op :throw :expr expr})
(defn op [node] (:op node))
;; ---------------------------------------------------------------------------
;; Structural recursion over IR child nodes.
;;
@ -127,5 +122,39 @@
n (if (get node :catch-body) (assoc n :catch-body (f (get node :catch-body))) n)
n (if (get node :finally) (assoc n :finally (f (get node :finally))) n)]
n)
;; :const :local :var :host :host-static :the-var :rt :quote — no child nodes
;; :const :local :var :host :host-static :the-var :quote — no child nodes
:else node)))
;; The read-only companion to map-ir-children: fold f over node's child IR nodes,
;; left to right, threading acc — same single-sourced child layout, so a read-only
;; analysis (size/closedness/purity) built on it is TOTAL over the op set (an
;; unknown op, or a leaf, folds over no children and returns acc unchanged). Skips
;; the same non-node positions map-ir-children does (binding NAMES, fn :params/
;; :rest, :op/:ns/:name/:val). f is (acc child) -> acc.
(defn reduce-ir-children [f acc node]
(let [op (get node :op)]
(cond
(= op :if) (f (f (f acc (get node :test)) (get node :then)) (get node :else))
(= op :do) (f (reduce f acc (get node :statements)) (get node :ret))
(= op :throw) (f acc (get node :expr))
(= op :set-var) (f acc (get node :val))
(= op :set-field) (f (f acc (get node :obj)) (get node :val))
(= op :defmacro) (f acc (get node :fn))
(= op :invoke) (reduce f (f acc (get node :fn)) (get node :args))
(= op :vector) (reduce f acc (get node :items))
(= op :set) (reduce f acc (get node :items))
(= op :map) (reduce (fn [a pr] (f (f a (nth pr 0)) (nth pr 1))) acc (get node :pairs))
(= op :let) (f (reduce (fn [a b] (f a (nth b 1))) acc (get node :bindings)) (get node :body))
(= op :loop) (f (reduce (fn [a b] (f a (nth b 1))) acc (get node :bindings)) (get node :body))
(= op :recur) (reduce f acc (get node :args))
(= op :fn) (reduce (fn [a ar] (f a (get ar :body))) acc (get node :arities))
(= op :def) (if (get node :init) (f acc (get node :init)) acc)
(= op :host-call) (reduce f (f acc (get node :target)) (get node :args))
(= op :host-new) (reduce f acc (get node :args))
(= op :try)
(let [a (f acc (get node :body))
a (if (get node :catch-body) (f a (get node :catch-body)) a)
a (if (get node :finally) (f a (get node :finally)) a)]
a)
;; leaves and any op with no child nodes
:else acc)))

View file

@ -23,6 +23,10 @@
reset-escapes! collected-escapes
set-check-mode! take-diags!]]))
;; Cap on inline -> flatten -> scalar-replace -> const-fold iterations. Each pass
;; sets `dirty` when it rewrote something; the loop stops at a clean pass or here.
(def ^:private inline-fixpoint-cap 8)
(defn run-passes
"All passes, in order. The back end applies this to every analyzed form. When
inlining is enabled for the unit (user code under direct-linking),
@ -41,7 +45,7 @@
opt (loop [i 0 n (const-fold node)]
(reset! dirty false)
(let [n2 (const-fold (scalar-replace (flatten-lets (inline-node n ctx))))]
(if (and @dirty (< i 8))
(if (and @dirty (< i inline-fixpoint-cap))
(recur (inc i) n2)
n2)))]
;; a final const-fold after inference propagates any predicate folded to a

View file

@ -46,7 +46,9 @@
folded (when (and ff (pos? (count args)) (every? const-num? args))
(try
{:op :const :val (apply ff (mapv (fn [a] (get a :val)) args))}
(catch Exception e nil)))]
;; :default (not Exception) — match the rest of jolt-core and
;; also catch a raw host condition from a folding primitive.
(catch :default e nil)))]
(or folded n))
(= op :if)

View file

@ -4,7 +4,7 @@
share the alpha-rename invariant (every spliced binder is made globally fresh)
and the `dirty` fixpoint flag. Portable Clojure (compiler-tier)."
(:require [jolt.host :refer [inline-ir]]
[jolt.ir :refer [map-ir-children]]
[jolt.ir :refer [map-ir-children reduce-ir-children]]
[jolt.passes.fold :refer [scalar-const?]]))
;; ---------------------------------------------------------------------------
@ -54,27 +54,12 @@
(defn- body-size
"Node count of an inline-eligible body. A disallowed op contributes a number
larger than any budget, so the caller's (<= size budget) test fails and we
never try to inline (or alpha-rename) such a body."
never try to inline (or alpha-rename) such a body. Only reached for safe ops,
so the shared child fold covers it exactly (leaves fold to 1)."
[node]
(let [op (get node :op)]
(cond
(not (safe-op? op)) 100000
(= op :if) (+ 1 (body-size (get node :test))
(body-size (get node :then))
(body-size (get node :else)))
(= op :do) (+ 1 (reduce + 0 (mapv body-size (get node :statements)))
(body-size (get node :ret)))
(= op :throw) (+ 1 (body-size (get node :expr)))
(= op :invoke) (+ 1 (body-size (get node :fn))
(reduce + 0 (mapv body-size (get node :args))))
(= op :let) (+ 1 (reduce + 0 (mapv (fn [b] (body-size (nth b 1))) (get node :bindings)))
(body-size (get node :body)))
(= op :vector) (+ 1 (reduce + 0 (mapv body-size (get node :items))))
(= op :set) (+ 1 (reduce + 0 (mapv body-size (get node :items))))
(= op :map) (+ 1 (reduce + 0 (mapv (fn [pr] (+ (body-size (nth pr 0))
(body-size (nth pr 1))))
(get node :pairs))))
:else 1)))
(if (not (safe-op? (get node :op)))
100000
(reduce-ir-children (fn [acc c] (+ acc (body-size c))) 1 node)))
(defn- subst
"Substitute locals in node per env (a map name -> replacement IR node), and
@ -129,24 +114,8 @@
(let [op (get node :op)]
(cond
(= op :local) (contains? scope (get node :name))
(= op :const) true
(= op :var) true
(= op :host) true
(= op :the-var) true
(= op :quote) true
(= op :if) (and (body-closed? (get node :test) scope)
(body-closed? (get node :then) scope)
(body-closed? (get node :else) scope))
(= op :do) (and (every? (fn [s] (body-closed? s scope)) (get node :statements))
(body-closed? (get node :ret) scope))
(= op :throw) (body-closed? (get node :expr) scope)
(= op :invoke) (and (body-closed? (get node :fn) scope)
(every? (fn [a] (body-closed? a scope)) (get node :args)))
(= op :vector) (every? (fn [x] (body-closed? x scope)) (get node :items))
(= op :set) (every? (fn [x] (body-closed? x scope)) (get node :items))
(= op :map) (every? (fn [pr] (and (body-closed? (nth pr 0) scope)
(body-closed? (nth pr 1) scope)))
(get node :pairs))
;; :let threads scope sequentially (each binding extends it), so it can't go
;; through the uniform fold.
(= op :let)
(let [res (reduce (fn [acc b]
(let [sc (nth acc 0) ok (nth acc 1)]
@ -156,6 +125,9 @@
[scope true]
(get node :bindings))]
(and (nth res 1) (body-closed? (get node :body) (nth res 0))))
;; leaves (:const/:var/:host/:the-var/:quote) fold to true; the rest AND
;; their children. Unsafe ops never reach here (body-size rejects them).
(safe-op? op) (reduce-ir-children (fn [ok c] (and ok (body-closed? c scope))) true node)
:else false)))
(defn- try-inline
@ -267,20 +239,13 @@
[node]
(let [op (get node :op)]
(cond
(= op :const) true
(= op :local) true
(= op :var) true
(= op :host) true
(= op :the-var) true
(= op :quote) true
(= op :if) (and (pure? (get node :test)) (pure? (get node :then)) (pure? (get node :else)))
(= op :do) (and (every? pure? (get node :statements)) (pure? (get node :ret)))
(= op :let) (and (every? (fn [b] (pure? (nth b 1))) (get node :bindings)) (pure? (get node :body)))
(= op :vector) (every? pure? (get node :items))
(= op :set) (every? pure? (get node :items))
(= op :map) (every? (fn [pr] (and (pure? (nth pr 0)) (pure? (nth pr 1)))) (get node :pairs))
;; :invoke is pure only for a known-pure fn / record ctor, and only its ARGS
;; are folded (not the :fn position) — so it can't go through the uniform fold.
(= op :invoke) (and (or (pure-fn? (get node :fn)) (ctor-shape node))
(every? pure? (get node :args)))
;; leaves (:const/:local/:var/:host/:the-var/:quote) fold to true; :if/:do/
;; :let/:vector/:set/:map AND their children's purity.
(safe-op? op) (reduce-ir-children (fn [ok c] (and ok (pure? c))) true node)
:else false)))
(defn- const-key-map? [node]
@ -340,7 +305,12 @@
"Does local nm escape in node i.e. is it used anywhere other than as the
subject of a constant-keyword lookup? Precise over straight-line expression
ops; conservatively true for loop/fn/try/recur/def (and any rebinding of nm),
so scalar replacement only fires where the whole use region is simple."
so scalar replacement only fires where the whole use region is simple.
Stays an explicit per-op walk (not the shared reduce-ir-children fold): its
default is conservatively TRUE, and the lookup-subject and rebinding cases
inspect node shape beyond child purity folding an unhandled op over its
children would under-report escape and is unsound for scalar replacement."
[node nm]
(let [op (get node :op)
k (lookup-key node nm)]

View file

@ -5,223 +5,78 @@
checker. Also the inter-procedural driver API the back end calls to
propagate param types across a unit / the whole program. Weakly coupled to the
IR-rewriting passes shares only the const-shape predicate (jolt.passes.fold)."
(:require [jolt.passes.fold :refer [scalar-const?]]))
(:require [jolt.passes.fold :refer [scalar-const?]]
[jolt.passes.types.lattice :refer
[velem selem sfields vec-type? set-type? struct-type? mk-vec mk-set
mk-struct union-cap scalar-t? union-type? umembers union-of merge-fields
join-t join type-depth cap struct-safe? field-type shape-order type-shape
mark-struct truthy-type? num-ret-fns vector-ret-fns]]))
;; ---------------------------------------------------------------------------
;; Collection-type inference, intra-procedural. A forward,
;; soft-typing-style pass (simplified HM: monovariant, never-fails, lattice top
;; = :any) that types expressions from literals/arithmetic and flows the type
;; through let bindings and if-joins. Where a keyword-lookup subject is PROVEN a
;; plain struct map it sets :hint :struct (the same channel a manual hint uses,
;; so the back end drops the guard); where the type is :any it leaves the
;; dynamic guard in place. Sound by construction: a concrete type is assigned
;; only when proven, so a wrong bare get is impossible.
;; --- engine state ------------------------------------------------------------
;; The walk threads an immutable `env` (mk-env) instead of reading scattered
;; module atoms: it carries the read-only config (rtenv/vtypes/record-shapes/
;; protocol-methods/map-shapes?) plus the per-run flags (checking?/strict?) and
;; per-run accumulator/guard CELLS (diags/calls/checking-set/diag-memo). A fresh
;; env per run makes the pass re-entrant — a nested probe (isolated-diag-count)
;; runs under a sub-env with its own diags cell, no save/restore.
;;
;; Recursive STRUCTURAL types (RFC 0005). A type mirrors the data tree:
;; compound: {:struct {field -> T}} (raw-get-safe map, field types)
;; {:vec T} (vector of T)
;; {:set T} (set of T)
;; scalar: :num :str :kw :truthy (all provably non-nil/non-false)
;; :phm (persistent hash map; NOT raw-get-safe)
;; :any (top), nil (bottom, identity for join).
;; Compound types are small jolt maps, so they compare by value on both the
;; Clojure and the host (orchestrator) side. struct/vec/set use distinct keys so
;; a type is recognised by which key it carries.
;; (get t :KEY) is nil for a keyword type and the child for a compound, so a
;; compound is detected by some? — no map?/contains? needed.
(defn- velem [t] (get t :vec))
(defn- selem [t] (get t :set))
(defn- sfields [t] (get t :struct))
(defn- vec-type? [t] (some? (velem t)))
(defn- set-type? [t] (some? (selem t)))
(defn- struct-type? [t] (some? (sfields t)))
(defn- mk-vec [t] {:vec (if t t :any)})
(defn- mk-set [t] {:set (if t t :any)})
(defn- mk-struct [fs] {:struct fs})
;; Only state whose lifecycle spans separate API calls stays module-level: the
;; config the orchestrator installs (set-*! before a sweep), the escapes and
;; user-sig registries (collected/registered across the forms of a sweep), and a
;; bridge holding the last checking run's diagnostics for take-diags!.
(def ^:private config-box
(atom {:rtenv {} ;; "ns/name" -> inferred return type
:vtypes {} ;; "ns/name" -> var VALUE type (fn=:truthy, def=init type)
:record-shapes {} ;; "ns/->Name" -> {:fields :tags :type}
:protocol-methods {} ;; "ns/method" -> [proto method]
:map-shapes? false})) ;; shape generic const-key maps (opt-in, JOLT_SHAPE)
;; var-keys used as a VALUE (not a call head) — accumulated across a whole sweep,
;; reset by reset-escapes! and read by collected-escapes.
(def ^:private escapes-box (atom #{}))
;; User-function error domains, opt-in. As the checker walks defs it registers
;; each non-redefinable single-fixed-arity user fn's {:params :body} here, keyed
;; "ns/name"; a later call site (strict mode) re-checks the body with one param
;; bound to its concrete argument type. Accumulates ACROSS forms — a def must
;; precede its call (the closed-world ordering RFC 0005 assumes).
(def ^:private user-sig-box (atom {})) ;; "ns/name" -> {:params [..] :body ir}
;; Diagnostics from the last checking run-inference, for take-diags! to drain.
(def ^:private last-diags-box (atom []))
;; Whether run-inference also checks, and strictly. Set by set-check-mode!.
(def ^:private check-mode-box (atom {:on false :strict false}))
;; Bounded union types (RFC 0006). A union {:union #{T...}} records
;; that a value is provably one of a small, fixed set of SCALAR types — what
;; differing if-branches used to collapse to :any. It exists so the success
;; checker can reject a use where EVERY member is in the op's error domain
;; ((inc (if c "a" :k))) while still accepting one where any member is valid
;; ((inc (if c 1 "x"))). Scalars only, capped cardinality: the member space is
;; the five scalar tags, so the lattice stays finite and the inter-procedural
;; fixpoint terminates. A union is opaque to every STRUCTURAL predicate
;; (struct-type?/vec-type?/set-type? key on :struct/:vec/:set, which a union
;; lacks), so specialization treats it exactly like :any — codegen is
;; unchanged; only the checker reads inside it.
(def ^:private union-cap 4)
(defn- scalar-t? [t] (or (= t :num) (= t :str) (= t :kw) (= t :truthy) (= t :phm)))
(defn- union-type? [t] (some? (get t :union)))
(defn- umembers [t] (get t :union))
(defn- union-of
"Normalize a seq of member types into a lattice value: flatten nested unions,
keep only scalars (any non-scalar member collapses the whole thing to :any,
the conservative top), then return the lone member if one, {:union #{...}}
for 2..cap distinct scalars, or :any past the cap."
[ts]
(let [flat (reduce (fn [acc t]
(if (union-type? t)
(reduce conj acc (umembers t))
(conj acc t)))
#{} ts)]
(cond
(not (every? scalar-t? flat)) :any
(= 0 (count flat)) :any
(= 1 (count flat)) (first flat)
(> (count flat) union-cap) :any
:else {:union flat})))
;; build a per-run env: a snapshot of the installed config plus this run's flags
;; and fresh accumulator/guard cells. escapes/user-sigs reference the sweep-level
;; module cells (their lifecycle spans calls); diags/calls/checking-set/diag-memo
;; are this run's own.
(defn- mk-env [checking? strict?]
(let [c @config-box]
{:rtenv (get c :rtenv) :vtypes (get c :vtypes)
:record-shapes (get c :record-shapes) :protocol-methods (get c :protocol-methods)
:map-shapes? (get c :map-shapes?)
:checking? checking? :strict? strict?
:diags (atom []) :calls (atom []) :checking-set (atom #{}) :diag-memo (atom {})
:escapes escapes-box :user-sigs user-sig-box}))
(declare join-t)
(defn- merge-fields
"Per-field join of two field maps (a key in only one side joins with :any)."
[fa fb]
(let [m1 (reduce (fn [m k] (assoc m k (join-t (get fa k :any) (get fb k :any)))) {} (keys fa))]
(reduce (fn [m k] (if (get m k) m (assoc m k (join-t (get fa k :any) (get fb k :any))))) m1 (keys fb))))
(defn- join-t [a b]
(cond
(= a b) a
(nil? a) b
(nil? b) a
(and (struct-type? a) (struct-type? b))
(let [merged (mk-struct (merge-fields (sfields a) (sfields b)))]
;; joining two values of the SAME complete shape preserves it — the
;; merged struct has the same key set. Different shapes
;; (or an incomplete side) drop it, as the layout is no longer proven.
(if (and (get a :shape) (= (get a :shape) (get b :shape)))
(assoc merged :shape (get a :shape))
merged))
(and (vec-type? a) (vec-type? b)) (mk-vec (join-t (velem a) (velem b)))
(and (set-type? a) (set-type? b)) (mk-set (join-t (selem a) (selem b)))
;; differing kinds: form a scalar union when both sides reduce to scalars
;; (or scalar unions); anything compound on either side stays :any
:else (let [ma (cond (union-type? a) (umembers a) (scalar-t? a) #{a} :else nil)
mb (cond (union-type? b) (umembers b) (scalar-t? b) #{b} :else nil)]
(if (and ma mb) (union-of (reduce conj ma mb)) :any))))
(defn- join [a b] (join-t a b))
;; depth cap (RFC 0005): truncate a type below depth d to :any, so recursive data
;; can't make an infinite type and the inter-procedural fixpoint stays finite.
(def ^:private type-depth 4)
(defn- cap [t d]
(cond
(<= d 0) (if (or (struct-type? t) (vec-type? t) (set-type? t)) :any t)
(struct-type? t)
;; capping truncates VALUES below depth d, but the KEY SET is unchanged, so
;; a complete :shape survives — keep it so nested/container field reads can
;; still bare-index. cap recurses into fields, so a nested
;; shaped value (a vec3 inside a hit-info) keeps its own :shape too.
(let [capped (mk-struct (reduce (fn [m k] (assoc m k (cap (get (sfields t) k) (dec d))))
{} (keys (sfields t))))
;; the record :type tag (and :shape) are independent of field-value
;; depth, so they survive truncation — a record read from a deep
;; container keeps its identity, so devirtualization, record? folding,
;; and the record fast path still fire on it.
capped (if (get t :shape) (assoc capped :shape (get t :shape)) capped)
capped (if (get t :type) (assoc capped :type (get t :type)) capped)]
capped)
(vec-type? t) (mk-vec (cap (velem t) (dec d)))
(set-type? t) (mk-set (cap (selem t) (dec d)))
:else t))
;; raw-get-safe (a struct / record): a struct type. The field type of key
;; k, if known, else :any.
(defn- struct-safe? [t] (struct-type? t))
(defn- field-type [t k] (if (struct-type? t) (get (sfields t) k :any) :any))
;; Shape (hidden class). A struct type built from a map LITERAL carries
;; its complete layout — :shape, the canonical (str-sorted) key vector. The back
;; end represents such a map as a shape tuple and reads a field by bare index.
;; A struct type from a JOIN or from field-access inference has no :shape
;; (incomplete: the full key set isn't proven), so it keeps the dynamic path —
;; never a bare index. No shape is hardcoded; any constant key set is one.
(defn- shape-order
"Canonical key order for a shape: keys sorted by their string form, so two
literals with the same keys in any order intern to the same shape."
[ks] (vec (sort (fn [a b] (compare (str a) (str b))) ks)))
(defn- type-shape [t] (get t :shape))
;; tag a node (any expression, not just a :local) so the back end can specialize
;; a lookup whose SUBJECT is that node — this is what makes nested access work:
;; (:direction ray) is tagged struct, so (:r (:direction ray)) drops its guard.
;; tag a lookup subject as a struct, carrying the complete shape when known
;; (so the back end bare-indexes).
(defn- mark-struct [node t]
(let [n (assoc node :hint :struct)]
(if (get t :shape) (assoc n :shape (get t :shape)) n)))
;; a value provably neither nil nor false — the back end only builds a struct
;; (vs a phm) when every value is non-nil/non-false, so a map literal is a struct
;; only when all its values have such a type. Collections are non-nil.
(defn- truthy-type? [t]
(or (= t :num) (= t :str) (= t :kw) (= t :truthy) (= t :phm)
(struct-type? t) (vec-type? t) (set-type? t)))
;; core fns whose result is a number (so it is non-nil/non-false and, for the
;; success-type checker, provably numeric).
(def ^:private num-ret-fns
#{"+" "-" "*" "/" "inc" "dec" "mod" "rem" "quot" "min" "max" "abs"
"bit-and" "bit-or" "bit-xor" "count"})
(def ^:private vector-ret-fns #{"vec" "vector" "mapv" "filterv" "subvec"})
;; Inter-procedural state. The orchestrator (backend
;; infer-unit!) drives a whole-unit fixpoint: before typing a fn body it installs
;; the current return-type estimates of all unit fns here, and after typing it
;; reads back the call sites this body made (callee + inferred arg types) to
;; propagate into callee param types. Both are plain module state, like `dirty`.
(def ^:private rtenv-box (atom {})) ;; "ns/name" -> inferred return type
(def ^:private calls-box (atom [])) ;; collected [ "ns/name" [arg-types...] ]
(def ^:private escapes-box (atom #{})) ;; var-keys used as a VALUE (not a call head)
(def ^:private diag-box (atom [])) ;; success-type-check diagnostics (RFC 0006)
;; a var reference's VALUE type — a fn var is :truthy (non-nil), a def
;; var carries its inferred init type (e.g. a color table -> {:vec :struct-map}).
;; The orchestrator populates this from sealed (opt-mode) cell roots + def inits.
(def ^:private vtype-box (atom {})) ;; "ns/name" -> value type
;; User-function error domains, opt-in. As the checker walks defs it
;; registers each non-redefinable single-fixed-arity user fn's {:params :body}
;; here, keyed "ns/name". At a later call site (strict mode only) the body is
;; re-checked with ONE parameter bound to its concrete argument type — if that
;; alone produces a diagnostic the all-:any body did not, that argument is
;; provably wrong and the CALL is reported. Module state, like rtenv-box: a def
;; must precede its call (the same closed-world ordering RFC 0005 assumes).
(def ^:private user-sig-box (atom {})) ;; "ns/name" -> {:params [..] :body ir}
;; a record constructor's return shape. "ns/->Name" -> [field-kw ...]
;; in DECLARED order (the runtime lays records out in declared field order, so
;; the back end bare-indexes by that order). A call (->Point a b) types as a
;; struct of this shape, so field reads on the result bare-index — declared
;; shapes are clean fuel: a lookup, not fragile inference.
(def ^:private record-shapes-box (atom {}))
;; protocol-method registry "ns/method" -> [proto method], for
;; devirtualizing a protocol call whose receiver is a known record type.
(def ^:private protocol-methods-box (atom {}))
;; build a record's struct TYPE from its registry entry, resolving each
;; field's declared type hint. A field tagged with a record type (its ctor-key)
;; recurses, so a Vec3 stored in a Ray field reads back as Vec3 — not :any —
;; which is what lets nested-record code prove its reads. Depth-bounded so a
;; self/cyclic-referencing record type can't loop.
;; build a record's struct TYPE from its registry entry, resolving each field's
;; declared type hint against `shapes` ("ns/->Name" -> entry). A field tagged with
;; a record type (its ctor-key) recurses, so a Vec3 stored in a Ray field reads
;; back as Vec3 — not :any — which is what lets nested-record code prove its reads.
;; Depth-bounded so a self/cyclic-referencing record type can't loop.
(declare record-type-from-entry)
(defn- field-type-from-tag [tag depth]
(defn- field-type-from-tag [tag depth shapes]
(cond
(or (nil? tag) (<= depth 0)) :any
(= tag "num") :num
:else (let [e (get @record-shapes-box tag)]
(if e (record-type-from-entry e depth) :any))))
(defn- record-type-from-entry [rs depth]
:else (let [e (get shapes tag)]
(if e (record-type-from-entry e depth shapes) :any))))
(defn- record-type-from-entry [rs depth shapes]
(let [fields (get rs :fields)
tags (get rs :tags)
fmap (reduce (fn [m i]
(assoc m (nth fields i)
(field-type-from-tag (when tags (nth tags i)) (dec depth))))
(field-type-from-tag (when tags (nth tags i)) (dec depth) shapes)))
{} (range (count fields)))]
(assoc (mk-struct fmap) :shape (vec fields) :type (get rs :type))))
;; whether to shape generic const-key MAP literals (opt-in, JOLT_SHAPE).
;; Records are shaped regardless; maps only when this is on.
(def ^:private map-shapes-box (atom false))
(def ^:private checking-box (atom #{})) ;; keys mid-recheck — cycle guard
(def ^:private strict-box (atom false)) ;; report against user-fn domains?
;; When true, `infer` emits success-type diagnostics as it types (jolt audit).
;; The checker IS the inference walk now — one O(n) pass that both types and
;; checks, instead of a separate check-walk that re-inferred every subtree
;; (quadratic in nesting). Off during the optimization fixpoint so it doesn't
;; emit intermediate diagnostics; on only inside check-form.
(def ^:private checking? (atom false))
;; fns that RETURN an element of their (first) collection arg, so a lookup on the
;; result of (rand-nth coll-of-structs) etc. types as the element.
@ -232,18 +87,19 @@
(defn- var-key [fnode] (str (get fnode :ns) "/" (get fnode :name)))
(defn- call-ret-type [fnode]
(let [op (get fnode :op)]
(defn- call-ret-type [fnode env]
(let [op (get fnode :op)
shapes (get env :record-shapes)]
(cond
;; a user fn whose return type the fixpoint has estimated
(= op :var) (let [rs (get @record-shapes-box (var-key fnode))]
(= op :var) (let [rs (get shapes (var-key fnode))]
(if rs
;; record ctor -> struct of declared shape; :shape
;; is the DECLARED field order the back end indexes by, :type
;; the record tag (devirt), and field types come from the
;; declared hints so nested records stay typed
(record-type-from-entry rs type-depth)
(let [r (get @rtenv-box (var-key fnode))]
(record-type-from-entry rs type-depth shapes)
(let [r (get (get env :rtenv) (var-key fnode))]
(if r r (let [nm (and (= "clojure.core" (get fnode :ns)) (get fnode :name))]
(cond (nil? nm) :any
(contains? num-ret-fns nm) :num
@ -302,7 +158,7 @@
types (seeds: param-index -> type), other params :any, captured locals from
tenv. Returns [ret-type node'] ret is the lub of arity tail types, used to
type the HOF result (e.g. reduce's accumulator, mapv's element)."
[node seeds tenv]
[node seeds tenv env]
(let [res (mapv (fn [a]
(let [params (get a :params)
pe (reduce (fn [e i]
@ -310,7 +166,7 @@
(let [s (get seeds i)] (if s s :any))))
tenv (range (count params)))
pe (if (get a :rest) (assoc pe (get a :rest) :any) pe)
br (infer (get a :body) pe)]
br (infer (get a :body) pe env)]
[(nth br 0) (assoc a :body (nth br 1))]))
(get node :arities))
rets (mapv (fn [r] (nth r 0)) res)
@ -320,8 +176,8 @@
(defn- infer
"Returns [type node'] the inferred type of node and node with struct-safe
:local references annotated :hint :struct. tenv maps in-scope local names to
inferred types."
[node tenv]
inferred types; env carries the inference config and this run's accumulators."
[node tenv env]
(let [op (get node :op)]
(cond
(= op :const)
@ -343,8 +199,8 @@
(= op :map)
(let [pairs (get node :pairs)
res (mapv (fn [pr]
(let [kr (infer (nth pr 0) tenv)
vr (infer (nth pr 1) tenv)]
(let [kr (infer (nth pr 0) tenv env)
vr (infer (nth pr 1) tenv env)]
[(nth kr 1) (nth vr 1) (nth vr 0) (get (nth pr 0) :val)]))
pairs)
struct? (and (> (count res) 0)
@ -354,38 +210,38 @@
(cap (mk-struct (reduce (fn [m r] (assoc m (nth r 3) (nth r 2))) {} res)) type-depth))
;; a literal is a COMPLETE shape: carry its sorted key vector so the
;; back end can lay it out and bare-index lookups
shp (when (and @map-shapes-box base (struct-type? base)) (shape-order (keys (sfields base))))
shp (when (and (get env :map-shapes?) base (struct-type? base)) (shape-order (keys (sfields base))))
t (if base (if shp (assoc base :shape shp) base) :any)
node' (assoc node :pairs (mapv (fn [r] [(nth r 0) (nth r 1)]) res))]
[t (if shp (assoc node' :shape shp) node')])
(= op :vector)
(let [irs (mapv (fn [x] (infer x tenv)) (get node :items))
(let [irs (mapv (fn [x] (infer x tenv env)) (get node :items))
ets (mapv (fn [r] (nth r 0)) irs)
el (if (empty? ets) :any (reduce join (first ets) (rest ets)))]
[(cap (mk-vec el) type-depth) (assoc node :items (mapv (fn [r] (nth r 1)) irs))])
(= op :set)
(let [irs (mapv (fn [x] (infer x tenv)) (get node :items))
(let [irs (mapv (fn [x] (infer x tenv env)) (get node :items))
ets (mapv (fn [r] (nth r 0)) irs)
el (if (empty? ets) :any (reduce join (first ets) (rest ets)))]
[(cap (mk-set el) type-depth) (assoc node :items (mapv (fn [r] (nth r 1)) irs))])
(= op :if)
(let [tr (infer (get node :test) tenv)
thn (infer (get node :then) tenv)
els (infer (get node :else) tenv)]
(let [tr (infer (get node :test) tenv env)
thn (infer (get node :then) tenv env)
els (infer (get node :else) tenv env)]
[(join (nth thn 0) (nth els 0))
(assoc node :test (nth tr 1) :then (nth thn 1) :else (nth els 1))])
(= op :do)
(let [stmts (mapv (fn [s] (nth (infer s tenv) 1)) (get node :statements))
r (infer (get node :ret) tenv)]
(let [stmts (mapv (fn [s] (nth (infer s tenv env) 1)) (get node :statements))
r (infer (get node :ret) tenv env)]
[(nth r 0) (assoc node :statements stmts :ret (nth r 1))])
(= op :throw)
[:any (assoc node :expr (nth (infer (get node :expr) tenv) 1))]
[:any (assoc node :expr (nth (infer (get node :expr) tenv env) 1))]
;; a :var reached HERE is in value position (an arg, a let init, ...), not
;; a call head — so the fn it names escapes and its params can't be inferred.
;; Its VALUE type comes from vtype-box (a fn is :truthy, a def carries its
;; Its VALUE type comes from vtypes (a fn is :truthy, a def carries its
;; inferred type); unknown -> :any.
(= op :var) (do (swap! escapes-box conj (var-key node))
[(let [vt (get @vtype-box (var-key node))] (if vt vt :any)) node])
(= op :var) (do (swap! (get env :escapes) conj (var-key node))
[(let [vt (get (get env :vtypes) (var-key node))] (if vt vt :any)) node])
(= op :invoke)
(let [fnode (get node :fn)
iscall-var (= :var (get fnode :op))
@ -399,32 +255,32 @@
;; after inference) collapsing any `if` it gates. Falls through to the
;; normal call path when the answer isn't provable or the arg is impure.
(and iscall-var (contains? fold-preds cn) (= n 1))
(let [ar (infer (nth args 0) tenv)
(let [ar (infer (nth args 0) tenv env)
v (pred-on cn (nth ar 0))]
(if (and (not (nil? v)) (pure-node? (nth ar 1)))
[:any {:op :const :val v}]
[(call-ret-type fnode) (assoc node :args [(nth ar 1)])]))
[(call-ret-type fnode env) (assoc node :args [(nth ar 1)])]))
;; (:k m) / (:k m default): the result is m's field type, and if m is a
;; struct the subject is tagged so the back end drops the guard — this
;; types nested access end to end (RFC 0005).
(and (= :const (get fnode :op)) (keyword? (get fnode :val)) (>= n 1) (<= n 2))
(let [mr (infer (nth args 0) tenv)
(let [mr (infer (nth args 0) tenv env)
mt (nth mr 0)
msub (if (struct-safe? mt) (mark-struct (nth mr 1) mt) (nth mr 1))
ft (field-type mt (get fnode :val))
dr (when (= n 2) (infer (nth args 1) tenv))]
dr (when (= n 2) (infer (nth args 1) tenv env))]
[(if dr (join ft (nth dr 0)) ft)
(assoc node :args (if dr [msub (nth dr 1)] [msub]))])
;; (get m :k [default]): same, when the key is a constant keyword.
(and (or (and (= :var (get fnode :op)) (= "clojure.core" (get fnode :ns)) (= "get" (get fnode :name)))
(and (= :host (get fnode :op)) (= "get" (get fnode :name))))
(>= n 2) (= :const (get (nth args 1) :op)) (keyword? (get (nth args 1) :val)))
(let [mr (infer (nth args 0) tenv)
(let [mr (infer (nth args 0) tenv env)
mt (nth mr 0)
msub (if (struct-safe? mt) (mark-struct (nth mr 1) mt) (nth mr 1))
kr (infer (nth args 1) tenv)
kr (infer (nth args 1) tenv env)
ft (field-type mt (get (nth args 1) :val))
dr (when (= n 3) (infer (nth args 2) tenv))]
dr (when (= n 3) (infer (nth args 2) tenv env))]
[(if dr (join ft (nth dr 0)) ft)
(assoc node :args (if dr [msub (nth kr 1) (nth dr 1)] [msub (nth kr 1)]))])
;; reduce over a typed vector with a fn-literal: seed the
@ -433,11 +289,11 @@
;; it makes — see those types.
(and (= cn "reduce") (>= n 2) (= :fn (get (nth args 0) :op)))
(let [three (>= n 3)
coll-r (infer (nth args (if three 2 1)) tenv)
init-r (when three (infer (nth args 1) tenv))
coll-r (infer (nth args (if three 2 1)) tenv env)
init-r (when three (infer (nth args 1) tenv env))
et (let [ct (nth coll-r 0)] (if (vec-type? ct) (velem ct) :any))
init-t (if init-r (nth init-r 0) :any)
fn-r (infer-fn-seeded (nth args 0) {0 init-t 1 et} tenv)]
fn-r (infer-fn-seeded (nth args 0) {0 init-t 1 et} tenv env)]
[(join init-t (nth fn-r 0))
(assoc node :args (if three
[(nth fn-r 1) (nth init-r 1) (nth coll-r 1)]
@ -445,16 +301,16 @@
;; map/mapv/filter/... over a typed vector with a fn-literal: seed the
;; fn's element param; mapv/filterv produce a typed vector.
(and cn (get hof-table cn) (>= n 2) (= :fn (get (nth args 0) :op)))
(let [coll-r (infer (nth args 1) tenv)
(let [coll-r (infer (nth args 1) tenv env)
et (let [ct (nth coll-r 0)] (if (vec-type? ct) (velem ct) :any))
fn-r (infer-fn-seeded (nth args 0) {(get (get hof-table cn) :epos) et} tenv)
fn-r (infer-fn-seeded (nth args 0) {(get (get hof-table cn) :epos) et} tenv env)
rt (cond (= cn "mapv") (mk-vec (nth fn-r 0))
(= cn "filterv") (mk-vec et)
:else :any)]
[rt (assoc node :args [(nth fn-r 1) (nth coll-r 1)])])
;; conj/into: track the element type of a vector being grown.
(and (or (= cn "conj") (= cn "into")) (>= n 1))
(let [ares (mapv (fn [a] (infer a tenv)) args)
(let [ares (mapv (fn [a] (infer a tenv env)) args)
base (nth (nth ares 0) 0)
rest-ts (mapv (fn [r] (nth r 0)) (rest ares))
rt (cond
@ -462,40 +318,40 @@
(mk-vec (reduce join (velem base) rest-ts))
(and (= cn "into") (vec-type? base) (= 2 n) (vec-type? (nth rest-ts 0)))
(mk-vec (join (velem base) (velem (nth rest-ts 0))))
:else (call-ret-type fnode))]
:else (call-ret-type fnode env))]
[rt (assoc node :args (mapv (fn [r] (nth r 1)) ares))])
;; everything else: type args, collect the call (var callee), use the
;; declared/estimated return type. range produces a numeric vector.
:else
(let [fr (when (not iscall-var) (infer fnode tenv))
(let [fr (when (not iscall-var) (infer fnode tenv env))
fnode' (if iscall-var fnode (nth fr 1))
;; the callee's value type: a var's from vtype-box (a fn is
;; the callee's value type: a var's from vtypes (a fn is
;; :truthy, a def carries its inferred type), else the inferred
;; type of the callee expression
callee-t (if iscall-var (get @vtype-box (var-key fnode)) (nth fr 0))
ares (mapv (fn [a] (infer a tenv)) args)]
callee-t (if iscall-var (get (get env :vtypes) (var-key fnode)) (nth fr 0))
ares (mapv (fn [a] (infer a tenv env)) args)]
(when iscall-var
(swap! calls-box conj [(var-key fnode) (mapv (fn [r] (nth r 0)) ares)]))
(swap! (get env :calls) conj [(var-key fnode) (mapv (fn [r] (nth r 0)) ares)]))
;; success-type check at this call, reusing the arg types just
;; computed (jolt audit): core error domains always, user-fn domains
;; in strict mode. The arg subtrees are inferred exactly once.
(when @checking?
(when (get env :checking?)
(let [ats (mapv (fn [r] (nth r 0)) ares) pos (get node :pos)]
(when cn (check-invoke cn args ats pos))
(when cn (check-invoke cn args ats pos env))
;; calling a provably non-function
(when (not-callable? callee-t)
(swap! diag-box conj
(swap! (get env :diags) conj
{:op :call :type (type-name callee-t) :pos pos
:msg (str "cannot call " (type-name callee-t) " as a function")}))
(when (and @strict-box iscall-var)
(let [k (var-key fnode) usig (get @user-sig-box k)]
(when usig (check-user-call k usig ats pos))))))
(when (and (get env :strict?) iscall-var)
(let [k (var-key fnode) usig (get @(get env :user-sigs) k)]
(when usig (check-user-call k usig ats pos env))))))
;; devirtualization: a protocol-method call whose receiver
;; (arg 0) is a known record type resolves to a direct method call.
;; Annotate the node with [type-tag proto method]; the back end looks
;; up the impl at emit time and calls it directly, skipping the
;; registry dispatch (~19x cheaper than protocol-dispatch).
(let [pm (and iscall-var (get @protocol-methods-box (var-key fnode)))
(let [pm (and iscall-var (get (get env :protocol-methods) (var-key fnode)))
rtype (when (and pm (pos? n)) (get (nth (nth ares 0) 0) :type))
base (assoc node :fn fnode' :args (mapv (fn [r] (nth r 1)) ares))]
[(cond
@ -503,27 +359,27 @@
;; element-returning fn over a typed vector -> the element type
(and cn (contains? elem-fns cn) (> n 0))
(let [a0 (nth (nth ares 0) 0)] (if (vec-type? a0) (velem a0) :any))
:else (call-ret-type fnode))
:else (call-ret-type fnode env))
(if rtype
(assoc base :devirt-type rtype :devirt-proto (nth pm 0) :devirt-method (nth pm 1))
base)]))))
(= op :let)
(let [res (reduce (fn [acc b]
(let [te (nth acc 0) binds (nth acc 1)
ir (infer (nth b 1) te)]
ir (infer (nth b 1) te env)]
[(assoc te (nth b 0) (nth ir 0)) (conj binds [(nth b 0) (nth ir 1)])]))
[tenv []] (get node :bindings))
br (infer (get node :body) (nth res 0))]
br (infer (get node :body) (nth res 0) env)]
[(nth br 0) (assoc node :bindings (nth res 1) :body (nth br 1))])
(= op :loop)
;; conservative + sound: loop bindings join across recur, which we don't
;; track here, so they stay :any. Still descend to annotate any
;; known-type lookups inside the body.
[:any (assoc node
:bindings (mapv (fn [b] [(nth b 0) (nth (infer (nth b 1) tenv) 1)]) (get node :bindings))
:body (nth (infer (get node :body) tenv) 1))]
:bindings (mapv (fn [b] [(nth b 0) (nth (infer (nth b 1) tenv env) 1)]) (get node :bindings))
:body (nth (infer (get node :body) tenv env) 1))]
(= op :recur)
[:any (assoc node :args (mapv (fn [a] (nth (infer a tenv) 1)) (get node :args)))]
[:any (assoc node :args (mapv (fn [a] (nth (infer a tenv env) 1)) (get node :args)))]
(= op :fn)
;; a closure inherits the enclosing tenv so CAPTURED locals keep their
;; types (e.g. a reduce closure that calls (f captured-struct ...)). Its own
@ -534,27 +390,28 @@
;; read its fields without the runtime tag guard.
[:any (assoc node :arities
(mapv (fn [a]
(let [phm (reduce (fn [m pr] (assoc m (nth pr 0) (nth pr 1)))
(let [shapes (get env :record-shapes)
phm (reduce (fn [m pr] (assoc m (nth pr 0) (nth pr 1)))
{} (get a :phints))
pe (reduce (fn [e p]
(assoc e p
(let [ent (get @record-shapes-box (get phm p))]
(if ent (record-type-from-entry ent type-depth) :any))))
(let [ent (get shapes (get phm p))]
(if ent (record-type-from-entry ent type-depth shapes) :any))))
tenv (get a :params))
pe (if (get a :rest) (assoc pe (get a :rest) :any) pe)]
(assoc a :body (nth (infer (get a :body) pe) 1))))
(assoc a :body (nth (infer (get a :body) pe env) 1))))
(get node :arities)))]
(= op :def)
(do (when @checking? (register-user-fn! node))
[:any (assoc node :init (nth (infer (get node :init) tenv) 1))])
(do (when (get env :checking?) (register-user-fn! node env))
[:any (assoc node :init (nth (infer (get node :init) tenv env) 1))])
(= op :try)
[:any (assoc node
:body (nth (infer (get node :body) tenv) 1)
:catch-body (when (get node :catch-body) (nth (infer (get node :catch-body) tenv) 1))
:finally (when (get node :finally) (nth (infer (get node :finally) tenv) 1)))]
:body (nth (infer (get node :body) tenv env) 1)
:catch-body (when (get node :catch-body) (nth (infer (get node :catch-body) tenv env) 1))
:finally (when (get node :finally) (nth (infer (get node :finally) tenv env) 1)))]
:else [:any node])))
(defn- infer-top [node] (nth (infer node {}) 1))
(defn- infer-top [node env] (nth (infer node {} env) 1))
;; ---------------------------------------------------------------------------
;; Success-type checking (RFC 0006). Reuse the inference above as a loose type
@ -614,15 +471,16 @@
(defn- check-invoke
"If node is a core-op call whose argument type is provably in the error domain,
conj a diagnostic. arg-types is the vector of inferred argument types; pos is
the call form's source offset, carried into each diagnostic."
[cn args arg-types pos]
conj a diagnostic into env's diags cell. arg-types is the vector of inferred
argument types; pos is the call form's source offset, carried into each
diagnostic."
[cn args arg-types pos env]
(cond
(contains? num-ops cn)
(reduce (fn [_ i]
(let [t (nth arg-types i)]
(when (not-number? t)
(swap! diag-box conj
(swap! (get env :diags) conj
{:op cn :argpos i :type (type-name t) :pos pos
:msg (str "`" cn "` requires a number, but argument "
(inc i) " is " (type-name t))})))
@ -631,7 +489,7 @@
(and (contains? seq-ops cn) (> (count args) 0))
(let [t (nth arg-types 0)]
(when (not-seqable? t)
(swap! diag-box conj
(swap! (get env :diags) conj
{:op cn :argpos 0 :type (type-name t) :pos pos
:msg (str "`" cn "` requires "
(if (= cn "count") "a countable collection" "a seqable")
@ -645,22 +503,20 @@
(reduce (fn [e p] (assoc e p :any)) {} params))
(defn- isolated-diag-count
"Count of diagnostics typing body under tenv produces, with the shared
diag-box saved and restored so this probe never leaks into the real report.
Runs the same checking inference as check-form (checking? is already on)."
[body tenv]
(let [saved @diag-box]
(reset! diag-box [])
(infer body tenv)
(let [n (count @diag-box)]
(reset! diag-box saved)
n)))
"Count of diagnostics typing body under tenv produces. Runs under a SUB-ENV
with its own diags cell, so this probe never leaks into the real report (the
shared calls/escapes/guard cells are intentionally still threaded they are
not read here). Runs the same checking inference as check-form."
[body tenv env]
(let [sub (assoc env :diags (atom []))]
(infer body tenv sub)
(count @(get sub :diags))))
(defn- register-user-fn!
"Record a (def name (fn [params] body)) single fixed arity, not redefinable
for later user-fn call checking. Redefinable/dynamic and multi/variadic fns are
skipped (their body is not a stable requirement)."
[node]
[node env]
(let [init (get node :init)
m (get node :meta)
redefable (and m (or (get m :redef) (get m :dynamic)))]
@ -669,7 +525,7 @@
(when (= 1 (count arities))
(let [ar (first arities)]
(when (not (get ar :rest))
(swap! user-sig-box assoc
(swap! (get env :user-sigs) assoc
(str (get node :ns) "/" (get node :name))
{:name (get node :name)
:params (get ar :params) :body (get ar :body)}))))))))
@ -682,55 +538,69 @@
its arg type (others :any); a diagnostic the all-:any body did not already
have means the argument alone is provably wrong. Monotonic binding a
concrete type can only ADD error-domain hits so no false positive.
Cycle-guarded so mutually recursive fns terminate."
[key sig arg-types pos]
(when (not (contains? @checking-box key))
(let [prev @checking-box]
(reset! checking-box (conj prev key))
(let [params (:params sig)
body (:body sig)
npar (count params)
nargs (count arg-types)]
(if (not= npar nargs)
;; arity is provably wrong regardless of types — report and stop (the
;; per-arg type re-check would bind params positionally, meaningless
;; under a mismatch)
(swap! diag-box conj
{:op :user-call :type :arity :pos pos
:msg (str "wrong number of args (" nargs ") passed to `"
(:name sig) "` (expected " npar ")")})
(let [base (isolated-diag-count body (all-any-env params))]
(reduce
(fn [_ i]
(let [at (nth arg-types i)]
(when (and (not= at :any) (not= at :truthy))
(let [pe (assoc (all-any-env params) (nth params i) at)]
(when (> (isolated-diag-count body pe) base)
(swap! diag-box conj
{:op :user-call :argpos i :type (type-name at) :pos pos
:msg (str "argument " (inc i) " to `" (:name sig)
"` is " (type-name at)
", which its body provably rejects")})))))
nil)
nil (range npar)))))
(reset! checking-box prev))))
Cycle-guarded (env's checking-set) so mutually recursive fns terminate."
[key sig arg-types pos env]
(let [cset (get env :checking-set)]
(when (not (contains? @cset key))
(let [prev @cset]
(reset! cset (conj prev key))
(let [params (:params sig)
body (:body sig)
npar (count params)
nargs (count arg-types)
memo (get env :diag-memo)]
(if (not= npar nargs)
;; arity is provably wrong regardless of types — report and stop (the
;; per-arg type re-check would bind params positionally, meaningless
;; under a mismatch)
(swap! (get env :diags) conj
{:op :user-call :type :arity :pos pos
:msg (str "wrong number of args (" nargs ") passed to `"
(:name sig) "` (expected " npar ")")})
;; all-any-env is built once (was rebuilt per param), and each probe is
;; memoized by [key i argtype] so the same fn re-checked across call
;; sites in this form re-infers its body at most once per (param, type).
(let [base-env (all-any-env params)
base (let [bk [:base key]]
(if (contains? @memo bk)
(get @memo bk)
(let [b (isolated-diag-count body base-env env)]
(swap! memo assoc bk b) b)))]
(reduce
(fn [_ i]
(let [at (nth arg-types i)]
(when (and (not= at :any) (not= at :truthy))
(let [mk [:arg key i at]
rejects (if (contains? @memo mk)
(get @memo mk)
(let [r (> (isolated-diag-count body (assoc base-env (nth params i) at) env) base)]
(swap! memo assoc mk r) r))]
(when rejects
(swap! (get env :diags) conj
{:op :user-call :argpos i :type (type-name at) :pos pos
:msg (str "argument " (inc i) " to `" (:name sig)
"` is " (type-name at)
", which its body provably rejects")})))))
nil)
nil (range npar)))))
(reset! cset prev)))))
;; --- Inter-procedural driver API consumed by the back end -------------------
(defn set-rtenv!
"Install the current return-type estimates (a map \"ns/name\" -> type) used to
type call results during the fixpoint."
[m] (reset! rtenv-box m))
[m] (swap! config-box assoc :rtenv (or m {})))
;; install record-ctor shapes ("ns/->Name" -> [field-kw ...]) and the
;; map-shaping flag (opt-in JOLT_SHAPE), both read by infer.
(defn set-record-shapes! [m] (reset! record-shapes-box (or m {})))
(defn set-protocol-methods! [m] (reset! protocol-methods-box (or m {})))
(defn set-map-shapes! [b] (reset! map-shapes-box (boolean b)))
(defn set-record-shapes! [m] (swap! config-box assoc :record-shapes (or m {})))
(defn set-protocol-methods! [m] (swap! config-box assoc :protocol-methods (or m {})))
(defn set-map-shapes! [b] (swap! config-box assoc :map-shapes? (boolean b)))
(defn set-vtypes!
"Install var VALUE types (a map \"ns/name\" -> type): fn vars are :truthy
(non-nil), def vars carry their inferred init type."
[m] (reset! vtype-box m))
[m] (swap! config-box assoc :vtypes (or m {})))
(defn join-types
"Public structural join (lub), used by the orchestrator's fixpoint so param/
@ -752,17 +622,12 @@
def must precede its call the same ordering RFC 0005 already assumes."
([node] (check-form node false))
([node strict?]
(reset! strict-box (if strict? true false))
(reset! checking-box #{})
(reset! diag-box [])
;; the check IS the inference: one walk that types and emits diagnostics
;; (jolt audit). checking? gates emission so the optimization fixpoint, which
;; also calls infer, stays silent.
(reset! checking? true)
(infer node {})
(reset! checking? false)
(reset! strict-box false)
(vec @diag-box)))
;; the check IS the inference: one walk that types and emits diagnostics into
;; this run's env. The optimization fixpoint runs with checking? false so it
;; stays silent.
(let [env (mk-env true strict?)]
(infer node {} env)
(vec @(get env :diags)))))
(defn infer-body
"Type `body` under tenv (local-name -> type). Returns [ret-type node' calls],
@ -770,9 +635,9 @@
propagating into callee param types). Also accumulates escapes (read with
collected-escapes after a full sweep)."
[body tenv]
(reset! calls-box [])
(let [r (infer body tenv)]
[(nth r 0) (nth r 1) @calls-box]))
(let [env (mk-env false false)
r (infer body tenv env)]
[(nth r 0) (nth r 1) @(get env :calls)]))
(defn reinfer-def
"Re-run inference on a stashed :def's fn arity bodies with param types seeded
@ -780,7 +645,9 @@
end emits the result directly (no further passes), so the param-typed lookups
keep their specialization. Used by the inter-procedural recompile."
[def-node ptmap]
(let [fnode (get def-node :init)]
(let [fnode (get def-node :init)
env (mk-env false false)
shapes (get env :record-shapes)]
(if (= :fn (get fnode :op))
(assoc def-node :init
(assoc fnode :arities
@ -792,12 +659,12 @@
;; as precise), so this only fills the gaps.
(let [pt (reduce (fn [m pr]
(let [nm (nth pr 0)
e (get @record-shapes-box (nth pr 1))]
e (get shapes (nth pr 1))]
(if (and e (not (contains? m nm)))
(assoc m nm (record-type-from-entry e type-depth))
(assoc m nm (record-type-from-entry e type-depth shapes))
m)))
ptmap (get a :phints))]
(assoc a :body (nth (infer (get a :body) pt) 1))))
(assoc a :body (nth (infer (get a :body) pt env) 1))))
(get fnode :arities))))
def-node)))
@ -812,11 +679,12 @@
fixpoint, so a field read off it (e.g. (:origin ^Ray r)) never tells a shared
callee its arg is a Vec3."
[params phints]
(let [m (reduce (fn [acc pr] (assoc acc (nth pr 0) (nth pr 1))) {} phints)]
(let [shapes (get @config-box :record-shapes)
m (reduce (fn [acc pr] (assoc acc (nth pr 0) (nth pr 1))) {} phints)]
(mapv (fn [nm]
(let [ck (get m nm)
e (and ck (get @record-shapes-box ck))]
(when e (record-type-from-entry e type-depth))))
e (and ck (get shapes ck))]
(when e (record-type-from-entry e type-depth shapes))))
params)))
;; Piggyback checking (jolt audit). In direct-link mode infer-top already runs
@ -826,27 +694,22 @@
;; run-passes and reads take-diags! after. It checks the POST-optimization IR,
;; which matches what the optimized program actually evaluates (scalar-replace
;; only drops provably-pure code, an accepted opt-mode divergence).
(def ^:private check-mode-box (atom {:on false :strict false}))
(defn set-check-mode!
"Enable/disable checking during the next run-passes inference (direct-link)."
[on strict?] (reset! check-mode-box {:on (if on true false) :strict (if strict? true false)}))
(defn take-diags!
"Diagnostics accumulated by the last checking run-passes; clears the buffer."
[] (let [d (vec @diag-box)] (reset! diag-box []) d))
[] (let [d @last-diags-box] (reset! last-diags-box []) d))
(defn run-inference
"Type-infer the optimized node (the inference walk specializes struct-safe
lookups). When check mode is on (set-check-mode!), the same walk also emits
success-type diagnostics into the buffer take-diags! drains afterward. Pulled
success-type diagnostics, stashed for take-diags! to drain afterward. Pulled
out of run-passes so the checking state stays private to this namespace."
[opt]
(if (get @check-mode-box :on)
(do (reset! diag-box [])
(reset! checking-box #{})
(reset! strict-box (get @check-mode-box :strict))
(reset! checking? true)
(let [r (infer-top opt)]
(reset! checking? false)
(reset! strict-box false)
r))
(infer-top opt)))
(let [env (mk-env true (get @check-mode-box :strict))
r (infer-top opt env)]
(reset! last-diags-box @(get env :diags))
r)
(infer-top opt (mk-env false false))))

View file

@ -0,0 +1,156 @@
(ns jolt.passes.types.lattice
"Structural type lattice for jolt.passes.types: scalar/struct/vec/set/union
types, join, depth-cap, shape, and the numeric/vector return-fn name sets. Pure
(no inference state) the inference + checker in jolt.passes.types build on it.")
;; ---------------------------------------------------------------------------
;; Collection-type inference, intra-procedural. A forward,
;; soft-typing-style pass (simplified HM: monovariant, never-fails, lattice top
;; = :any) that types expressions from literals/arithmetic and flows the type
;; through let bindings and if-joins. Where a keyword-lookup subject is PROVEN a
;; plain struct map it sets :hint :struct (the same channel a manual hint uses,
;; so the back end drops the guard); where the type is :any it leaves the
;; dynamic guard in place. Sound by construction: a concrete type is assigned
;; only when proven, so a wrong bare get is impossible.
;;
;; Recursive STRUCTURAL types (RFC 0005). A type mirrors the data tree:
;; compound: {:struct {field -> T}} (raw-get-safe map, field types)
;; {:vec T} (vector of T)
;; {:set T} (set of T)
;; scalar: :num :str :kw :truthy (all provably non-nil/non-false)
;; :phm (persistent hash map; NOT raw-get-safe)
;; :any (top), nil (bottom, identity for join).
;; Compound types are small jolt maps, so they compare by value on both the
;; Clojure and the host (orchestrator) side. struct/vec/set use distinct keys so
;; a type is recognised by which key it carries.
;; (get t :KEY) is nil for a keyword type and the child for a compound, so a
;; compound is detected by some? — no map?/contains? needed.
(defn velem [t] (get t :vec))
(defn selem [t] (get t :set))
(defn sfields [t] (get t :struct))
(defn vec-type? [t] (some? (velem t)))
(defn set-type? [t] (some? (selem t)))
(defn struct-type? [t] (some? (sfields t)))
(defn mk-vec [t] {:vec (if t t :any)})
(defn mk-set [t] {:set (if t t :any)})
(defn mk-struct [fs] {:struct fs})
;; Bounded union types (RFC 0006). A union {:union #{T...}} records
;; that a value is provably one of a small, fixed set of SCALAR types — what
;; differing if-branches used to collapse to :any. It exists so the success
;; checker can reject a use where EVERY member is in the op's error domain
;; ((inc (if c "a" :k))) while still accepting one where any member is valid
;; ((inc (if c 1 "x"))). Scalars only, capped cardinality: the member space is
;; the five scalar tags, so the lattice stays finite and the inter-procedural
;; fixpoint terminates. A union is opaque to every STRUCTURAL predicate
;; (struct-type?/vec-type?/set-type? key on :struct/:vec/:set, which a union
;; lacks), so specialization treats it exactly like :any — codegen is
;; unchanged; only the checker reads inside it.
(def union-cap 4)
(defn scalar-t? [t] (or (= t :num) (= t :str) (= t :kw) (= t :truthy) (= t :phm)))
(defn union-type? [t] (some? (get t :union)))
(defn umembers [t] (get t :union))
(defn union-of
"Normalize a seq of member types into a lattice value: flatten nested unions,
keep only scalars (any non-scalar member collapses the whole thing to :any,
the conservative top), then return the lone member if one, {:union #{...}}
for 2..cap distinct scalars, or :any past the cap."
[ts]
(let [flat (reduce (fn [acc t]
(if (union-type? t)
(reduce conj acc (umembers t))
(conj acc t)))
#{} ts)]
(cond
(not (every? scalar-t? flat)) :any
(= 0 (count flat)) :any
(= 1 (count flat)) (first flat)
(> (count flat) union-cap) :any
:else {:union flat})))
(declare join-t)
(defn merge-fields
"Per-field join of two field maps (a key in only one side joins with :any)."
[fa fb]
(let [m1 (reduce (fn [m k] (assoc m k (join-t (get fa k :any) (get fb k :any)))) {} (keys fa))]
(reduce (fn [m k] (if (get m k) m (assoc m k (join-t (get fa k :any) (get fb k :any))))) m1 (keys fb))))
(defn join-t [a b]
(cond
(= a b) a
(nil? a) b
(nil? b) a
(and (struct-type? a) (struct-type? b))
(let [merged (mk-struct (merge-fields (sfields a) (sfields b)))]
;; joining two values of the SAME complete shape preserves it — the
;; merged struct has the same key set. Different shapes
;; (or an incomplete side) drop it, as the layout is no longer proven.
(if (and (get a :shape) (= (get a :shape) (get b :shape)))
(assoc merged :shape (get a :shape))
merged))
(and (vec-type? a) (vec-type? b)) (mk-vec (join-t (velem a) (velem b)))
(and (set-type? a) (set-type? b)) (mk-set (join-t (selem a) (selem b)))
;; differing kinds: form a scalar union when both sides reduce to scalars
;; (or scalar unions); anything compound on either side stays :any
:else (let [ma (cond (union-type? a) (umembers a) (scalar-t? a) #{a} :else nil)
mb (cond (union-type? b) (umembers b) (scalar-t? b) #{b} :else nil)]
(if (and ma mb) (union-of (reduce conj ma mb)) :any))))
(defn join [a b] (join-t a b))
;; depth cap (RFC 0005): truncate a type below depth d to :any, so recursive data
;; can't make an infinite type and the inter-procedural fixpoint stays finite.
(def type-depth 4)
(defn cap [t d]
(cond
(<= d 0) (if (or (struct-type? t) (vec-type? t) (set-type? t)) :any t)
(struct-type? t)
;; capping truncates VALUES below depth d, but the KEY SET is unchanged, so
;; a complete :shape survives — keep it so nested/container field reads can
;; still bare-index. cap recurses into fields, so a nested
;; shaped value (a vec3 inside a hit-info) keeps its own :shape too.
(let [capped (mk-struct (reduce (fn [m k] (assoc m k (cap (get (sfields t) k) (dec d))))
{} (keys (sfields t))))
;; the record :type tag (and :shape) are independent of field-value
;; depth, so they survive truncation — a record read from a deep
;; container keeps its identity, so devirtualization, record? folding,
;; and the record fast path still fire on it.
capped (if (get t :shape) (assoc capped :shape (get t :shape)) capped)
capped (if (get t :type) (assoc capped :type (get t :type)) capped)]
capped)
(vec-type? t) (mk-vec (cap (velem t) (dec d)))
(set-type? t) (mk-set (cap (selem t) (dec d)))
:else t))
;; raw-get-safe (a struct / record): a struct type. The field type of key
;; k, if known, else :any.
(defn struct-safe? [t] (struct-type? t))
(defn field-type [t k] (if (struct-type? t) (get (sfields t) k :any) :any))
;; Shape (hidden class). A struct type built from a map LITERAL carries
;; its complete layout — :shape, the canonical (str-sorted) key vector. The back
;; end represents such a map as a shape tuple and reads a field by bare index.
;; A struct type from a JOIN or from field-access inference has no :shape
;; (incomplete: the full key set isn't proven), so it keeps the dynamic path —
;; never a bare index. No shape is hardcoded; any constant key set is one.
(defn shape-order
"Canonical key order for a shape: keys sorted by their string form, so two
literals with the same keys in any order intern to the same shape."
[ks] (vec (sort (fn [a b] (compare (str a) (str b))) ks)))
(defn type-shape [t] (get t :shape))
;; tag a node (any expression, not just a :local) so the back end can specialize
;; a lookup whose SUBJECT is that node — this is what makes nested access work:
;; (:direction ray) is tagged struct, so (:r (:direction ray)) drops its guard.
;; tag a lookup subject as a struct, carrying the complete shape when known
;; (so the back end bare-indexes).
(defn mark-struct [node t]
(let [n (assoc node :hint :struct)]
(if (get t :shape) (assoc n :shape (get t :shape)) n)))
;; a value provably neither nil nor false — the back end only builds a struct
;; (vs a phm) when every value is non-nil/non-false, so a map literal is a struct
;; only when all its values have such a type. Collections are non-nil.
(defn truthy-type? [t]
(or (= t :num) (= t :str) (= t :kw) (= t :truthy) (= t :phm)
(struct-type? t) (vec-type? t) (set-type? t)))
;; core fns whose result is a number (so it is non-nil/non-false and, for the
;; success-type checker, provably numeric).
(def num-ret-fns
#{"+" "-" "*" "/" "inc" "dec" "mod" "rem" "quot" "min" "max" "abs"
"bit-and" "bit-or" "bit-xor" "count"})
(def vector-ret-fns #{"vec" "vector" "mapv" "filterv" "subvec"})

View file

@ -1,463 +0,0 @@
(ns jolt.reader
"Reads Clojure source text into reader forms.
The lexing and parsing is portable Clojure; form construction and
string->number parsing delegate to the jolt.host contract (form-make-symbol/
char, form-char-from-name, form-scan-number). A Clojure source file can't write
a {:jolt/type :symbol} literal it would parse as a tagged reader form and
the concrete form representation belongs to the host. The analyzer uses the same
split. Once cross-compiled this runs on Chez to drive compile-from-source.
Positions are character indices; for ASCII source they coincide with byte
indices, and form values are identical either way the parity gate compares
values, not positions."
(:require [clojure.string :as str]
[jolt.host :refer [form-make-symbol form-make-char form-char-from-name
form-scan-number form-make-list form-make-vector
form-make-map form-sym-merge-meta form-make-set
form-make-tagged form-gensym-name
form-sym? form-sym-name form-sym-ns form-char?
form-list? form-vec? form-set? form-map?
form-elements form-vec-items form-set-items
form-map-pairs]]))
;; Source access by CHARACTER codepoint
;; (identical to byte access for ASCII). cp = codepoint at i; len = character count.
(defn- cp [s i] (int (nth s i)))
(defn- len [s] (count s))
(defn- whitespace? [c] (or (= c 32) (= c 9) (= c 10) (= c 13) (= c 44))) ; space tab nl cr ,
(defn- digit? [c] (and (>= c 48) (<= c 57)))
(defn- hex-digit? [c]
(or (digit? c) (and (>= c 65) (<= c 70)) (and (>= c 97) (<= c 102))))
(defn- symbol-start? [c]
(or (and (>= c 65) (<= c 90)) (and (>= c 97) (<= c 122))
(= c 42) (= c 43) (= c 33) (= c 95) (= c 45) (= c 63) (= c 46)
(= c 60) (= c 62) (= c 61) (= c 38) (= c 124) (= c 36) (= c 37) (= c 47)))
(defn- symbol-char? [c]
(or (symbol-start? c) (digit? c) (= c 35) (= c 39) (= c 58))) ; + # ' :
(defn- skip-whitespace [s pos]
(if (and (< pos (len s)) (whitespace? (cp s pos)))
(recur s (inc pos))
pos))
(defn- read-until-newline [s pos]
(if (or (>= pos (len s)) (= (cp s pos) 10)) pos (recur s (inc pos))))
;; --- symbols -----------------------------------------------------------------
(defn- read-symbol-name [s pos end]
(if (and (< end (len s)) (symbol-char? (cp s end))) (recur s pos (inc end)) end))
(defn- read-symbol* [s pos]
(let [end (read-symbol-name s pos pos)]
(when (= end pos)
(throw (ex-info (str "Unrecognized character: " (char (cp s pos))) {})))
(let [nm (subs s pos end)]
(cond
(= nm "nil") [nil end]
(= nm "true") [true end]
(= nm "false") [false end]
:else [(form-make-symbol nm) end]))))
;; --- keywords ----------------------------------------------------------------
(defn- read-keyword-name [s pos end]
(if (and (< end (len s)) (symbol-char? (cp s end))) (recur s pos (inc end)) end))
(defn- read-keyword* [s pos]
;; pos is at the first colon; ::foo is treated as :foo (no auto-resolution).
(let [start (if (and (< (inc pos) (len s)) (= (cp s (inc pos)) 58)) (+ pos 2) (inc pos))
end (read-keyword-name s start start)]
[(keyword (subs s start end)) end]))
;; --- strings -----------------------------------------------------------------
(defn- escape-char [c]
(cond (= c 110) "\n" (= c 116) "\t" (= c 114) "\r" (= c 92) "\\" (= c 34) "\""
:else (str (char c))))
(defn- read-string* [s pos]
;; pos at opening double-quote
(loop [p (inc pos) acc []]
(when (>= p (len s)) (throw (ex-info "Unterminated string" {})))
(let [c (cp s p)]
(cond
(= c 92) (let [np (inc p)]
(when (>= np (len s)) (throw (ex-info "Unterminated escape" {})))
(recur (+ p 2) (conj acc (escape-char (cp s np)))))
(= c 34) [(apply str acc) (inc p)]
:else (recur (inc p) (conj acc (str (char c))))))))
;; --- numbers -----------------------------------------------------------------
(defn- read-digits [s pos end]
(if (and (< end (len s)) (digit? (cp s end))) (recur s pos (inc end)) end))
(defn- read-hex-digits [s pos end]
(if (and (< end (len s)) (hex-digit? (cp s end))) (recur s pos (inc end)) end))
;; Value of an alphanumeric digit for radix parsing (0-9, a-z/A-Z = 10-35).
(defn- radix-digit-val [c]
(cond
(and (>= c 48) (<= c 57)) (- c 48)
(and (>= c 97) (<= c 122)) (+ 10 (- c 97))
(and (>= c 65) (<= c 90)) (+ 10 (- c 65))
:else nil))
(defn- read-alnum [s pos end]
(if (and (< end (len s)) (radix-digit-val (cp s end))) (recur s pos (inc end)) end))
(defn- read-exponent [s end]
;; if s[end] is e/E (optionally signed) followed by digits, return index past it
(if (and (< end (len s)) (let [c (cp s end)] (or (= c 101) (= c 69))))
(let [p (if (and (< (inc end) (len s)) (let [c (cp s (inc end))] (or (= c 43) (= c 45))))
(+ end 2) (inc end))
de (read-digits s p p)]
(if (> de p) de end))
end))
;; Jolt has no bignum/ratio: N (bigint) / M (bigdec) suffixes read as the plain
;; number, a ratio a/b reads as the double quotient, radixed ints by base.
(defn- read-number* [s pos]
(let [length (len s)
;; optional leading sign: - negates; + is a positive no-op (Clojure reads
;; +5 as 5). read-form only dispatches +digit/-digit, so the sign is real.
neg (and (< pos length) (= (cp s pos) 45))
plus (and (< pos length) (= (cp s pos) 43))
start (if (or neg plus) (inc pos) pos)
hex? (and (< (inc start) length) (= (cp s start) 48)
(let [c1 (cp s (inc start))] (or (= c1 120) (= c1 88))))] ; 0x / 0X
(if hex?
(let [hs (+ start 2) he (read-hex-digits s hs hs)]
(when (= he hs) (throw (ex-info "Expected hex digits" {})))
(let [he2 (if (and (< he length) (= (cp s he) 78)) (inc he) he) ; trailing N
val (form-scan-number (str "0x" (subs s hs he)))]
[(if neg (- val) val) he2]))
(let [iend (read-digits s start start)]
(when (= iend start) (throw (ex-info "Expected number" {})))
(cond
;; radix integer <base>r<digits>
(and (< iend length) (let [c (cp s iend)] (or (= c 114) (= c 82))))
(let [base (form-scan-number (subs s start iend))
ds (inc iend) de (read-alnum s ds ds)]
(when (= de ds) (throw (ex-info "Expected radix digits" {})))
(let [acc (reduce (fn [a i] (+ (* a base) (radix-digit-val (cp s i)))) 0 (range ds de))]
[(if neg (- acc) acc) de]))
;; ratio <int>/<int> (only when a digit follows the slash)
(and (< (inc iend) length) (= (cp s iend) 47) (digit? (cp s (inc iend))))
(let [ds (inc iend) de (read-digits s ds ds)
numr (form-scan-number (subs s start iend))
den (form-scan-number (subs s ds de))]
[(if neg (- (/ numr den)) (/ numr den)) de])
;; fractional and/or exponent, optional trailing N/M
:else
(let [frac-end (if (and (< iend length) (= (cp s iend) 46))
(let [fs (inc iend) fe (read-digits s fs fs)]
(when (= fe fs) (throw (ex-info "Expected digit after ." {})))
fe)
iend)
exp-end (read-exponent s frac-end)
val (form-scan-number (subs s start exp-end))
fin (if (and (< exp-end length) (let [c (cp s exp-end)] (or (= c 78) (= c 77))))
(inc exp-end) exp-end)]
[(if neg (- val) val) fin]))))))
;; --- characters --------------------------------------------------------------
(defn- read-char-name-end [s pos]
(if (and (< pos (len s)) (symbol-char? (cp s pos))) (recur s (inc pos)) pos))
(defn- read-char* [s pos]
(when (>= (inc pos) (len s)) (throw (ex-info "unexpected end of input after \\" {})))
(let [end (read-char-name-end s (inc pos))]
(if (= end (inc pos))
;; a non-symbol char right after \ is a one-character literal of itself
[(form-make-char (cp s (inc pos))) (+ pos 2)]
[(form-char-from-name (subs s (inc pos) end)) end])))
;; --- dispatcher --------------------------------------------------------------
;; read-form returns a CONTROL triple [kind payload pos]:
;; :form payload=the form a real datum
;; :skip payload=nil a comment (;) or #_ discard — produced nothing
;; :splice payload=items-vector #?@ — contributes 0+ items to the enclosing coll
;; Out-of-band control (rather than :jolt/skip / :jolt/splice sentinel
;; FORMS) keeps it collision-free and host-neutral — no tagged-struct to build or
;; recognize. Collection readers dispatch on kind; read-next-form skips :skip.
(declare read-form)
(defn- number-start? [s pos c]
(or (digit? c)
(and (= c 45) (< (inc pos) (len s)) (digit? (cp s (inc pos))))
(and (= c 43) (< (inc pos) (len s)) (digit? (cp s (inc pos))))))
;; Read items until `close`, dispatching control kinds. Returns [items-vec end].
(defn- read-delimited [s start-pos close errmsg]
(loop [pos start-pos items []]
(let [pos (skip-whitespace s pos)]
(when (>= pos (len s)) (throw (ex-info errmsg {})))
(if (= (cp s pos) close)
[items (inc pos)]
(let [[kind payload np] (read-form s pos)]
(case kind
:skip (recur np items)
:splice (recur np (into items payload))
:form (recur np (conj items payload))))))))
(defn- read-list* [s pos]
(let [[items end] (read-delimited s (inc pos) 41 "Unterminated list")] ; )
[:form (form-make-list items) end]))
(defn- read-vector* [s pos]
(let [[items end] (read-delimited s (inc pos) 93 "Unterminated vector")] ; ]
[:form (form-make-vector items) end]))
;; Map: pair up keys and values, skipping comments/#_ in either slot while keeping
;; the pending key (dropping both desyncs the pairing). A key/value is always a
;; single :form (or :skip) — splice in a map slot is not supported.
(defn- read-map* [s pos]
(loop [pos (inc pos) kvs []]
(let [pos (skip-whitespace s pos)]
(when (>= pos (len s)) (throw (ex-info "Unterminated map" {})))
(if (= (cp s pos) 125) ; }
[:form (form-make-map kvs) (inc pos)]
(let [[kk kp knp] (read-form s pos)]
(if (= kk :skip)
(recur knp kvs)
;; key in hand; read the value slot, skipping trivia but keeping the key
(let [[v vnp]
(loop [vp (skip-whitespace s knp)]
(when (>= vp (len s)) (throw (ex-info "Unterminated map" {})))
(let [[vk vp2 vnp2] (read-form s vp)]
(if (= vk :skip) (recur (skip-whitespace s vnp2)) [vp2 vnp2])))]
(recur vnp (conj (conj kvs kp) v)))))))))
;; Read the next REAL form (skip :skip), returning [form pos]. Used wherever a
;; single datum is needed (quote/meta/top level).
(defn- read-next-form [s pos]
(let [[kind payload np] (read-form s pos)]
(case kind
:skip (recur s np)
:form [payload np]
:splice (throw (ex-info "splice (#?@) not inside a collection" {})))))
;; syntax-quote of a self-evaluating literal collapses to the literal at read time
;; (so nested backticks over literals are inert). NOT symbols (they qualify) or
;; collections (they template).
(defn- self-evaluating-literal? [form]
(or (nil? form) (true? form) (false? form) (number? form)
(string? form) (keyword? form) (form-char? form)))
(defn- read-quote* [s newpos token-sym]
(let [[form finalpos] (read-next-form s newpos)]
(if (and (= "syntax-quote" (form-sym-name token-sym)) (self-evaluating-literal? form))
[:form form finalpos]
[:form (form-make-list [token-sym form]) finalpos])))
;; Normalize a metadata reader form: keyword -> {kw true}; symbol/string -> {:tag …}
;; (a symbol tag keeps its ns qualifier); else nil (a map-literal meta).
(defn- meta-form->map [meta-form]
(cond
(keyword? meta-form) {meta-form true}
(form-sym? meta-form) {:tag (if (form-sym-ns meta-form)
(str (form-sym-ns meta-form) "/" (form-sym-name meta-form))
(form-sym-name meta-form))}
(string? meta-form) {:tag meta-form}
:else nil))
(defn- read-meta* [s pos]
;; pos at ^
(let [[meta-form np] (read-next-form s (inc pos))
[form np2] (read-next-form s np)
m (meta-form->map meta-form)]
(if (and m (form-sym? form))
;; attach to the symbol itself (^Type x / ^:dynamic) — stays a bare symbol
[:form (form-sym-merge-meta form m) np2]
;; non-symbol target -> a runtime with-meta form (normalized map, or the
;; raw map-literal meta when m is nil)
[:form (form-make-list [(form-make-symbol "with-meta") form (if m m meta-form)]) np2])))
;; --- dispatch (#) ------------------------------------------------------------
;; Reader-conditional feature set (spec 02-reader). jolt's portable default; the
;; JOLT_FEATURES env override is a host concern wired later. :default always honored.
(def reader-features (atom #{:jolt :default}))
(defn set-reader-features! [features] (reset! reader-features (conj (set features) :default)))
(defn- read-set* [s pos]
;; pos at #, next char {
(let [[items end] (read-delimited s (+ pos 2) 125 "Unterminated set")] ; }
[:form (form-make-set items) end]))
(defn- read-var-quote* [s pos]
;; pos at #, next char '
(let [[form np] (read-next-form s (+ pos 2))]
[:form (form-make-list [(form-make-symbol "var") form]) np]))
(defn- read-regex* [s pos]
;; pos at #, next char "; read raw to the unescaped closing " (backslashes kept)
(loop [i (+ pos 2)]
(when (>= i (len s)) (throw (ex-info "Unterminated regex literal" {})))
(let [c (cp s i)]
(cond
(= c 92) (recur (+ i 2)) ; backslash escapes next char
(= c 34) [:form (form-make-tagged :regex (subs s (+ pos 2) i)) (inc i)]
:else (recur (inc i))))))
;; #?(…) / #?@(…): pick the first clause whose feature key is active (clause order,
;; like Clojure). #? -> :skip when the result is nil (e.g. a :cljs branch); #?@ ->
;; :splice the resolved items into the enclosing collection.
(defn- rc-resolve [clauses]
;; clauses: a jolt vector of [feature-kw form feature-kw form ...]
(loop [i 0]
(if (>= i (count clauses))
[false nil]
(if (contains? @reader-features (nth clauses i))
[true (nth clauses (inc i))]
(recur (+ i 2))))))
(defn- read-reader-conditional* [s pos]
;; pos at #, next char ? (optionally ?@)
(let [splice? (and (< (+ pos 2) (len s)) (= (cp s (+ pos 2)) 64)) ; @
form-start (if splice? (+ pos 3) (+ pos 2))
[form np] (read-next-form s form-start)]
(if (form-list? form)
(let [clauses (form-elements form)
[matched result] (rc-resolve clauses)]
(if splice?
(let [items (cond (not matched) []
(form-list? result) (vec (form-elements result))
(form-vec? result) (vec (form-vec-items result))
:else [result])]
[:splice items np])
(if (or (not matched) (nil? result)) [:skip nil np] [:form result np])))
(throw (ex-info "reader conditional body must be a list" {})))))
;; Symbolic values ##Inf ##-Inf ##NaN.
(defn- read-symbolic* [s pos]
(let [end (read-symbol-name s (+ pos 2) (+ pos 2))
nm (subs s (+ pos 2) end)]
(cond
(= nm "Inf") [:form ##Inf end]
(= nm "-Inf") [:form ##-Inf end]
(= nm "NaN") [:form ##NaN end]
:else (throw (ex-info (str "Invalid symbolic value: ##" nm) {})))))
(defn- read-tagged* [s pos]
;; unknown dispatch -> a tagged literal (#inst, #uuid, #foo). The tag includes
;; the leading # (read-symbol-name starts at #).
(let [end (read-symbol-name s pos pos)
tag (subs s pos end)
[form np] (read-next-form s end)]
[:form (form-make-tagged (keyword tag) form) np]))
(declare read-anon-fn*)
(defn- read-dispatch* [s pos]
;; pos at #
(when (>= (inc pos) (len s)) (throw (ex-info "Unexpected end after #" {})))
(let [c (cp s (inc pos))]
(cond
(= c 123) (read-set* s pos) ; #{
(= c 40) (read-anon-fn* s pos) ; #(
(= c 63) (read-reader-conditional* s pos) ; #?
(= c 95) (let [[_ _ np] (read-form s (+ pos 2))] [:skip nil np]) ; #_ discard
(= c 39) (read-var-quote* s pos) ; #'
(= c 94) (read-meta* s (inc pos)) ; #^ (deprecated, = ^)
(= c 34) (read-regex* s pos) ; #"
(= c 35) (read-symbolic* s pos) ; ##
:else (read-tagged* s pos))))
;; #(...) anonymous fn. Positional %-arg index: % and %1 => 1, %N => N, %& => the
;; rest param (:rest); anything else is not positional (nil). Fixed arity = max
;; index used (Clojure: #(do %2 %&) => [p1 p2 & rest], unused lower slots still
;; get a placeholder param).
(defn- pct-index [nm]
(cond
(= nm "%") 1
(= nm "%&") :rest
(and (> (count nm) 1) (= "%" (subs nm 0 1)))
(let [n (form-scan-number (subs nm 1))]
(if (and n (integer? n) (>= n 1)) n nil))
:else nil))
;; Pass 1: collect every %-index used anywhere in the form tree.
(defn- collect-pcts [form acc]
(cond
(form-sym? form) (let [i (pct-index (form-sym-name form))] (if i (conj acc i) acc))
(form-list? form) (reduce (fn [a x] (collect-pcts x a)) acc (form-elements form))
(form-vec? form) (reduce (fn [a x] (collect-pcts x a)) acc (form-vec-items form))
(form-set? form) (reduce (fn [a x] (collect-pcts x a)) acc (form-set-items form))
(form-map? form) (reduce (fn [a p] (collect-pcts (nth p 1) (collect-pcts (nth p 0) a)))
acc (form-map-pairs form))
:else acc))
;; Pass 2: replace each %-symbol with its slot's gensym (rebuilding collections).
(defn- replace-pct [form slot-syms rest-sym]
(cond
(form-sym? form) (let [idx (pct-index (form-sym-name form))]
(cond (= idx :rest) rest-sym
idx (get slot-syms idx)
:else form))
(form-list? form) (form-make-list (mapv #(replace-pct % slot-syms rest-sym) (form-elements form)))
(form-vec? form) (form-make-vector (mapv #(replace-pct % slot-syms rest-sym) (form-vec-items form)))
(form-set? form) (form-make-set (mapv #(replace-pct % slot-syms rest-sym) (form-set-items form)))
(form-map? form) (form-make-map
(vec (mapcat (fn [p] [(replace-pct (nth p 0) slot-syms rest-sym)
(replace-pct (nth p 1) slot-syms rest-sym)])
(form-map-pairs form))))
:else form))
(defn- gensym-param [] (form-make-symbol (str (form-gensym-name) "#")))
(defn- read-anon-fn* [s pos]
;; pos at #, next char (
(let [[form np] (read-next-form s (inc pos))
pcts (collect-pcts form [])
max-n (reduce (fn [m i] (if (and (number? i) (> i m)) i m)) 0 pcts)
has-rest (boolean (some #(= :rest %) pcts))
slot-syms (into {} (map (fn [i] [i (gensym-param)]) (range 1 (inc max-n))))
rest-sym (when has-rest (gensym-param))
replaced (replace-pct form slot-syms rest-sym)
arg-names (let [base (mapv #(get slot-syms %) (range 1 (inc max-n)))]
(if has-rest (conj base (form-make-symbol "&") rest-sym) base))]
[:form (form-make-list [(form-make-symbol "fn*") (form-make-vector arg-names) replaced]) np]))
(defn read-form [s pos]
(let [pos (skip-whitespace s pos)]
(if (>= pos (len s))
[:form nil pos]
(let [c (cp s pos)]
(cond
(= c 59) [:skip nil (read-until-newline s pos)] ; ; comment
(= c 34) (let [r (read-string* s pos)] [:form (nth r 0) (nth r 1)])
(= c 58) (let [r (read-keyword* s pos)] [:form (nth r 0) (nth r 1)])
(= c 92) (let [r (read-char* s pos)] [:form (nth r 0) (nth r 1)])
(= c 40) (read-list* s pos) ; (
(= c 91) (read-vector* s pos) ; [
(= c 123) (read-map* s pos) ; {
(= c 39) (read-quote* s (inc pos) (form-make-symbol "quote")) ; '
(= c 96) (read-quote* s (inc pos) (form-make-symbol "syntax-quote")) ; `
(= c 126) (if (and (< (inc pos) (len s)) (= (cp s (inc pos)) 64)) ; ~ / ~@
(read-quote* s (+ pos 2) (form-make-symbol "unquote-splicing"))
(read-quote* s (inc pos) (form-make-symbol "unquote")))
(= c 64) (read-quote* s (inc pos) (form-make-symbol "clojure.core/deref")) ; @
(= c 94) (read-meta* s pos) ; ^
(= c 41) (throw (ex-info "Unmatched delimiter: )" {}))
(= c 93) (throw (ex-info "Unmatched delimiter: ]" {}))
(= c 125) (throw (ex-info "Unmatched delimiter: }" {}))
(= c 35) (read-dispatch* s pos) ; #
(number-start? s pos c) (let [r (read-number* s pos)] [:form (nth r 0) (nth r 1)])
(symbol-start? c) (let [r (read-symbol* s pos)] [:form (nth r 0) (nth r 1)])
:else (throw (ex-info (str "read-form: unexpected char '" (char c) "' (" c ")") {})))))))
(defn read-one
"Read the first form of `s` (skipping leading trivia). Returns the form."
[s]
(first (read-next-form s 0)))
(defn read-all
"Read every top-level form of `s`, returning them in a vector (trivia skipped)."
[s]
(loop [pos 0 acc []]
(let [p (skip-whitespace s pos)]
(if (>= p (len s))
acc
(let [[kind payload np] (read-form s p)]
(case kind
:skip (recur np acc)
:splice (recur np (into acc payload))
:form (recur np (conj acc payload))))))))

View file

@ -55,12 +55,10 @@
PushbackReader, io/reader results) expose char-wise .read; a raw file
handle is read whole."
[reader]
(if (= :core/file (janet/type reader))
(janet.file/read reader :all)
(loop [acc (transient []) c (.read reader)]
(if (== -1 c)
(apply str (map char (persistent! acc)))
(recur (conj! acc c) (.read reader))))))
(loop [acc (transient []) c (.read reader)]
(if (== -1 c)
(apply str (map char (persistent! acc)))
(recur (conj! acc c) (.read reader)))))
(defn read
"Reads one EDN object from reader (a PushbackReader or any jolt reader).

View file

@ -96,15 +96,6 @@
[s]
(str-trimr s))
(defn trim-newline
[s]
(var result s)
(while (or (= (subs result (dec (count result))) "\n")
(= (subs result (dec (count result))) "\r"))
(set result (subs result 0 (dec (count result)))))
result)
(defn escape
[s cmap]