Chez concurrency pt.3: async agents (per-agent serialized dispatch)

Replace the Janet synchronous agent shim (agent = atom, send applies inline) with
JVM-style async agents: send/send-off enqueue an action and a single worker thread
per agent applies them in order; deref reads the current (maybe not-yet-updated)
state without blocking; await blocks until the queue drains. A validator rejection
or a thrown action puts the agent in an error state (agent-error) and halts the
queue; restart-agent clears it. send and send-off share one serialized worker (a
superset of the JVM's fixed/cached pool split). Native versions re-asserted in
post-prelude over the overlay; await/restart-agent are new.

Corpus: the two "send/send-off applies" cases do (send a f) (deref a) with no
await, so they now read state before the action runs — diverging like the JVM
(the suite was literally "synchronous shim"). Allowlisted on both gates; floors
-2 (zero-Janet 2569->2567, prelude 2559->2557). cli-test covers async agents via
await (ordered 100-send dispatch, error capture) — 49/49. Janet gate + JVM cert
green; 0 new divergences on either corpus.

jolt-byjr
This commit is contained in:
Yogthos 2026-06-20 14:22:37 -04:00
parent 48ed72974f
commit c719e54543
5 changed files with 120 additions and 7 deletions

View file

@ -76,7 +76,14 @@
["(require (quote [clojure.core.async :refer [chan go <! >! <!! alts!]])) (def x (chan)) (def y (chan)) (go (>! y :v)) (<!! (go (let [[v ch] (alts! [x y])] (and (= v :v) (= ch y)))))" "true"]
["(require (quote [clojure.core.async :refer [chan go go-loop <! >! <!! close!]])) (def c (chan 10 (map inc))) (go (>! c 1) (>! c 2) (>! c 3) (close! c)) (<!! (go-loop [o []] (let [v (<! c)] (if (nil? v) o (recur (conj o v))))))" "[2 3 4]"]
["(require (quote [clojure.core.async :refer [timeout <!!]])) (<!! (timeout 10)) :done" ":done"]
["(require (quote [clojure.core.async :refer [chan go <! >! <!!]])) (def ^:dynamic *x* 0) (<!! (binding [*x* 7] (go (<! (clojure.core.async/timeout 5)) *x*)))" "7"]])
["(require (quote [clojure.core.async :refer [chan go <! >! <!!]])) (def ^:dynamic *x* 0) (<!! (binding [*x* 7] (go (<! (clojure.core.async/timeout 5)) *x*)))" "7"]
# jolt-byjr: async agents (serialized per-agent dispatch); await for determinism.
["(deref (agent 0))" "0"]
["(let [a (agent 0)] (send-off a + 5) (await a) (deref a))" "5"]
["(let [a (agent 1)] (send a + 6) (await a) (deref a))" "7"]
["(let [a (agent 0)] (dotimes [_ 100] (send a inc)) (await a) (deref a))" "100"]
["(agent-error (agent 0))" ""]
["(let [a (agent 0)] (send a (fn [_] (throw (ex-info \"boom\" {})))) (await a) (boolean (agent-error a)))" "true"]])
(each [src want] cases
(def [code out err] (joltc src))

View file

@ -102,7 +102,12 @@
# 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})
"future-cancelled? after cancel" true
# agents are async on Chez (per-agent serialized dispatch) = JVM, not Janet's
# synchronous shim: (send a f) (deref a) reads state before the action runs, so
# these sync-shim cases diverge like the JVM. Per jvm-parity-north-star.
"send applies" true
"send-off applies" 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.
@ -305,8 +310,10 @@
# 2280->2295, 0 new divergences.
# jolt-byjr (real-thread futures/pmap on Chez): future-call resolves, so the
# future/deref/pmap cases run instead of crashing. 2534->2559, 0 new divergences.
# Then -2 when agents went async (the two "send/send-off applies" sync-shim cases
# match the JVM's async raciness and are allowlisted) -> 2557.
# Strided runs scale down.
(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "2559")))
(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_PRELUDE_FLOOR") "2557")))
(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)))

View file

@ -89,7 +89,13 @@
# 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})
"future-cancelled? after cancel" true
# agents are async on Chez (per-agent serialized dispatch) = JVM, not Janet's
# synchronous shim: (send a f) (deref a) reads the state BEFORE the action runs
# (use await to observe the result), so these sync-shim cases now diverge like
# the JVM. Deliberate per jvm-parity-north-star.
"send applies" true
"send-off applies" 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
@ -183,7 +189,10 @@
# 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") "2569")))
# 2569 after futures/pmap (jolt-byjr pt.1); -2 when agents went async (the two
# "send/send-off applies" sync-shim cases now match the JVM's async raciness and
# are allowlisted) -> 2567.
(def base-floor (scan-number (or (os/getenv "JOLT_CHEZ_ZJ_FLOOR") "2567")))
(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)))