Chez concurrency pt.1: real OS-thread futures + blocking promises (shared heap)

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
This commit is contained in:
Yogthos 2026-06-20 13:11:31 -04:00
parent a23095b502
commit ec30c9e405
10 changed files with 330 additions and 42 deletions

View file

@ -47,7 +47,28 @@
["(map eval [(quote (+ 1 1)) (quote (* 3 3))])" "(2 9)"]
["(defmacro add1 [x] (list (quote +) x 1)) (add1 10)" "11"]
["(defmacro twice [x] `(do ~x ~x)) (twice (+ 2 3))" "5"]
["(defmacro m [x] `(+ ~x 1)) (m (m (m 0)))" "3"]])
["(defmacro m [x] `(+ ~x 1)) (m (m (m 0)))" "3"]
# jolt-byjr: concurrency — futures/pmap/promise on real OS threads (shared heap).
["(deref (future (+ 1 2)))" "3"]
["@(future (* 6 7))" "42"]
["(deref (future (mapv inc [1 2 3])))" "[2 3 4]"]
["(let [f (future (+ 1 1))] [(deref f) (deref f)])" "[2 2]"]
["(future? (future 1))" "true"]
["(future? 42)" "false"]
["(let [f (future 1)] (deref f) (future-done? f))" "true"]
["(let [f (future 1)] (deref f) (realized? f))" "true"]
["(let [f (future 42)] (deref f) (deref f 1000 :nope))" "42"]
["(vec (pmap inc [1 2 3]))" "[2 3 4]"]
["(vec (pmap + [1 2 3] [4 5 6]))" "[5 7 9]"]
["(vec (pcalls (fn [] 1) (fn [] 2)))" "[1 2]"]
["(vec (pvalues (+ 1 2) (+ 3 4)))" "[3 7]"]
# shared heap = JVM semantics (NOT Janet's isolated-heap snapshot): a captured
# atom is shared, so the future's swap! is visible to the parent.
["(let [a (atom 0)] (deref (future (swap! a inc))) @a)" "1"]
["(let [a (atom 0)] (dorun (pmap (fn [_] (swap! a inc)) [1 2 3 4])) @a)" "4"]
# promise blocks until delivered (JVM), unlike the Janet atom-shim.
["(let [p (promise)] (deliver p 7) @p)" "7"]
["(let [p (promise)] (future (deliver p :hi)) @p)" ":hi"]])
(each [src want] cases
(def [code out err] (joltc src))

View file

@ -91,7 +91,22 @@
"atom?" true
# Same atom-class gap, via the folded-in conformance case (jolt-ohtd):
# (instance? clojure.lang.Atom (atom 1)).
"instance? Atom" true})
"instance? Atom" true
# concurrency (jolt-byjr): the Chez runtime now uses real OS-thread futures
# sharing the heap = JVM semantics, NOT Janet's isolated-heap snapshot. So a
# captured atom is shared (the corpus :expected is the Janet snapshot value).
# Deliberate per jvm-parity-north-star. Same allowlist as the zero-Janet gate.
"captured atom is snapshotted, not shared" true # Chez 1 (shared) vs Janet 0
"snapshot semantics" true # pmap: Chez 2 (shared) vs Janet 0
# future-cancel of a trivial body races the worker under real threads (cancel
# usually loses -> false), like the JVM; the Janet :expected relies on its
# cooperative scheduler. Flaky/divergent.
"cancel an in-flight future returns true" true
"future-cancelled? after cancel" true})
# Cases that BLOCK forever on a shared-heap / JVM host (profile.edn :bucket
# :timeout) — skip, like :throws, so a hung per-case process can't stall the gate.
(def skip-blocking {"promise undelivered" true})
(def ctx (d/make-ctx))
@ -130,7 +145,7 @@
(def t1 (os/clock))
(each row cases
(def {:expected e :actual a :label l} row)
(if (= e :throws)
(if (or (= e :throws) (get skip-blocking l))
nil # :throws error-semantics aren't modeled here; skip (counted out of run)
(let [src (string "(= " e " " a ")")
res (d/eval-e-with-prelude ctx src prelude-path)]

View file

@ -78,7 +78,24 @@
"inf inside coll" true
# #?@ reader-conditional splicing into the enclosing seq isn't supported yet
# (plain #? works); a single niche corpus case.
"reader cond splice" true})
"reader cond splice" true
# concurrency (jolt-byjr): Chez futures are real OS threads sharing the heap =
# JVM semantics, NOT Janet's isolated-heap snapshot. So a captured atom IS
# shared (the corpus :expected is the Janet snapshot value). Deliberate per the
# jvm-parity-north-star — Chez matches the JVM, Janet keeps the old snapshot.
"captured atom is snapshotted, not shared" true # Chez 1 (shared) vs Janet 0
"snapshot semantics" true # pmap: Chez 2 (shared) vs Janet 0
# future-cancel on a trivial body is timing-dependent under real threads: the
# future usually completes before the cancel (cancel -> false), like the JVM.
# The Janet :expected "true" relies on cooperative scheduling. Flaky/divergent.
"cancel an in-flight future returns true" true
"future-cancelled? after cancel" true})
# Cases that BLOCK forever on a shared-heap / JVM host (profile.edn :bucket
# :timeout) — skip them, like :throws: a single hung case would stall the whole
# batched process. (deref of an undelivered promise blocks on the JVM and now on
# Chez; Janet's non-blocking atom-shim returned nil.)
(def skip-blocking @{"promise undelivered" true})
(var pass 0)
(def crashes @[]) # nonzero chez exit (analyzer/emitter raised, or runtime gap)
@ -116,8 +133,8 @@
(def rows-by-idx @{})
(def pairs @[])
(eachp [i row] cases
(def {:expected e :actual a} row)
(if (= e :throws)
(def {:expected e :actual a :label l} row)
(if (or (= e :throws) (get skip-blocking l))
(++ throws)
(let [key (string i)]
(put rows-by-idx key row)
@ -166,7 +183,7 @@
# Regression floor: raise as the Chez-hosted compiler closes gaps. The gate fails
# on any NEW divergence or if pass drops below the floor. Strided runs scale to 0.
(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_ZJ_FLOOR") "2544")))
(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_ZJ_FLOOR") "2569")))
(def floor (if (os/getenv "JOLT_CORPUS_LIMIT") 0 base-floor))
(when (or (> (length diverged) 0) (< pass floor))
(printf "REGRESSION: pass %d < floor %d or %d new divergence(s)" pass floor (length diverged)))