jolt/host/chez/java/host-static-classes.ss
Yogthos 0becba7f93 A fn def'd into a var reports a JVM-style class name (clojure.core$odd_QMARK_)
jolt fns reported (class f) = clojure.lang.IFn, so they carried no defining
symbol — clojure.spec.alpha's fn-sym (which reads a fn's class name to recover its
symbol) produced garbage, so explain-data's :pred for a bare-fn predicate was `/`
instead of e.g. clojure.core/keyword?.

Now def-var! records proc -> (ns . name) (first def of a proc wins, so an alias
like (def inc' inc) doesn't rename inc), and jolt-class-name returns "ns$munged"
for a known fn — matching the JVM, where (class odd?) is clojure.core$odd_QMARK_.
A munged fn class's ancestors include clojure.lang.AFunction's hierarchy
(IFn/AFn/Fn/Runnable/Callable), so (ancestors (class f)) still holds. Anonymous /
unregistered fns stay clojure.lang.IFn (fn-sym yields :unknown, as on the JVM).

This fixes explain-data / s/form / s/describe of bare-fn predicates in
clojure.spec.alpha (and unblocks parts of its suite + test.check's reporter test).

make test green (+1 corpus row, the (type inc) unit row updated to the JVM value),
shakesmoke byte-identical, runtime only (no re-mint).
2026-06-27 21:03:12 -04:00

1023 lines
64 KiB
Scheme

;; host-static-classes.ss — instantiable host object classes: ArrayList, HashMap,
;; the String/Reader/Writer/Tokenizer shims, BigInteger/MapEntry ctors, and URL
;; codecs. Holds the tagged-table method dispatch (the (.method ...) arm on a jhost)
;; and the pluggable instance? hook. Loaded after host-static-methods.ss; the
;; `Class/member` static methods live there, the registry core in host-static.ss.
;; ---- 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))))))))
(define arraylist-methods
(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 "addAll" (lambda (self . a)
;; (.addAll coll) appends; (.addAll i coll) inserts at i.
(let* ((at-i (= 2 (length a)))
(i (if at-i (jnum->exact (car a)) (al-cnt self)))
(coll (if at-i (cadr a) (car a))))
(let loop ((xs (seq->list (jolt-seq coll))) (k i))
(if (null? xs) (pair? (seq->list (jolt-seq coll)))
(begin (al-insert-at! self k (car xs)) (loop (cdr xs) (fx+ k 1))))))))
(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-host-methods! "arraylist" arraylist-methods)
;; java.util.LinkedList: the ArrayList backing plus the Deque surface
;; (addFirst/addLast/removeFirst/removeLast/getFirst/getLast/peek/push/pop).
;; tools.reader holds pending splice forms in one and (seq)s / .remove(0)s it.
(define (al-first self) (vector-ref (al-vec self) 0))
(define (al-last self) (vector-ref (al-vec self) (fx- (al-cnt self) 1)))
(define linkedlist-methods
(append arraylist-methods
(list
(cons "addFirst" (lambda (self x) (al-insert-at! self 0 x) jolt-nil))
(cons "addLast" (lambda (self x) (al-push! self x) jolt-nil))
(cons "offer" (lambda (self x) (al-push! self x) #t))
(cons "removeFirst" (lambda (self) (let ((o (al-first self))) (al-remove-at! self 0) o)))
(cons "removeLast" (lambda (self) (let ((o (al-last self))) (al-remove-at! self (fx- (al-cnt self) 1)) o)))
(cons "getFirst" al-first) (cons "getLast" al-last)
(cons "peek" (lambda (self) (if (fx=? 0 (al-cnt self)) jolt-nil (al-first self))))
(cons "poll" (lambda (self) (if (fx=? 0 (al-cnt self)) jolt-nil (let ((o (al-first self))) (al-remove-at! self 0) o))))
(cons "push" (lambda (self x) (al-insert-at! self 0 x) jolt-nil))
(cons "pop" (lambda (self) (let ((o (al-first self))) (al-remove-at! self 0) o))))))
(define (make-linkedlist xs)
(let ((al (make-arraylist xs))) (make-jhost "linkedlist" (jhost-state al))))
(register-host-methods! "linkedlist" linkedlist-methods)
(let ((ctor (lambda args
(cond ((null? args) (make-linkedlist '()))
(else (make-linkedlist (seq->list (jolt-seq (car args)))))))))
(register-class-ctor! "LinkedList" ctor)
(register-class-ctor! "java.util.LinkedList" ctor))
;; ArrayList / LinkedList are Iterable: (seq al) walks the elements (nil if empty),
;; so (seq pending-forms) and reduce/into over one work like the JVM.
(define %al-seq jolt-seq)
(set! jolt-seq
(lambda (x)
(if (and (jhost? x) (or (string=? (jhost-tag x) "arraylist") (string=? (jhost-tag x) "linkedlist")))
(list->cseq (al->list x))
(%al-seq x))))
;; Appendable.append text: append(x) renders x; append(csq,start,end) appends the
;; subsequence csq[start,end) (data.json's writer appends string runs this way).
(define (append-text x rest)
(if (null? rest)
(render-piece x)
(substring (render-piece x) (jnum->exact (car rest)) (jnum->exact (cadr rest)))))
(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 . rest) (sb-set! self (string-append (sb-str self) (append-text x rest))) 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))))
;; (str sb) / print a StringBuilder -> its accumulated content, like the JVM
;; (str calls toString). Without this str renders the opaque host object.
(register-str-render! (lambda (x) (and (jhost? x) (string=? (jhost-tag x) "string-builder"))) sb-str)
;; ---- 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 . rest) (sb-set! self (string-append (sb-str self) (append-text x rest))) self))
(cons "flush" (lambda (self) jolt-nil))
(cons "close" (lambda (self) jolt-nil))
(cons "toString" (lambda (self) (sb-str self)))))
;; (str sw) / print a StringWriter -> its accumulated content, like the JVM
;; (str calls toString) — data.csv writes CSV to a StringWriter and reads it back.
(register-str-render! (lambda (x) (and (jhost? x) (string=? (jhost-tag x) "writer"))) sb-str)
;; 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 . rest) (fw-append! self (append-text x rest)) 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 . rest) (display (append-text x rest) (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))))
;; PrintWriter — a thin wrapper over a target writer. write/append/print forward
;; the rendered text to the target. clojure.data.json's pretty printer builds
;; (PrintWriter. *out*) where *out* is bound to clojure.pprint's pretty-writer (a
;; jolt record), so forwarding routes column-aware through clojure.pprint/-write;
;; for a host writer target it falls back to that writer's own write.
(define (pw-forward target s)
(cond
((and (jhost? target) (string=? (jhost-tag target) "port-writer"))
(display s (vector-ref (jhost-state target) 0)))
((and (jhost? target) (memv #t (list (string=? (jhost-tag target) "writer")
(string=? (jhost-tag target) "string-builder"))))
(sb-set! target (string-append (sb-str target) s)))
(else
(jolt-invoke (var-deref "clojure.pprint" "-write") target s))))
(register-class-ctor! "PrintWriter"
(lambda args (make-jhost "print-writer" (vector (if (pair? args) (car args) jolt-nil)))))
(register-class-ctor! "java.io.PrintWriter"
(lambda args (make-jhost "print-writer" (vector (if (pair? args) (car args) jolt-nil)))))
(register-host-methods! "print-writer"
(list (cons "write" (lambda (self x . rest) (pw-forward (vector-ref (jhost-state self) 0) (append-text x rest)) jolt-nil))
(cons "print" (lambda (self x) (pw-forward (vector-ref (jhost-state self) 0) (render-piece x)) jolt-nil))
(cons "append" (lambda (self x . rest) (pw-forward (vector-ref (jhost-state self) 0) (append-text x rest)) self))
(cons "flush" (lambda (self) jolt-nil))
(cons "close" (lambda (self) jolt-nil))
(cons "toString" (lambda (self) ""))))
;; ---- 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))))))
;; java.util.concurrent.ConcurrentHashMap — one shared heap, so the mutable
;; HashMap shim serves. (get a-hashmap k) reads the map (clojure.core/get).
(define (make-hashmap-jhost . 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))))
(register-class-ctor! "ConcurrentHashMap" make-hashmap-jhost)
(register-class-ctor! "java.util.concurrent.ConcurrentHashMap" make-hashmap-jhost)
(register-get-arm! (lambda (x) (and (jhost? x) (string=? (jhost-tag x) "hashmap")))
(lambda (coll k d) (hashtable-ref (hm-tbl coll) k d)))
;; count / contains? over the mutable map shim (clojure.core/count + contains?,
;; which core.cache's SoftCache uses on its backing ConcurrentHashMap).
(define (jhost-hashmap? x) (and (jhost? x) (string=? (jhost-tag x) "hashmap")))
(let ((prev-count jolt-count) (prev-contains jolt-contains?))
(set! jolt-count (lambda (c) (if (jhost-hashmap? c) (hashtable-size (hm-tbl c)) (prev-count c))))
(set! jolt-contains? (lambda (c k) (if (jhost-hashmap? c)
(if (hashtable-contains? (hm-tbl c) k) #t #f)
(prev-contains c k)))))
;; ---- java.lang.ref.Soft/WeakReference + ReferenceQueue ----------------------
;; Real GC reclamation via Chez's generational collector: the referent is held
;; through a weak pair (collected once otherwise unreachable, leaving the bwp
;; object), and a guardian registered on the referent makes the reference itself
;; available the moment its referent is reclaimed — which the ReferenceQueue
;; surfaces as enqueued, exactly like the JVM. (Chez has no softer-than-weak
;; reference, so a SoftReference clears on unreachability rather than under memory
;; pressure — its SoftCache evicts more eagerly than the JVM's, but it is genuine
;; GC eviction, not an unbounded strong cache. Immediates like fixnums/keywords
;; are never collected.)
;; ref-queue state: #(guardian pending-list); reference state: #(weak-pair queue enqueued?).
(define (rq-guardian-of q) (vector-ref (jhost-state q) 0))
(define (rq-add! q ref)
(let ((st (jhost-state q))) (vector-set! st 1 (append (vector-ref st 1) (list ref)))))
(define (rq-pump! q) ; drain GC-reclaimed refs onto the list
(let loop ()
(let ((rep ((rq-guardian-of q)))) (when rep (rq-add! q rep) (loop)))))
(define (rq-poll q)
(rq-pump! q)
(let* ((st (jhost-state q)) (l (vector-ref st 1)))
(if (null? l) jolt-nil (begin (vector-set! st 1 (cdr l)) (car l)))))
(define (a-ref-queue? x) (and (jhost? x) (string=? (jhost-tag x) "ref-queue")))
(define (make-reference v rest)
(let* ((rq (if (pair? rest) (car rest) jolt-nil))
(ref (make-jhost "weak-ref" (vector (weak-cons v #f) rq #f))))
(when (a-ref-queue? rq) ((rq-guardian-of rq) v ref)) ; fire on the referent's collection
ref))
(for-each (lambda (nm) (register-class-ctor! nm (lambda (v . rest) (make-reference v rest))))
'("SoftReference" "java.lang.ref.SoftReference" "WeakReference" "java.lang.ref.WeakReference"))
(register-host-methods! "weak-ref"
(list (cons "get" (lambda (self) (let ((r (car (vector-ref (jhost-state self) 0))))
(if (bwp-object? r) jolt-nil r))))
(cons "clear" (lambda (self) (set-car! (vector-ref (jhost-state self) 0) jolt-nil) jolt-nil))
(cons "isEnqueued" (lambda (self) (vector-ref (jhost-state self) 2)))
(cons "enqueue" (lambda (self)
(let* ((st (jhost-state self)) (rq (vector-ref st 1)))
(if (vector-ref st 2) #f
(begin (vector-set! st 2 #t) (when (a-ref-queue? rq) (rq-add! rq self)) #t)))))))
(for-each (lambda (nm) (register-class-ctor! nm (lambda _ (make-jhost "ref-queue" (vector (make-guardian) '())))))
'("ReferenceQueue" "java.lang.ref.ReferenceQueue"))
(register-host-methods! "ref-queue"
(list (cons "poll" (lambda (self . _) (rq-poll self)))
(cons "remove" (lambda (self . _) (rq-poll 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 . rest)
(let ((s (sr-s self)) (p (sr-pos self)))
(cond
;; .read() -> one char code, -1 at EOF
((null? rest)
(if (>= p (string-length s)) -1
(begin (sr-pos! self (+ p 1)) (->num (char->integer (string-ref s p))))))
;; .read(cbuf, off, len) -> fill cbuf, return count or -1 at EOF
(else
(let ((slen (string-length s)))
(if (>= p slen) -1
(let ((cbuf (car rest)) (off (jnum->exact (cadr rest))) (len (jnum->exact (caddr rest))))
(let ((n (min len (- slen p))) (dv (jolt-array-vec cbuf)))
(let loop ((i 0)) (when (< i n) (vector-set! dv (+ off i) (string-ref s (+ p i))) (loop (+ i 1))))
(sr-pos! self (+ p n)) (->num n))))))))))
(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 '()))))
;; LineNumberingPushbackReader: a pushback-reader (jolt doesn't track line
;; numbers; getLineNumber is a stub for error-reporting paths that read it).
(register-class-ctor! "LineNumberingPushbackReader"
(lambda (rdr . _) (make-jhost "pushback-reader" (vector rdr '()))))
(register-class-ctor! "clojure.lang.LineNumberingPushbackReader"
(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 . rest)
(define (read1)
(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)))))
(if (null? rest)
(read1)
;; .read(cbuf, off, len) -> read one code unit at a time into cbuf,
;; return count or -1 at immediate EOF.
(let ((off (jnum->exact (cadr rest))) (len (jnum->exact (caddr rest))) (dv (jolt-array-vec (car rest))))
(let loop ((i 0))
(if (>= i len) (->num i)
(let ((c (jnum->exact (read1))))
(if (= c -1) (if (= i 0) -1 (->num i))
(begin (vector-set! dv (+ off i) (integer->char c)) (loop (+ i 1)))))))))))
(cons "unread"
(lambda (self ch . rest)
(if (null? rest)
;; unread(int|char) — push one code unit back
(vector-set! (jhost-state self) 1
(cons (if (char? ch) (->num (char->integer ch)) ch) (vector-ref (jhost-state self) 1)))
;; unread(char[] cbuf, off, len) — push cbuf[off,off+len) so cbuf[off]
;; reads back first (the list head).
(let ((dv (jolt-array-vec ch)) (off (jnum->exact (car rest))) (len (jnum->exact (cadr rest))))
(let loop ((i (- (+ off len) 1)) (acc (vector-ref (jhost-state self) 1)))
(if (< i off)
(vector-set! (jhost-state self) 1 acc)
(loop (- i 1) (cons (->num (char->integer (vector-ref dv i))) acc))))))
jolt-nil))
(cons "close" (lambda (self) jolt-nil))
(cons "getLineNumber" (lambda (self) 0))))
;; ---- 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. char[] [offset count]) — the whole array or a slice. Buffered
;; readers (data.json) build a string from a fill buffer this way.
((and (jolt-array? x) (eq? (jolt-array-kind x) 'char))
(let ((v (jolt-array-vec x)))
(if (pair? rest)
(let* ((off (jnum->exact (car rest))) (cnt (jnum->exact (cadr rest))) (out (make-string cnt)))
(let loop ((i 0)) (when (fx<? i cnt) (string-set! out i (vector-ref v (fx+ off i))) (loop (fx+ i 1))))
out)
(list->string (vector->list v)))))
((string? x) x)
(else (jolt-str-render-one x)))))
;; (BigInteger. s) | (BigInteger. s radix) — parse a string in the given radix
;; (default 10). tools.reader's integer parser builds (BigInteger. digits radix).
(register-class-ctor! "BigInteger"
(lambda (v . r) (parse-int-or-throw v (if (null? r) 10 (jnum->exact (car r))) "BigInteger")))
(register-class-ctor! "java.math.BigInteger"
(lambda (v . r) (parse-int-or-throw v (if (null? r) 10 (jnum->exact (car r))) "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" "EOFException" "java.io.EOFException"
"Error" "AssertionError"))
;; ---- 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)
(let ((rest (if (jolt-nil? rest-args) '() (seq->list rest-args))))
(cond
((regex-t? obj)
(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))
((or (string=? method-name "toString")) (regex-t-source obj))
;; (.matcher pattern s) -> a Matcher (matcher-t) for stepping matches.
((string=? method-name "matcher") (jolt-re-matcher obj (car rest)))
(else (error #f (string-append "No method " method-name " on Pattern")))))
;; java.util.regex.Matcher: .matches (anchored whole-region), .find
;; (next match), .group [n], .groupCount.
((jolt-matcher? obj)
(cond ((string=? method-name "matches") (jolt-matcher-matches obj))
((string=? method-name "find") (not (jolt-nil? (jolt-re-find obj))))
((string=? method-name "group") (apply jolt-matcher-group obj rest))
((string=? method-name "groupCount") (jolt-matcher-group-count obj))
(else (error #f (string-append "No method " method-name " on Matcher")))))
(else (%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))
;; java.lang.ThreadLocal via a Chez thread-parameter: real per-thread storage with
;; a lazy initialValue (the proxy macro lowers (proxy [ThreadLocal] …) to this).
;; .get returns the thread's value, computing initialValue once; .set / .remove.
(define tl-unset (list 'tl-unset))
(define (jolt-make-thread-local init-thunk)
(make-jhost "threadlocal" (vector (make-thread-parameter tl-unset) init-thunk)))
(register-host-methods! "threadlocal"
(list (cons "get" (lambda (self)
(let* ((st (jhost-state self)) (tp (vector-ref st 0)) (v (tp)))
(if (eq? v tl-unset)
(let ((nv (jolt-invoke (vector-ref st 1)))) (tp nv) nv)
v))))
(cons "set" (lambda (self v) ((vector-ref (jhost-state self) 0) v) jolt-nil))
(cons "remove" (lambda (self) ((vector-ref (jhost-state self) 0) tl-unset) jolt-nil))))
(def-var! "jolt.host" "make-thread-local" jolt-make-thread-local)
;; 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))
;; (instance? clojure.lang.IFoo x) for the core clojure.lang interfaces libraries
;; branch on — jolt's value model satisfies them, so report it. Matched by the
;; interface's last dotted segment, so "clojure.lang.IObj" and "IObj" both hit.
(define (hsc-last-segment s)
(let loop ((i (- (string-length s) 1)))
(cond ((< i 0) s)
((char=? (string-ref s i) #\.) (substring s (+ i 1) (string-length s)))
(else (loop (- i 1))))))
;; values that carry metadata (mirrors jolt-with-meta's set in natives-meta.ss).
(define (hsc-imeta? x)
(or (pvec? x) (pmap? x) (pset? x) (cseq? x) (empty-list-t? x)
(jolt-lazyseq? x) (jrec? x) (jreify? x) (procedure? x) (symbol-t? x)))
(register-instance-check-arm!
(lambda (type-sym val)
(let ((iface (hsc-last-segment (symbol-t-name type-sym))))
(let ((hit (cond
((or (string=? iface "IObj") (string=? iface "IMeta")) (hsc-imeta? val))
((or (string=? iface "IMapEntry") (string=? iface "MapEntry")) (jolt-map-entry? val))
((string=? iface "IRecord") (jrec? val))
((string=? iface "IPersistentMap") (or (pmap? val) (htable-sorted-map? val)))
((string=? iface "IPersistentVector") (and (pvec? val) (not (jolt-map-entry? val))))
((string=? iface "IPersistentSet") (or (pset? val) (htable-sorted-set? val)))
((string=? iface "ISeq")
(or (cseq? val) (empty-list-t? val) (jolt-lazyseq? val)))
;; Seqable is anything (seq x) works on — every persistent
;; collection, not just seqs (a vector IS Seqable, not an ISeq).
((string=? iface "Seqable")
(or (cseq? val) (empty-list-t? val) (jolt-lazyseq? val)
(pvec? val) (pmap? val) (pset? val)
(htable-sorted-map? val) (htable-sorted-set? val)))
((string=? iface "Sequential")
(or (pvec? val) (cseq? val) (empty-list-t? val) (jolt-lazyseq? val)))
((string=? iface "IFn")
(or (procedure? val) (keyword? val) (symbol-t? val)
(pmap? val) (pset? val) (pvec? val)))
;; host-class interfaces libraries branch on (data.json, etc.).
;; Matched by last segment, so java.util.Map and Map both hit.
((string=? iface "Named") (or (keyword? val) (symbol-t? val)))
((string=? iface "CharSequence") (string? val))
((string=? iface "Number") (number? val))
((string=? iface "Map") (or (pmap? val) (htable-sorted-map? val)))
((string=? iface "Set") (or (pset? val) (htable-sorted-set? val)))
;; a Java List is a vector or a seq/list — not a set or map.
((string=? iface "List")
(or (and (pvec? val) (not (jolt-map-entry? val)))
(cseq? val) (empty-list-t? val) (jolt-lazyseq? val)))
;; a Java Collection is any of those plus a set — but NOT a map.
((string=? iface "Collection")
(or (pvec? val) (pset? val) (cseq? val) (empty-list-t? val)
(jolt-lazyseq? val) (htable-sorted-set? val)))
((string=? iface "Associative")
(or (pmap? val) (htable-sorted-map? val)
(and (pvec? val) (not (jolt-map-entry? val)))))
;; ILookup (valAt): maps and vectors; Indexed (nth): vectors;
;; Counted: the counted collections. A deftype that declares one
;; is matched by type-satisfies? in instance-check-base.
((string=? iface "ILookup")
(or (pmap? val) (htable-sorted-map? val)
(and (pvec? val) (not (jolt-map-entry? val)))))
((string=? iface "Indexed")
(and (pvec? val) (not (jolt-map-entry? val))))
((string=? iface "Counted")
(or (pmap? val) (pset? val) (pvec? val)
(htable-sorted-map? val) (htable-sorted-set? val)))
;; reader jhosts — data.json re-wraps a reader in a new
;; PushbackReader unless (instance? PushbackReader r), so this
;; must hold for repeated reads from one reader to work.
((string=? iface "PushbackReader")
(and (jhost? val) (string=? (jhost-tag val) "pushback-reader")))
((string=? iface "StringReader")
(and (jhost? val) (string=? (jhost-tag val) "string-reader")))
((or (string=? iface "Reader") (string=? iface "BufferedReader"))
(reader-jhost? val))
(else 'none))))
(if (eq? hit 'none) 'pass (if hit #t #f))))))
;; java.lang.Class value: (class x) / (.getClass x) return one. It renders like
;; the JVM — str/.toString -> "class <name>", pr -> "<name>", .getName -> "<name>"
;; — but stays = and hash equal to its name STRING, so (= (class x) String),
;; class-keyed maps/sets, multimethod dispatch on class, and instance? all keep
;; working against the bare class-name tokens.
(define (make-class-obj name) (make-jhost "class" (vector name)))
(define (jclass? x) (and (jhost? x) (string=? (jhost-tag x) "class")))
(define (jclass-name x) (vector-ref (jhost-state x) 0))
(define (class-key x) (cond ((jclass? x) (jclass-name x)) ((string? x) x) (else #f)))
(register-eq-arm! (lambda (a b) (or (jclass? a) (jclass? b)))
(lambda (a b) (let ((ka (class-key a)) (kb (class-key b)))
(and ka kb (string=? ka kb) #t))))
(register-hash-arm! jclass? (lambda (x) (jolt-hash (jclass-name x))))
(register-str-render! jclass? (lambda (x) (string-append "class " (jclass-name x))))
(register-pr-arm! jclass? (lambda (x) (jclass-name x)))
(register-host-methods! "class"
(list (cons "getName" (lambda (self) (jclass-name self)))
(cons "getCanonicalName" (lambda (self) (jclass-name self)))
(cons "getSimpleName" (lambda (self) (hsc-last-segment (jclass-name self))))
(cons "toString" (lambda (self) (string-append "class " (jclass-name self))))
(cons "isArray" (lambda (self) (let ((n (jclass-name self)))
(and (fx>? (string-length n) 0) (char=? (string-ref n 0) #\[)))))
;; Class.isInstance(o) == (instance? class o); core.logic's deftype .equals
;; uses (.. this getClass (isInstance o)).
(cons "isInstance" (lambda (self o) (if (instance-check self o) #t #f)))
(cons "getClass" (lambda (self) (make-class-obj "java.lang.Class")))))
;; (jolt.host/table? x) — is x a host tagged-table?
(def-var! "jolt.host" "table?" (lambda (x) (if (htable? x) #t #f)))
;; --- java.util.Arrays -------------------------------------------------------
(let ((arrays-statics
(list
(cons "equals" (lambda (a b)
(cond ((and (jolt-nil? a) (jolt-nil? b)) #t)
((or (jolt-nil? a) (jolt-nil? b)) #f)
(else (equal? (jolt-array-vec a) (jolt-array-vec b))))))
(cons "fill" (lambda (a v) (vector-fill! (jolt-array-vec a) v) jolt-nil))
(cons "copyOf" (lambda (a n)
(let* ((src (jolt-array-vec a)) (len (jnum->exact n))
(out (make-vector len 0)))
(do ((i 0 (fx+ i 1))) ((fx=? i (min len (vector-length src))))
(vector-set! out i (vector-ref src i)))
(make-jolt-array out (jolt-array-kind a)))))
(cons "copyOfRange" (lambda (a from to)
(let* ((src (jolt-array-vec a)) (f (jnum->exact from)) (tt (jnum->exact to))
(len (- tt f)) (out (make-vector len 0)))
(do ((i 0 (fx+ i 1))) ((fx=? i len))
(vector-set! out i (vector-ref src (+ f i))))
(make-jolt-array out (jolt-array-kind a)))))
(cons "toString" (lambda (a) (jolt-pr-str (apply jolt-vector (vector->list (jolt-array-vec a)))))))))
(register-class-statics! "Arrays" arrays-statics)
(register-class-statics! "java.util.Arrays" arrays-statics))
;; --- java.util.Random -------------------------------------------------------
;; A non-cryptographic PRNG over Chez's `random`. A seed argument is accepted but
;; not honored for reproducibility (jolt has no seedable Random state); callers
;; that need determinism use SecureRandom or their own generator.
(for-each
(lambda (nm) (register-class-ctor! nm (lambda args (make-jhost "random" (vector)))))
'("Random" "java.util.Random"))
(register-host-methods! "random"
(list
(cons "nextBytes" (lambda (self ba)
(let ((v (jolt-array-vec ba)))
(do ((i 0 (fx+ i 1))) ((fx=? i (vector-length v)))
(vector-set! v i (random 256))))
jolt-nil))
(cons "nextInt" (lambda (self . a)
(->num (if (pair? a) (random (jnum->exact (car a))) (- (random 4294967296) 2147483648)))))
(cons "nextLong" (lambda (self) (->num (- (random 18446744073709551616) 9223372036854775808))))
(cons "nextDouble" (lambda (self) (random 1.0)))
(cons "nextFloat" (lambda (self) (random 1.0)))
(cons "nextBoolean" (lambda (self) (fx=? 0 (random 2))))))
;; --- java.util.Optional -----------------------------------------------------
;; Returned by getters across java.time / java.net.http (e.g. HttpRequest.timeout,
;; HttpClient.connectTimeout). Value-equal so (= (Optional/of x) (Optional/of x)).
(define (jt-optional present? value) (make-jhost "optional" (vector present? value)))
(define jt-optional-empty (jt-optional #f jolt-nil))
(define (opt? x) (and (jhost? x) (string=? (jhost-tag x) "optional")))
(define (opt-present? o) (vector-ref (jhost-state o) 0))
(define (opt-value o) (vector-ref (jhost-state o) 1))
(let ((statics (list (cons "of" (lambda (v) (if (jolt-nil? v) (error #f "Optional.of(null)") (jt-optional #t v))))
(cons "ofNullable" (lambda (v) (if (jolt-nil? v) jt-optional-empty (jt-optional #t v))))
(cons "empty" (lambda _ jt-optional-empty)))))
(register-class-statics! "Optional" statics)
(register-class-statics! "java.util.Optional" statics))
(register-host-methods! "optional"
(list (cons "isPresent" (lambda (o) (opt-present? o)))
(cons "isEmpty" (lambda (o) (not (opt-present? o))))
(cons "get" (lambda (o) (if (opt-present? o) (opt-value o) (error #f "Optional.get() on empty Optional"))))
(cons "orElse" (lambda (o d) (if (opt-present? o) (opt-value o) d)))
(cons "orElseGet" (lambda (o f) (if (opt-present? o) (opt-value o) (jolt-invoke f))))
(cons "ifPresent" (lambda (o f) (when (opt-present? o) (jolt-invoke f (opt-value o))) jolt-nil))
(cons "toString" (lambda (o) (if (opt-present? o)
(string-append "Optional[" (jolt-str-render-one (opt-value o)) "]")
"Optional.empty")))))
(register-eq-arm! (lambda (a b) (or (opt? a) (opt? b)))
(lambda (a b) (and (opt? a) (opt? b) (eq? (opt-present? a) (opt-present? b))
(or (not (opt-present? a)) (jolt=2 (opt-value a) (opt-value b))))))
;; --- minimal JVM class/interface ancestry -----------------------------------
;; A handful of libraries reflect over the class hierarchy — e.g. core.memoize
;; validates its first argument with (some #{IFn AFn Runnable Callable}
;; (ancestors (class f))). jolt models a class as its name string and has no
;; reflection, so supers/ancestors return nothing on their own. This table gives
;; the common interfaces the direct supers the JVM reports, and the overlay's
;; supers/ancestors fold it in. Keyed by canonical class name; value = direct
;; supers. Extend as more interfaces are exercised.
(define class-supers-tbl (make-hashtable string-hash string=?))
(define (reg-class-supers! name supers) (hashtable-set! class-supers-tbl name supers))
(reg-class-supers! "clojure.lang.IFn" '("java.lang.Runnable" "java.util.concurrent.Callable"))
(reg-class-supers! "clojure.lang.AFn" '("clojure.lang.IFn" "java.lang.Runnable" "java.util.concurrent.Callable"))
(reg-class-supers! "clojure.lang.AFunction" '("clojure.lang.AFn" "clojure.lang.IFn" "clojure.lang.Fn"
"java.lang.Runnable" "java.util.concurrent.Callable"))
;; common exception hierarchy, so (instance? IOException e) / (catch IOException e)
;; match a more specific throwable a library threw (e.g. http-client's
;; UnknownHostException, caught by clj-http-lite's :ignore-unknown-host?).
(reg-class-supers! "java.lang.Throwable" '("java.lang.Object"))
(reg-class-supers! "java.lang.Exception" '("java.lang.Throwable" "java.lang.Object"))
(reg-class-supers! "java.lang.RuntimeException" '("java.lang.Exception" "java.lang.Throwable" "java.lang.Object"))
(reg-class-supers! "java.io.IOException" '("java.lang.Exception" "java.lang.Throwable" "java.lang.Object"))
(reg-class-supers! "java.io.InterruptedIOException" '("java.io.IOException" "java.lang.Exception" "java.lang.Throwable" "java.lang.Object"))
(reg-class-supers! "java.net.SocketException" '("java.io.IOException" "java.lang.Exception" "java.lang.Throwable" "java.lang.Object"))
(reg-class-supers! "java.net.UnknownHostException" '("java.io.IOException" "java.lang.Exception" "java.lang.Throwable" "java.lang.Object"))
(reg-class-supers! "java.net.ConnectException" '("java.net.SocketException" "java.io.IOException" "java.lang.Exception" "java.lang.Throwable" "java.lang.Object"))
(reg-class-supers! "java.net.SocketTimeoutException" '("java.io.InterruptedIOException" "java.io.IOException" "java.lang.Exception" "java.lang.Throwable" "java.lang.Object"))
;; clojure.lang / java.util ancestry for the builtins (class) reports, so a
;; class-keyed multimethod / (isa? (class x) SomeClass) dispatches like the JVM.
;; (Object is supplied universally by class-isa?, so it need not be listed.)
(reg-class-supers! "clojure.lang.IFn" '("clojure.lang.Fn" "java.lang.Runnable" "java.util.concurrent.Callable"))
;; Keyword and Symbol implement IFn (they are callable: (:k m) / ('s m)), so a
;; (class x)-dispatched multimethod with an IFn method matches them, like the JVM.
(reg-class-supers! "clojure.lang.Keyword" '("clojure.lang.Named" "java.lang.Comparable"
"clojure.lang.IFn" "clojure.lang.Fn"
"java.lang.Runnable" "java.util.concurrent.Callable"))
(reg-class-supers! "clojure.lang.Symbol" '("clojure.lang.Named" "java.lang.Comparable"
"clojure.lang.IFn" "clojure.lang.Fn"
"java.lang.Runnable" "java.util.concurrent.Callable"))
(reg-class-supers! "java.lang.String" '("java.lang.CharSequence" "java.lang.Comparable"))
(reg-class-supers! "clojure.lang.PersistentHashSet" '("clojure.lang.APersistentSet" "clojure.lang.IPersistentSet" "clojure.lang.IPersistentCollection" "java.util.Set" "java.util.Collection" "java.lang.Iterable"))
(reg-class-supers! "clojure.lang.PersistentTreeSet" '("clojure.lang.APersistentSet" "clojure.lang.IPersistentSet" "clojure.lang.IPersistentCollection" "java.util.Set" "java.util.Collection" "java.lang.Iterable"))
(reg-class-supers! "clojure.lang.PersistentVector" '("clojure.lang.APersistentVector" "clojure.lang.IPersistentVector" "clojure.lang.IPersistentCollection" "clojure.lang.Sequential" "clojure.lang.Associative" "java.util.List" "java.util.Collection" "java.lang.Iterable"))
(reg-class-supers! "clojure.lang.PersistentArrayMap" '("clojure.lang.APersistentMap" "clojure.lang.IPersistentMap" "clojure.lang.IPersistentCollection" "clojure.lang.Associative" "java.util.Map" "java.lang.Iterable"))
(reg-class-supers! "clojure.lang.PersistentHashMap" '("clojure.lang.APersistentMap" "clojure.lang.IPersistentMap" "clojure.lang.IPersistentCollection" "clojure.lang.Associative" "java.util.Map" "java.lang.Iterable"))
(reg-class-supers! "clojure.lang.PersistentList" '("clojure.lang.ASeq" "clojure.lang.ISeq" "clojure.lang.IPersistentCollection" "clojure.lang.Sequential" "clojure.lang.Seqable" "java.util.List" "java.util.Collection" "java.lang.Iterable"))
(reg-class-supers! "clojure.lang.LazySeq" '("clojure.lang.ISeq" "clojure.lang.IPersistentCollection" "clojure.lang.Sequential" "clojure.lang.Seqable" "java.lang.Iterable"))
(reg-class-supers! "clojure.lang.Cons" '("clojure.lang.ASeq" "clojure.lang.ISeq" "clojure.lang.Sequential" "clojure.lang.Seqable" "java.lang.Iterable"))
;; A munged fn class name "ns$name" (jolt-class for a def'd fn) isn't in the table;
;; like the JVM (a fn extends clojure.lang.AFunction) its super is AFunction, whose
;; registered supers give AFn / IFn / Fn / Runnable / Callable transitively.
(define (str-has-dollar? s)
(let loop ((i 0)) (and (< i (string-length s)) (or (char=? (string-ref s i) #\$) (loop (+ i 1))))))
(define (class-direct-supers name)
(or (hashtable-ref class-supers-tbl name #f)
(and (str-has-dollar? name) '("clojure.lang.AFunction"))
'()))
;; transitive closure of direct supers (set semantics via an accumulator list)
(define (class-ancestors-list name)
(let loop ((pending (class-direct-supers name)) (seen '()))
(cond ((null? pending) (reverse seen))
((member (car pending) seen) (loop (cdr pending) seen))
(else (loop (append (class-direct-supers (car pending)) (cdr pending))
(cons (car pending) seen))))))
;; (instance? Class e) on a throwable tagged-table carrying a JVM :class matches the
;; carried class or any of its ancestors (full name or last segment), so a library's
;; (catch UnknownHostException e …) / (catch IOException e …) matches the ex-info
;; envelope it threw. Mirrors the (class e) arm (host-table.ss) for catch dispatch,
;; which lowers to (instance? C e). Non-match returns 'pass so other arms still run.
(register-instance-check-arm!
(lambda (type-sym val)
(if (and (htable? val) (string? (hashtable-ref (htable-h val) "class" #f)))
(let* ((cls (hashtable-ref (htable-h val) "class" #f))
(want (symbol-t-name type-sym))
(want-seg (hsc-last-segment want)))
(let loop ((names (cons cls (class-ancestors-list cls))))
(cond ((null? names) 'pass)
((or (string=? want (car names))
(string=? want-seg (hsc-last-segment (car names)))) #t)
(else (loop (cdr names))))))
'pass)))
;; JVM class assignability for isa? (20-coll): true when child and parent are both
;; class values and parent is child, java.lang.Object (every class's root), or a
;; modeled ancestor of child (full name or last segment). nil for non-class args, so
;; isa? falls through to its hierarchy/vector logic.
(def-var! "jolt.host" "class-isa?"
(lambda (child parent)
(let ((cc (class-key child)) (pp (class-key parent)))
(if (and cc pp)
(let ((pseg (hsc-last-segment pp)))
(if (let loop ((names (cons cc (class-ancestors-list cc))))
(cond ((string=? pp "java.lang.Object") #t)
((null? names) #f)
((or (string=? pp (car names))
(string=? pseg (hsc-last-segment (car names)))) #t)
(else (loop (cdr names)))))
#t jolt-nil))
jolt-nil))))
;; (jolt.host/class-supers name) / (jolt.host/class-ancestors name) — a jolt seq of
;; super / ancestor class-name strings, or nil when jolt models no hierarchy for it.
(def-var! "jolt.host" "class-supers"
(lambda (x)
(let ((name (class-key x)))
(if (and name (hashtable-contains? class-supers-tbl name))
(list->cseq (hashtable-ref class-supers-tbl name '()))
jolt-nil))))
(def-var! "jolt.host" "class-ancestors"
(lambda (x)
(let ((name (class-key x)))
(if name
(let ((as (class-ancestors-list name)))
(if (null? as) jolt-nil (list->cseq as)))
jolt-nil))))