future/future-call run the body on a native thread (fork-thread) over the SAME heap — JVM semantics, not Janet's isolated-heap snapshot. deref blocks on a mutex+condition latch; timed (deref f ms val) uses an absolute deadline. promise is a real blocking promise (deref parks until deliver), replacing the Janet non-blocking atom shim. future?/future-done?/future-cancelled?/future-cancel /realized? are native (the overlay versions read Janet map keys); re-asserted in post-prelude over the overlay. pmap/pcalls/pvalues (overlay, over future) light up for free. Thread-safety this forces: - atoms get a per-atom mutex; swap!/swap-vals! are a JVM-style CAS loop (f runs outside the lock, so a watch/validator can deref the same atom); reset!/ compare-and-set! are atomic. - the dynamic binding stack becomes a Chez thread-parameter, so each future/thread has its own; Chez inherits it at fork, giving binding conveyance (the shim also installs an explicit snapshot). - Thread/sleep really sleeps now (a worker sleeping doesn't block the parent). Re-minted the seed: future-call now resolves at compile time, so pmap compiles to a var-deref instead of the host-static-call fallback that crashed. image.ss unchanged. Corpus: the 2 snapshot cases now match the JVM (shared) not Janet (isolated) — allowlisted on both Chez gates; the two racy future-cancel cases allowlisted; "promise undelivered" (blocks on JVM/Chez, profile :bucket :timeout) skipped like :throws. Zero-Janet corpus 2544 -> 2569, 0 new divergences, floor raised. Full Janet gate + JVM cert green. jolt-byjr
143 lines
6.4 KiB
Scheme
143 lines
6.4 KiB
Scheme
;; dynamic var binding (jolt-2o7x, Phase 2) — binding / with-bindings* / var-set /
|
|
;; thread-bound? / with-local-vars / with-redefs / bound-fn* / get-thread-bindings.
|
|
;;
|
|
;; A per-thread dynamic-binding stack: a list of frames, innermost (most recently
|
|
;; pushed) at the HEAD. Each frame is an alist of (var-cell . value) MUTABLE pairs
|
|
;; — so var-set can update the innermost binding in place (set-cdr!), matching
|
|
;; Clojure where var-set targets the current binding, not the root.
|
|
;;
|
|
;; The binding macro builds a frame as a jolt map (array-map of (var x) -> value);
|
|
;; push-thread-bindings folds it into the alist. Lookups walk frames by cell
|
|
;; IDENTITY (eq?) — vars are interned, so (var x) always yields the same cell, and
|
|
;; this sidesteps the seed's persistent-hash-map-can't-find-a-var-key quirk.
|
|
;;
|
|
;; var reads (var-deref in compiled code, jolt-var-get / deref on a cell) consult
|
|
;; the stack before falling back to the cell root. Loaded LAST (after vars.ss and
|
|
;; ns.ss) so it chains the fully-extended jolt-var-get and overrides rt.ss var-deref.
|
|
|
|
;; THREAD-LOCAL (jolt-byjr): a Chez thread parameter, so each OS thread (a future
|
|
;; / go block) has its own binding stack. Chez initializes a new thread's parameter
|
|
;; to the spawning thread's value at fork time, giving Clojure binding conveyance
|
|
;; for free (the future shim also installs an explicit snapshot, belt-and-suspenders).
|
|
(define dyn-binding-stack (make-thread-parameter '()))
|
|
|
|
;; find the innermost (cell . value) pair binding CELL, or #f.
|
|
(define (dyn-find-binding cell)
|
|
(let loop ((frames (dyn-binding-stack)))
|
|
(and (pair? frames)
|
|
(or (assq cell (car frames))
|
|
(loop (cdr frames))))))
|
|
|
|
;; a unique sentinel: distinguishes "no thread binding" from a binding whose
|
|
;; value happens to be jolt-nil.
|
|
(define dyn-no-binding (list 'no-binding))
|
|
(define (dyn-binding-value cell)
|
|
(if (pair? (dyn-binding-stack))
|
|
(let ((p (dyn-find-binding cell)))
|
|
(if p
|
|
(let ((val (cdr p)))
|
|
(if (var-cell? val) (jolt-var-get val) val)) ; nested var deref (Clojure)
|
|
dyn-no-binding))
|
|
dyn-no-binding))
|
|
|
|
;; push-thread-bindings: frame is a jolt map of var-cell -> value. Fold it into an
|
|
;; identity-keyed alist of mutable pairs and push.
|
|
(define (jolt-push-thread-bindings frame)
|
|
(dyn-binding-stack
|
|
(cons (pmap-fold frame (lambda (k v acc) (cons (cons k v) acc)) '())
|
|
(dyn-binding-stack)))
|
|
jolt-nil)
|
|
|
|
(define (jolt-pop-thread-bindings)
|
|
(when (pair? (dyn-binding-stack))
|
|
(dyn-binding-stack (cdr (dyn-binding-stack))))
|
|
jolt-nil)
|
|
|
|
;; get-thread-bindings: a jolt map of every currently-bound cell -> value,
|
|
;; innermost wins. Merge oldest-frame-first (the stack head is innermost). The
|
|
;; result can be re-pushed by with-bindings* / bound-fn*.
|
|
(define (jolt-get-thread-bindings)
|
|
(let loop ((frames (reverse (dyn-binding-stack))) (m (jolt-hash-map)))
|
|
(if (null? frames)
|
|
m
|
|
(loop (cdr frames)
|
|
(let frame-loop ((alist (car frames)) (m m))
|
|
(if (null? alist)
|
|
m
|
|
(frame-loop (cdr alist)
|
|
(pmap-assoc m (caar alist) (cdar alist)))))))))
|
|
|
|
;; __thread-bound? — single var; true iff it has a thread binding.
|
|
(define (jolt-thread-bound? v)
|
|
(and (var-cell? v) (dyn-find-binding v) #t))
|
|
|
|
;; var-set: update the innermost frame that binds v (in place); else set the root.
|
|
(define (jolt-var-set v val)
|
|
(if (var-cell? v)
|
|
(let ((p (dyn-find-binding v)))
|
|
(if p
|
|
(begin (set-cdr! p val) val)
|
|
(begin (var-cell-root-set! v val) (var-cell-defined?-set! v #t) val)))
|
|
(error #f "var-set: not a var" v)))
|
|
|
|
;; alter-var-root: atomically apply f to the current root plus args.
|
|
(define (jolt-alter-var-root v f . args)
|
|
(let ((new (apply jolt-invoke f (var-cell-root v) args)))
|
|
(var-cell-root-set! v new)
|
|
(var-cell-defined?-set! v #t)
|
|
new))
|
|
|
|
;; __local-var: a fresh free-standing var cell (not interned). with-local-vars
|
|
;; binds these as lexical locals; var-get/var-set read/write the root. Each gets a
|
|
;; unique name so two locals never compare/hash equal as map keys.
|
|
(define local-var-counter 0)
|
|
(define (jolt-local-var . args)
|
|
(set! local-var-counter (fx+ local-var-counter 1))
|
|
(make-var-cell "" (string-append "local-" (number->string local-var-counter))
|
|
(if (pair? args) (car args) jolt-nil)
|
|
#t))
|
|
|
|
;; --- chain the var-read paths onto the binding stack -------------------------
|
|
|
|
;; var-deref (rt.ss): the compiled-code read path for every clojure.core var
|
|
;; reference. Consult the stack first; fall straight back to the root (NOT through
|
|
;; jolt-var-get's unbound-error path) so undefined-var reads keep prior behaviour.
|
|
(define %dyn-rt-var-deref var-deref)
|
|
(set! var-deref
|
|
(lambda (ns name)
|
|
(let ((cell (jolt-var ns name)))
|
|
(let ((bv (dyn-binding-value cell)))
|
|
(if (eq? bv dyn-no-binding) (var-cell-root cell) bv)))))
|
|
|
|
;; jolt-var-get (vars.ss): the var-get fn + deref/@ on a cell. Stack first, then
|
|
;; the original (which errors on an unbound root, matching Clojure).
|
|
(define %dyn-var-get jolt-var-get)
|
|
(set! jolt-var-get
|
|
(lambda (v)
|
|
(if (var-cell? v)
|
|
(let ((bv (dyn-binding-value v)))
|
|
(if (eq? bv dyn-no-binding) (%dyn-var-get v) bv))
|
|
(%dyn-var-get v))))
|
|
|
|
;; var-cell keys hash/compare by ns/name (jolt=2 in vars.ss already compares
|
|
;; ns/name) — stable under root mutation, so a var works as a map key (with-redefs
|
|
;; builds (hash-map (var f) v); get-thread-bindings returns a var-keyed map).
|
|
(define %dyn-hash jolt-hash)
|
|
(set! jolt-hash
|
|
(lambda (x)
|
|
(if (var-cell? x)
|
|
(equal-hash (cons (var-cell-ns x) (var-cell-name x)))
|
|
(%dyn-hash x))))
|
|
|
|
;; --- bind the host seams the overlay references -----------------------------
|
|
(def-var! "clojure.core" "push-thread-bindings" jolt-push-thread-bindings)
|
|
(def-var! "clojure.core" "pop-thread-bindings" jolt-pop-thread-bindings)
|
|
(def-var! "clojure.core" "get-thread-bindings" jolt-get-thread-bindings)
|
|
(def-var! "clojure.core" "__thread-bound?" jolt-thread-bound?)
|
|
(def-var! "clojure.core" "var-set" jolt-var-set)
|
|
(def-var! "clojure.core" "alter-var-root" jolt-alter-var-root)
|
|
(def-var! "clojure.core" "__local-var" jolt-local-var)
|
|
;; re-assert var-get / deref to the new (stack-aware) closures (vars.ss captured
|
|
;; the pre-chain values).
|
|
(def-var! "clojure.core" "var-get" jolt-var-get)
|
|
(def-var! "clojure.core" "deref" jolt-deref)
|