feat: core.async Phase 2 — dynamic var binding conveyance
Jolt's dynamic-var binding stack was a single global array, so concurrent go blocks interleaved each other's bindings and a go block didn't see the bindings in effect when it was spawned. Move the binding stack into Janet's fiber-local dyn (:jolt/binding-stack): each fiber (go block) lazily gets its own array, so bindings can't interleave. Janet ev/go fibers inherit the parent's dyn, but go-spawn now snapshots the binding stack at spawn time and installs a private copy in the new fiber — Clojure binding conveyance. A go block's own (binding ...) shadows the conveyed frame. types.janet: cur-binding-stack / snapshot-bindings / install-bindings; var-get/ var-set/push/pop use the fiber-local stack. async.janet: go-spawn conveys. spec: core.async/binding-conveyance (4 cases — conveyance, isolation, no leak to root, inner shadowing). jpm test green.
This commit is contained in:
parent
7d1e1f42e1
commit
e8cb4605ac
4 changed files with 52 additions and 9 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
{"_type":"issue","id":"jolt-alz","title":"CRITICAL: self-referential lazy-seq/lazy-cat yields only literal prefix","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:13Z","created_by":"Yogthos","updated_at":"2026-06-04T18:09:15Z","closed_at":"2026-06-04T18:09:15Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"jolt-alz","title":"CRITICAL: self-referential lazy-seq/lazy-cat yields only literal prefix","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:13Z","created_by":"Yogthos","updated_at":"2026-06-04T18:09:15Z","closed_at":"2026-06-04T18:09:15Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"jolt-wec","title":"CRITICAL: multi-collection map returns unrealized thunk","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:12Z","created_by":"Yogthos","updated_at":"2026-06-04T18:06:02Z","closed_at":"2026-06-04T18:06:02Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"jolt-wec","title":"CRITICAL: multi-collection map returns unrealized thunk","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:12Z","created_by":"Yogthos","updated_at":"2026-06-04T18:06:02Z","closed_at":"2026-06-04T18:06:02Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"jolt-5i5","title":"core.async Phase 2: dynamic var binding conveyance","description":"Snapshot/restore the global binding-stack per fiber so dynamic var bindings don't interleave across concurrent go blocks (Clojure binding conveyance).","status":"open","priority":2,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-06-05T18:55:08Z","created_by":"Yogthos","updated_at":"2026-06-05T18:55:08Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"jolt-5i5","title":"core.async Phase 2: dynamic var binding conveyance","description":"Snapshot/restore the global binding-stack per fiber so dynamic var bindings don't interleave across concurrent go blocks (Clojure binding conveyance).","status":"open","priority":2,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-06-05T18:55:08Z","created_by":"Yogthos","updated_at":"2026-06-05T18:55:08Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"jolt-tkw","title":"core.async on Janet fibers — Phase 1: API layer","description":"Implement clojure.core.async API (chan/go/\u003c!/\u003e!/\u003c!!/\u003e!!/close!/alts!/timeout/put!/take!/buffers) on top of Janet ev/ channels and fibers. go = run body in a fiber (stackful, no CPS macro needed).","status":"open","priority":2,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-06-05T18:55:07Z","created_by":"Yogthos","updated_at":"2026-06-05T18:55:07Z","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"jolt-tkw","title":"core.async on Janet fibers — Phase 1: API layer","description":"Implement clojure.core.async API (chan/go/\u003c!/\u003e!/\u003c!!/\u003e!!/close!/alts!/timeout/put!/take!/buffers) on top of Janet ev/ channels and fibers. go = run body in a fiber (stackful, no CPS macro needed).","status":"closed","priority":2,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-06-05T18:55:07Z","created_by":"Yogthos","updated_at":"2026-06-05T19:17:25Z","closed_at":"2026-06-05T19:17:25Z","close_reason":"Phase 1 complete: chan/go/\u003c!/\u003e!/close!/alts!/timeout/put!/take! on Janet fibers; two-channel design (values + done) gives drain-then-nil with no leaked fibers; 16 spec cases","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"jolt-lko","title":"clojure-test-suite: strictness — throw on malformed calls like Clojure","description":"Make jolt throw where Clojure throws (bad-shape conj!/assoc!, arithmetic on non-numbers, out-of-range indices, etc.) to satisfy the suite's ~292 p/thrown? assertions. Targeted input validation across affected fns; guard against regressing jolt's spec/conformance.","status":"closed","priority":2,"issue_type":"task","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T14:45:20Z","created_by":"Yogthos","updated_at":"2026-06-05T18:07:56Z","started_at":"2026-06-05T14:45:39Z","closed_at":"2026-06-05T18:07:56Z","close_reason":"Implemented Clojure-strict argument validation across ~30 fns; suite pass 3691-\u003e3898, p/thrown? bucket 292-\u003e~60 (remainder is platform: int 32-bit range, subs Unicode, min-key NaN, subvec floats, map-entry-as-2-vector)","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"jolt-lko","title":"clojure-test-suite: strictness — throw on malformed calls like Clojure","description":"Make jolt throw where Clojure throws (bad-shape conj!/assoc!, arithmetic on non-numbers, out-of-range indices, etc.) to satisfy the suite's ~292 p/thrown? assertions. Targeted input validation across affected fns; guard against regressing jolt's spec/conformance.","status":"closed","priority":2,"issue_type":"task","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T14:45:20Z","created_by":"Yogthos","updated_at":"2026-06-05T18:07:56Z","started_at":"2026-06-05T14:45:39Z","closed_at":"2026-06-05T18:07:56Z","close_reason":"Implemented Clojure-strict argument validation across ~30 fns; suite pass 3691-\u003e3898, p/thrown? bucket 292-\u003e~60 (remainder is platform: int 32-bit range, subs Unicode, min-key NaN, subvec floats, map-entry-as-2-vector)","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"jolt-kxb","title":"clojure-test-suite: transducers don't short-circuit over infinite seqs (hangs)","description":"(into [] (take 5) (range)) and similar transducer-over-infinite forms hang jolt's eager evaluator (no reduced/early-termination). 7 suite files time out. Make transducing honor reduced.","status":"closed","priority":2,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:18Z","created_by":"Yogthos","updated_at":"2026-06-05T13:18:37Z","started_at":"2026-06-05T13:09:59Z","closed_at":"2026-06-05T13:18:37Z","close_reason":"reduce-with-reduced steps lazy seqs incrementally; transducers short-circuit over infinite seqs","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"jolt-kxb","title":"clojure-test-suite: transducers don't short-circuit over infinite seqs (hangs)","description":"(into [] (take 5) (range)) and similar transducer-over-infinite forms hang jolt's eager evaluator (no reduced/early-termination). 7 suite files time out. Make transducing honor reduced.","status":"closed","priority":2,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:18Z","created_by":"Yogthos","updated_at":"2026-06-05T13:18:37Z","started_at":"2026-06-05T13:09:59Z","closed_at":"2026-06-05T13:18:37Z","close_reason":"reduce-with-reduced steps lazy seqs incrementally; transducers short-circuit over infinite seqs","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"jolt-js6","title":"Port clojure-test-suite as integration battery","description":"Build a minimal clojure.test + portability shim so Jolt can load the external ~/src/clojure-test-suite (240 per-fn .cljc files in clojure.test format). Add a baseline-guarded integration runner following the jank-conformance pattern (skip if absent).","status":"closed","priority":2,"issue_type":"task","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T12:22:01Z","created_by":"Yogthos","updated_at":"2026-06-05T13:05:04Z","started_at":"2026-06-05T12:22:14Z","closed_at":"2026-06-05T13:05:04Z","close_reason":"Ported clojure-test-suite as baseline-guarded integration battery; surfaced+fixed 2 evaluator macro bugs","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"jolt-js6","title":"Port clojure-test-suite as integration battery","description":"Build a minimal clojure.test + portability shim so Jolt can load the external ~/src/clojure-test-suite (240 per-fn .cljc files in clojure.test format). Add a baseline-guarded integration runner following the jank-conformance pattern (skip if absent).","status":"closed","priority":2,"issue_type":"task","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T12:22:01Z","created_by":"Yogthos","updated_at":"2026-06-05T13:05:04Z","started_at":"2026-06-05T12:22:14Z","closed_at":"2026-06-05T13:05:04Z","close_reason":"Ported clojure-test-suite as baseline-guarded integration battery; surfaced+fixed 2 evaluator macro bugs","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,15 @@
|
||||||
# Run thunk (a jolt 0-arg closure, directly callable) in a fiber; return a
|
# Run thunk (a jolt 0-arg closure, directly callable) in a fiber; return a
|
||||||
# buffered(1) channel that conveys its value once, then closes. A nil result
|
# buffered(1) channel that conveys its value once, then closes. A nil result
|
||||||
# just closes. Buffered(1) so a fire-and-forget go leaves no parked fiber.
|
# just closes. Buffered(1) so a fire-and-forget go leaves no parked fiber.
|
||||||
|
#
|
||||||
|
# The dynamic-var bindings in effect at spawn time are conveyed into the fiber
|
||||||
|
# (Clojure binding conveyance): we snapshot them here (on the spawning fiber)
|
||||||
|
# and install a private copy inside the new fiber before running the body.
|
||||||
(defn async-go-spawn [thunk]
|
(defn async-go-spawn [thunk]
|
||||||
|
(def snap (snapshot-bindings))
|
||||||
(def w (async-chan 1))
|
(def w (async-chan 1))
|
||||||
(ev/go (fn []
|
(ev/go (fn []
|
||||||
|
(install-bindings snap)
|
||||||
(def res (protect (thunk)))
|
(def res (protect (thunk)))
|
||||||
(when (and (in res 0) (not (nil? (in res 1))))
|
(when (and (in res 0) (not (nil? (in res 1))))
|
||||||
(async-give w (in res 1)))
|
(async-give w (in res 1)))
|
||||||
|
|
|
||||||
|
|
@ -40,17 +40,36 @@
|
||||||
# Var
|
# Var
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
(def- binding-stack @[]) # stack of {var → value} tables for thread-local bindings
|
# Dynamic-var binding stack. Stored fiber-locally (via Janet's dyn), so that
|
||||||
|
# concurrent go blocks — each a Janet fiber — don't interleave each other's
|
||||||
|
# dynamic bindings, and a go block conveys the bindings in effect when it was
|
||||||
|
# spawned (see snapshot-bindings/install-bindings). Each fiber lazily gets its
|
||||||
|
# own array on first use.
|
||||||
|
(defn cur-binding-stack []
|
||||||
|
(or (dyn :jolt/binding-stack)
|
||||||
|
(let [s @[]] (setdyn :jolt/binding-stack s) s)))
|
||||||
|
|
||||||
(defn push-thread-bindings
|
(defn push-thread-bindings
|
||||||
"Push a frame of dynamic var bindings. Takes a struct of var→value."
|
"Push a frame of dynamic var bindings. Takes a struct of var→value."
|
||||||
[bindings]
|
[bindings]
|
||||||
(array/push binding-stack bindings))
|
(array/push (cur-binding-stack) bindings))
|
||||||
|
|
||||||
(defn pop-thread-bindings
|
(defn pop-thread-bindings
|
||||||
"Pop the most recent frame of dynamic var bindings."
|
"Pop the most recent frame of dynamic var bindings."
|
||||||
[]
|
[]
|
||||||
(array/pop binding-stack))
|
(array/pop (cur-binding-stack)))
|
||||||
|
|
||||||
|
(defn snapshot-bindings
|
||||||
|
"Shallow copy of the current binding stack (frames are immutable value maps).
|
||||||
|
Captured by a go block at spawn time for binding conveyance."
|
||||||
|
[]
|
||||||
|
(array/slice (cur-binding-stack)))
|
||||||
|
|
||||||
|
(defn install-bindings
|
||||||
|
"Install a snapshot as this fiber's binding stack (a fresh copy, so the
|
||||||
|
fiber's own push/pop/var-set don't mutate the snapshot's frames array)."
|
||||||
|
[snap]
|
||||||
|
(setdyn :jolt/binding-stack (array/slice snap)))
|
||||||
|
|
||||||
(defn make-var
|
(defn make-var
|
||||||
"Create a new Jolt Var.
|
"Create a new Jolt Var.
|
||||||
|
|
@ -107,10 +126,11 @@
|
||||||
Otherwise return the root binding."
|
Otherwise return the root binding."
|
||||||
[v]
|
[v]
|
||||||
# walk binding stack top-down for this var
|
# walk binding stack top-down for this var
|
||||||
|
(def bs (cur-binding-stack))
|
||||||
(var result nil)
|
(var result nil)
|
||||||
(var i (dec (length binding-stack)))
|
(var i (dec (length bs)))
|
||||||
(while (>= i 0)
|
(while (>= i 0)
|
||||||
(let [frame (in binding-stack i)
|
(let [frame (in bs i)
|
||||||
val (get frame v)]
|
val (get frame v)]
|
||||||
(if (not (nil? val))
|
(if (not (nil? val))
|
||||||
(do
|
(do
|
||||||
|
|
@ -124,12 +144,13 @@
|
||||||
the innermost frame that binds it (matching Clojure, where var-set targets the
|
the innermost frame that binds it (matching Clojure, where var-set targets the
|
||||||
current binding); otherwise set the root."
|
current binding); otherwise set the root."
|
||||||
[v val]
|
[v val]
|
||||||
(var i (dec (length binding-stack)))
|
(def bs (cur-binding-stack))
|
||||||
|
(var i (dec (length bs)))
|
||||||
(var done false)
|
(var done false)
|
||||||
(while (and (not done) (>= i 0))
|
(while (and (not done) (>= i 0))
|
||||||
(let [frame (in binding-stack i)]
|
(let [frame (in bs i)]
|
||||||
(if (not (nil? (get frame v)))
|
(if (not (nil? (get frame v)))
|
||||||
(do (put binding-stack i (merge frame {v val})) (set done true))
|
(do (put bs i (merge frame {v val})) (set done true))
|
||||||
(-- i))))
|
(-- i))))
|
||||||
(unless done (put v :root val))
|
(unless done (put v :root val))
|
||||||
val)
|
val)
|
||||||
|
|
|
||||||
|
|
@ -53,3 +53,19 @@
|
||||||
"99" (a "(<! (go (try (<! (go 99)) (catch :default e -1))))")]
|
"99" (a "(<! (go (try (<! (go 99)) (catch :default e -1))))")]
|
||||||
["<! inside a nested fn called in a go"
|
["<! inside a nested fn called in a go"
|
||||||
"7" (a "(def c (chan)) (go (>! c 7)) (<! (go ((fn [] (<! c)))))")])
|
"7" (a "(def c (chan)) (go (>! c 7)) (<! (go ((fn [] (<! c)))))")])
|
||||||
|
|
||||||
|
# Dynamic-var binding conveyance (Phase 2): a go block sees the dynamic bindings
|
||||||
|
# in effect when it was spawned, concurrent go blocks don't interleave, and a
|
||||||
|
# go block's own binding shadows the conveyed one.
|
||||||
|
(defn- d [body] (string "(do " REQ "(def ^:dynamic *x* 0) " body ")"))
|
||||||
|
(defspec "core.async / binding conveyance"
|
||||||
|
["go conveys the binding"
|
||||||
|
"10" (d "(<! (binding [*x* 10] (go (<! (timeout 5)) *x*)))")]
|
||||||
|
["concurrent go blocks isolated"
|
||||||
|
"[:a :b]"
|
||||||
|
(d "(def ra (binding [*x* :a] (go (<! (timeout 20)) *x*))) (def rb (binding [*x* :b] (go (<! (timeout 5)) *x*))) [(<! ra) (<! rb)]")]
|
||||||
|
["binding doesn't leak to root"
|
||||||
|
"0" (d "(<! (binding [*x* 99] (go (<! (timeout 5))))) *x*")]
|
||||||
|
["go's own binding shadows conveyed"
|
||||||
|
":inner"
|
||||||
|
(d "(<! (binding [*x* :outer] (go (binding [*x* :inner] (<! (timeout 5)) *x*))))")])
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue