;; persistent collections on the Chez RT. ;; ;; The vector / map / set the emitted programs construct from literals and ;; operate on via the lowered leaf ops (conj/get/nth/count/assoc/...). Loaded by ;; rt.ss after values.ss; jolt=2 / jolt-hash (values.ss) call into the ;; jolt-coll? / jolt-coll=? / jolt-coll-hash hooks defined here (forward refs, ;; resolved at run time — nothing is CALLED during load). ;; ;; The persistent vector is a copy-on-write Scheme vector and the map/set are a ;; bitmap HAMT. They live in Scheme; correctness, not perf, is the gate. ;; ============================================================================ ;; small immutable-vector helpers (manual; avoid stdlib arg-order ambiguity) ;; ============================================================================ (define (vec-copy-range v start end) (let ((out (make-vector (fx- end start)))) (let loop ((i start)) (when (fx=? i (pv-tailoff (pvec-cnt p))) (pvec-tail p) (let loop ((node (pvec-root p)) (level (pvec-shift p))) (if (fx>? level 0) (loop (vector-ref node (fxand (fxsra i level) pv-mask)) (fx- level pv-bits)) node)))) ;; jolt models every number as a double, so vector indices arrive as flonums — ;; coerce an integer-valued index to a Scheme fixnum before bounds math. (define (->idx i) (if (fixnum? i) i (if (flonum? i) (exact (floor i)) i))) (define (pvec-count p) (pvec-cnt p)) (define (pvec-nth-d p i d) (let ((i (->idx i))) (if (and (fixnum? i) (fx>=? i 0) (fx? (fxsra cnt pv-bits) (fxsll 1 shift)) ;; root overflow: grow the trie a level (mk-pvec (fx+ cnt 1) (fx+ shift pv-bits) (vector (pvec-root p) (pv-new-path shift tail-node)) (vector x) #f) (mk-pvec (fx+ cnt 1) shift (pv-push-tail cnt shift (pvec-root p) tail-node) (vector x) #f)))))) (define (pv-assoc-trie level node i x) (if (fx=? level 0) (vec-set node (fxand i pv-mask) x) (let ((subidx (fxand (fxsra i level) pv-mask))) (vec-set node subidx (pv-assoc-trie (fx- level pv-bits) (vector-ref node subidx) i x))))) (define (pvec-assoc p i x) ; i in [0,count]; =count appends (let ((i (->idx i)) (cnt (pvec-cnt p))) (cond ((fx=? i cnt) (pvec-conj p x)) ((and (fx>=? i 0) (fx=? i (pv-tailoff cnt)) (mk-pvec cnt (pvec-shift p) (pvec-root p) (vec-set (pvec-tail p) (fxand i pv-mask) x) #f) (mk-pvec cnt (pvec-shift p) (pv-assoc-trie (pvec-shift p) (pvec-root p) i x) (pvec-tail p) #f))) (else (jolt-throw (jolt-host-throwable "java.lang.IndexOutOfBoundsException" "vector index out of bounds")))))) (define (pvec-peek p) (let ((n (pvec-cnt p))) (if (fx=? n 0) jolt-nil (pvec-nth-d p (fx- n 1) jolt-nil)))) ;; pop the last trie chunk back into the tail; #f means the subtree emptied. (define (pv-pop-tail cnt level node) (let ((subidx (fxand (fxsra (fx- cnt 2) level) pv-mask))) (cond ((fx>? level pv-bits) (let ((newchild (pv-pop-tail cnt (fx- level pv-bits) (vector-ref node subidx)))) (cond ((and (not newchild) (fx=? subidx 0)) #f) (newchild (vec-set node subidx newchild)) (else (vec-take node subidx))))) ((fx=? subidx 0) #f) (else (vec-take node subidx))))) (define (pvec-pop p) (let ((cnt (pvec-cnt p)) (shift (pvec-shift p))) (cond ((fx=? cnt 0) (error 'pop "can't pop empty vector")) ((fx=? cnt 1) empty-pvec) ((fx>? (fx- cnt (pv-tailoff cnt)) 1) (mk-pvec (fx- cnt 1) shift (pvec-root p) (vec-drop-last (pvec-tail p)) #f)) (else (let* ((new-tail (pv-chunk-for p (fx- cnt 2))) (popped (pv-pop-tail cnt shift (pvec-root p))) (new-root (or popped pv-empty-node))) (if (and (fx>? shift pv-bits) (fxvector xs))) (define (make-map-entry k v) (make-pvec (vector k v) #t)) (define (jolt-map-entry? x) (and (pvec? x) (pvec-ent x) #t)) ;; ============================================================================ ;; bitmap HAMT — keys hashed by jolt-hash, leaves compared by jolt= ;; arr slot is one of: leaf (cons k v) | hnode (branch) | hcoll (hash bucket) ;; ============================================================================ (define-record-type hnode (fields bm arr) (nongenerative chez-hnode-v1)) (define-record-type hcoll (fields hash alist) (nongenerative chez-hcoll-v1)) (define empty-hnode (make-hnode 0 (vector))) (define hmask #x3FFFFFFFFFFFFFF) ; 58-bit non-negative hash window (define max-shift 55) ;; bitwise-and (not fxand): jolt-hash is set!-decorated per type (records/inst/ ;; sorted return their own hash) and Chez's equal-hash can yield a BIGNUM, so a ;; key's hash isn't guaranteed to be a fixnum. Masking with the 58-bit window via ;; the generic bitwise-and always lands in fixnum range for the HAMT's fx slicing. (define (key-hash k) (bitwise-and (jolt-hash k) hmask)) (define (chunk h shift) (fxand (fxsra h shift) 31)) (define (bitpos h shift) (fxsll 1 (chunk h shift))) (define (popcount n) (let loop ((n n) (c 0)) (if (fx=? n 0) c (loop (fxand n (fx- n 1)) (fx+ c 1))))) (define (arr-index bm bit) (popcount (fxand bm (fx- bit 1)))) ;; jolt= alist ops (for hash-collision buckets) (define (assoc-jolt k al) (cond ((null? al) #f) ((jolt= (caar al) k) (car al)) (else (assoc-jolt k (cdr al))))) (define (alist-replace k v al) (if (jolt= (caar al) k) (cons (cons k v) (cdr al)) (cons (car al) (alist-replace k v (cdr al))))) (define (alist-remove k al) (cond ((null? al) '()) ((jolt= (caar al) k) (cdr al)) (else (cons (car al) (alist-remove k (cdr al)))))) ;; split two leaves that collided at `shift` into a subtree (or hcoll if the ;; full hashes are equal / the hash is exhausted). (define (split-leaf shift ek ev h k v) (let ((eh (key-hash ek))) (if (or (fx>? shift max-shift) (fx=? eh h)) (make-hcoll h (list (cons ek ev) (cons k v))) (let ((ei (chunk eh shift)) (ni (chunk h shift))) (if (fx=? ei ni) (make-hnode (fxsll 1 ei) (vector (split-leaf (fx+ shift 5) ek ev h k v))) (let ((eb (fxsll 1 ei)) (nb (fxsll 1 ni))) (if (fx= 16 in the reference). Clojure 1.13 ;; raised the limit to 64 for maps whose keys are ALL keywords (the common ;; keyword-map case); mixed-key maps still cap at 8. (define array-map-limit 8) (define array-map-limit-kw 64) (define (all-keywords? ks) (or (null? ks) (and (keyword? (car ks)) (all-keywords? (cdr ks))))) ;; Should a map of `cnt` entries with insertion order `ord` stay in array mode ;; when key `k` is added? Under 8 always; a keyword-only map (existing keys + the ;; new key all keywords) grows to 64; otherwise it caps at 8. (define (pmap-array-keep? cnt ord k) (cond ((fx=? cnt array-map-limit-kw) #f) ((and (keyword? k) (all-keywords? ord)) #t) (else #f))) (define (append-key ord k) (append ord (list k))) (define (remove-key ord k) (let loop ((o ord)) (cond ((null? o) '()) ((jolt= (car o) k) (cdr o)) (else (cons (car o) (loop (cdr o))))))) ;; growth rule (PersistentArrayMap.assoc): a new key appends to the order while in ;; array mode under the limit; otherwise the result is hash-ordered. Replacing an ;; existing key (or assoc onto an already-hash map) keeps the current order. (define (pmap-assoc m k v) (let* ((added (box #f)) (r (node-assoc (pmap-root m) 0 (key-hash k) k v added)) (cnt (pmap-cnt m)) (ord (pmap-order m))) (if (unbox added) (if (and ord (pmap-array-keep? cnt ord k)) (make-pmap r (fx+ cnt 1) (append-key ord k)) (make-pmap r (fx+ cnt 1) #f)) (make-pmap r cnt ord)))) ;; force-ordered / force-hash inserts for rebuilding a map whose final mode is ;; already decided (array-map ctor, transient persistent!). (define (pmap-put-ordered m k v) (let* ((added (box #f)) (r (node-assoc (pmap-root m) 0 (key-hash k) k v added))) (if (unbox added) (make-pmap r (fx+ (pmap-cnt m) 1) (append-key (or (pmap-order m) '()) k)) (make-pmap r (pmap-cnt m) (pmap-order m))))) (define (pmap-put-hash m k v) (let* ((added (box #f)) (r (node-assoc (pmap-root m) 0 (key-hash k) k v added))) (make-pmap r (if (unbox added) (fx+ (pmap-cnt m) 1) (pmap-cnt m)) #f))) (define (pmap->hash m) (if (pmap-order m) (make-pmap (pmap-root m) (pmap-cnt m) #f) m)) (define (pmap-dissoc m k) (let* ((removed (box #f)) (r (node-dissoc (pmap-root m) 0 (key-hash k) k removed)) (ord (pmap-order m))) (if (unbox removed) (make-pmap r (fx- (pmap-cnt m) 1) (if ord (remove-key ord k) #f)) m))) (define (pmap-get m k default) (node-get (pmap-root m) 0 (key-hash k) k default)) (define (pmap-contains? m k) (not (eq? pmap-absent (node-get (pmap-root m) 0 (key-hash k) k pmap-absent)))) ;; The universal fold idiom across the runtime is `(pmap-fold m (lambda (k v a) ;; (cons ... a)) '())`, which accumulates in REVERSE visitation order. So that this ;; reconstructs the map's INSERTION order, pmap-fold visits an array-mode map's keys ;; in reverse insertion order; a hash-mode map visits HAMT order (its iteration ;; order is unspecified, so reverse-of-HAMT is equivalent and matches prior ;; behaviour). Use pmap-fold-fwd when building a value directly in iteration order. (define (pmap-fold m proc acc) (let ((ord (pmap-order m))) (if ord (fold-right (lambda (k a) (proc k (pmap-get m k jolt-nil) a)) acc ord) ; visits last->first (node-fold (pmap-root m) proc acc)))) ;; visit entries in iteration (insertion) order — for code that builds a new map / ;; ordered value directly rather than via cons-accumulation. (define (pmap-fold-fwd m proc acc) (let ((ord (pmap-order m))) (if ord (let loop ((ks ord) (a acc)) (if (null? ks) a (loop (cdr ks) (proc (car ks) (pmap-get m (car ks) jolt-nil) a)))) (node-fold (pmap-root m) proc acc)))) ;; map LITERAL ({...}): array map up to 8 entries (64 if keyword-only, per 1.13), ;; hash map beyond (RT.map). (define (jolt-hash-map . kvs) (let loop ((m empty-pmap) (kvs kvs)) (cond ((null? kvs) (let ((cnt (pmap-cnt m)) (ord (pmap-order m))) (if (fx>? cnt (if (all-keywords? ord) array-map-limit-kw array-map-limit)) (pmap->hash m) m))) ((null? (cdr kvs)) (error 'hash-map "odd number of map literal entries")) (else (loop (pmap-put-ordered m (car kvs) (cadr kvs)) (cddr kvs)))))) ;; array-map ctor: insertion-ordered regardless of size (createAsIfByAssoc). (define (jolt-array-map-build kvs) (let loop ((m empty-pmap) (kvs kvs)) (cond ((null? kvs) m) ((null? (cdr kvs)) (error 'array-map "odd number of map entries")) (else (loop (pmap-put-ordered m (car kvs) (cadr kvs)) (cddr kvs)))))) ;; hash-map ctor: hash order (PersistentHashMap). (define (jolt-hash-map-build kvs) (let loop ((m empty-pmap-hash) (kvs kvs)) (cond ((null? kvs) m) ((null? (cdr kvs)) (error 'hash-map "odd number of map entries")) (else (loop (pmap-put-hash m (car kvs) (cadr kvs)) (cddr kvs)))))) (define-record-type pset (fields m) (nongenerative chez-pset-v1)) (define empty-pset (make-pset empty-pmap-hash)) ; sets are hash-ordered (define (pset-conj s e) (if (pmap-contains? (pset-m s) e) s (make-pset (pmap-assoc (pset-m s) e e)))) (define (pset-disj s e) (make-pset (pmap-dissoc (pset-m s) e))) (define (pset-contains? s e) (pmap-contains? (pset-m s) e)) (define (pset-count s) (pmap-cnt (pset-m s))) (define (pset-fold s proc acc) (pmap-fold (pset-m s) (lambda (k v a) (proc k a)) acc)) (define (jolt-hash-set . xs) (let loop ((s empty-pset) (xs xs)) (if (null? xs) s (loop (pset-conj s (car xs)) (cdr xs))))) ;; ============================================================================ ;; leaf ops the emitter lowers core/clojure fns to (mirrors native-ops) ;; ============================================================================ (define (jolt-conj1 coll x) (cond ((pvec? coll) (pvec-conj coll x)) ; nil is a valid vector/set element ((pset? coll) (pset-conj coll x)) ;; a list/seq conjs by PREPENDING (seq.ss: cseq / empty-list). conj onto a ;; list stays a list, conj onto a lazy/realized seq yields a seq cell (a ;; Cons) — list?-preserving. ((cseq? coll) (if (cseq-list? coll) (cseq-list x coll) (cseq-realized x coll))) ((empty-list-t? coll) (cseq-list x jolt-nil)) ((pmap? coll) (cond ((jolt-nil? x) coll) ; (conj m nil) = m ((pmap? x) (pmap-fold-fwd x (lambda (k v m) (pmap-assoc m k v)) coll)) ; merge in x's order ((and (pvec? x) (fx=? 2 (pvec-count x))) (pmap-assoc coll (pvec-nth-d x 0 jolt-nil) (pvec-nth-d x 1 jolt-nil))) (else (error 'conj "conj on a map expects a [k v] pair or a map")))) ((rec-coll-method coll "cons") => (lambda (m) (jolt-invoke m coll x))) (else (error 'conj "unsupported collection")))) ;; (conj) -> []; (conj nil a b ...) builds a list (conj prepending -> (b a)). (define (jolt-conj . args) (if (null? args) (jolt-vector) (let ((coll (car args)) (xs (cdr args))) (cond ;; 1-arity returns the coll untouched — (conj nil) is nil ((null? xs) coll) ((jolt-nil? coll) (fold-left jolt-conj1 jolt-empty-list xs)) (else (meta-carry coll (fold-left jolt-conj1 coll xs))))))) ;; A host shim registers a type's get via register-get-arm! (handler: (coll k d) -> ;; value) instead of set!-wrapping jolt-get — disjoint coll types, checked before the ;; base map/set/vec/string cases (cf. register-hash-arm!). (define jolt-get-arms '()) (define (register-get-arm! pred handler) (set! jolt-get-arms (cons (cons pred handler) jolt-get-arms))) (define (jolt-get-base coll k d) (cond ((pmap? coll) (pmap-get coll k d)) ((pset? coll) (if (pset-contains? coll k) k d)) ((pvec? coll) (pvec-nth-d coll k d)) ((string? coll) (let ((i (->idx k))) (if (and (fixnum? i) (fx>=? i 0) (fxidx i))) (cond ((jolt-nil? coll) jolt-nil) ; RT.nth(nil, i) is nil at any index ((pvec? coll) (let ((v (pvec-v coll))) (if (and (fx>=? i 0) (fx=? i 0) (fx (lambda (m) (jolt-invoke m coll i))) (else (error 'nth "unsupported collection"))))) ((coll i d) (jolt-nth-nil-idx! i) (let ((i (->idx i))) (cond ((jolt-nil? coll) d) ; RT.nth(nil, i, notFound) is notFound ((pvec? coll) (pvec-nth-d coll i d)) ((string? coll) (if (and (fx>=? i 0) (fx (lambda (m) (jolt-invoke m coll i d))) (else d)))))) ;; a count is an exact integer (JVM parity: count returns a long). jolt= is ;; exactness-aware, so this must be exact to match an exact integer literal: ;; (= 2 (count m)) -> 2 vs exact 2 -> true. (define (jolt-count coll) (begin (cond ((pvec? coll) (pvec-count coll)) ((pmap? coll) (pmap-cnt coll)) ((pset? coll) (pset-count coll)) ((string? coll) (string-length coll)) ((jolt-nil? coll) 0) ((empty-list-t? coll) 0) ((cseq? coll) (let loop ((s coll) (n 0)) ; walk (forces a finite seq) (if (jolt-nil? s) n (loop (jolt-seq (seq-more s)) (fx+ n 1))))) ((rec-coll-method coll "count") => (lambda (m) (jolt-invoke m coll))) (else (error 'count "uncountable"))))) (define (jolt-assoc1 coll k v) (cond ((pmap? coll) (pmap-assoc coll k v)) ((pvec? coll) (pvec-assoc coll k v)) ((jolt-nil? coll) (pmap-assoc empty-pmap k v)) ((rec-coll-method coll "assoc") => (lambda (m) (jolt-invoke m coll k v))) (else (error 'assoc "unsupported collection")))) (define (jolt-assoc coll . kvs) (meta-carry coll (let loop ((coll coll) (kvs kvs)) (cond ((null? kvs) coll) ((null? (cdr kvs)) (error 'assoc "assoc expects an even number of key/vals")) (else (loop (jolt-assoc1 coll (car kvs) (cadr kvs)) (cddr kvs))))))) (define (jolt-dissoc coll . ks) (cond ((jolt-nil? coll) jolt-nil) ((pmap? coll) (meta-carry coll (fold-left pmap-dissoc coll ks))) (else (error 'dissoc "unsupported collection")))) (define (jolt-contains? coll k) (cond ((pmap? coll) (pmap-contains? coll k)) ((pset? coll) (pset-contains? coll k)) ((pvec? coll) (let ((k (->idx k))) (and (fixnum? k) (fx>=? k 0) (fx= k 0) (< k (string-length coll))) (jolt-throw (jolt-host-throwable "java.lang.IllegalArgumentException" "contains? not supported on type: java.lang.String")))) ((or (cseq? coll) (empty-list-t? coll) (number? coll) (boolean? coll) (keyword? coll) (jolt-symbol? coll) (char? coll)) (jolt-throw (jolt-host-throwable "java.lang.IllegalArgumentException" (string-append "contains? not supported on type: " (guard (e (#t "?")) (jolt-class-name coll)))))) (else #f))) (define (jolt-empty? coll) (cond ((jolt-nil? coll) #t) ((pvec? coll) (fx=? 0 (pvec-count coll))) ((pmap? coll) (fx=? 0 (pmap-cnt coll))) ((pset? coll) (fx=? 0 (pset-count coll))) ((string? coll) (fx=? 0 (string-length coll))) ((empty-list-t? coll) #t) ((cseq? coll) #f) ; a cseq is non-empty by construction (else (error 'empty? "unsupported collection")))) (define (jolt-stack-throw coll) (jolt-throw (jolt-host-throwable "java.lang.ClassCastException" (string-append "class " (guard (e (#t "?")) (jolt-class-name coll)) " cannot be cast to class clojure.lang.IPersistentStack")))) (define (jolt-peek coll) (cond ((pvec? coll) (pvec-peek coll)) ;; list peek = first; a non-list seq (range, a rest chain) is not an ;; IPersistentStack on the JVM ((and (cseq? coll) (cseq-list? coll)) (jolt-first coll)) ((empty-list-t? coll) (jolt-first coll)) ((jolt-nil? coll) jolt-nil) (else (jolt-stack-throw coll)))) (define (jolt-pop coll) (cond ((jolt-nil? coll) jolt-nil) ; RT.pop(nil) is nil ((pvec? coll) (meta-carry coll (pvec-pop coll))) ((and (cseq? coll) (cseq-list? coll)) (meta-carry coll (jolt-rest coll))) ((empty-list-t? coll) (error 'pop "can't pop empty list")) (else (jolt-stack-throw coll)))) ;; ============================================================================ ;; equality / hash hooks called from values.ss (jolt=2 / jolt-hash) ;; ============================================================================ (define (jolt-coll? x) (or (pvec? x) (pmap? x) (pset? x))) (define (jolt-coll=? a b) (cond ((and (pvec? a) (pvec? b)) (let ((va (pvec-v a)) (vb (pvec-v b))) (and (fx=? (vector-length va) (vector-length vb)) (let loop ((i 0)) (or (fx=? i (vector-length va)) (and (jolt= (vector-ref va i) (vector-ref vb i)) (loop (fx+ i 1)))))))) ((and (pmap? a) (pmap? b)) (and (fx=? (pmap-cnt a) (pmap-cnt b)) (pmap-fold a (lambda (k v ok) (and ok (jolt= (pmap-get b k pmap-absent) v))) #t))) ((and (pset? a) (pset? b)) (and (fx=? (pset-count a) (pset-count b)) (pset-fold a (lambda (e ok) (and ok (pset-contains? b e))) #t))) (else #f))) (define (jolt-coll-hash x) (cond ((pvec? x) (let ((v (pvec-v x))) (let loop ((i 0) (h 1)) (if (fx=? i (vector-length v)) (bitwise-and h hmask) (loop (fx+ i 1) (bitwise-and (+ (* 31 h) (key-hash (vector-ref v i))) hmask)))))) ;; maps/sets hash order-independently (sum), consistent with unordered = ((pmap? x) (bitwise-and (pmap-fold x (lambda (k v a) (+ a (fxxor (key-hash k) (key-hash v)))) 0) hmask)) ((pset? x) (bitwise-and (pset-fold x (lambda (e a) (+ a (key-hash e))) 0) hmask))))