feat: clojure.core.async on Janet fibers (Phase 1 — API layer)

Janet fibers are stackful coroutines, so a go block is just its body run in a
fiber that parks on channel ops by yielding to the event loop — the interpreter
call stack rides along, no CPS/state-machine transform. So <!/>! work anywhere
(inside try, nested fns, loops), unlike Clojure's go macro.

src/jolt/async.janet implements chan/chan?/close!/<!/>!/<!!/>!!/go/go-loop/
thread/alts!/timeout/put!/take! over ev/ channels and fibers, installed as the
clojure.core.async namespace (pre-populated in init, so require finds it).

A channel is a pair of ev/chans (:ch values + :done close-signal); a take is
(ev/select :ch :done), which drains buffered values before the close signal —
giving Clojure's drain-then-nil semantics without the buffer loss of
ev/chan-close, and with no leaked fibers (close! just closes :done).

Single-threaded cooperative scheduling: <! (park) and <!! (block) coincide.
Dynamic-var conveyance (Phase 2) and channel transducers (Phase 3) are TODO.

spec: test/spec/core-async-spec (16 cases — go/channels, buffering+close,
go-loop pipelines, alts!/timeout, parking inside try/nested-fn). jpm test green.
This commit is contained in:
Yogthos 2026-06-05 15:17:15 -04:00
parent 858c7fed14
commit 7d1e1f42e1
7 changed files with 212 additions and 0 deletions

View file

@ -5,6 +5,8 @@
{"_type":"issue","id":"jolt-x8s","title":"CRITICAL: collections not callable as IFn (vector/map/set)","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:14Z","created_by":"Yogthos","updated_at":"2026-06-04T17:50:54Z","closed_at":"2026-06-04T17:50:54Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-x8s","title":"CRITICAL: collections not callable as IFn (vector/map/set)","status":"closed","priority":1,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T17:46:14Z","created_by":"Yogthos","updated_at":"2026-06-04T17:50:54Z","closed_at":"2026-06-04T17:50:54Z","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-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-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-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}
@ -15,6 +17,7 @@
{"_type":"issue","id":"jolt-40n","title":"HIGH: update/update-in/assoc-in fail on map literals (struct)","status":"closed","priority":2,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:56Z","created_by":"Yogthos","updated_at":"2026-06-04T18:40:42Z","closed_at":"2026-06-04T18:40:42Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-40n","title":"HIGH: update/update-in/assoc-in fail on map literals (struct)","status":"closed","priority":2,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:56Z","created_by":"Yogthos","updated_at":"2026-06-04T18:40:42Z","closed_at":"2026-06-04T18:40:42Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-ytt","title":"HIGH: str semantics — (str nil)='nil' should be '', (str :b)='b' should be ':b'","status":"closed","priority":2,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:56Z","created_by":"Yogthos","updated_at":"2026-06-04T18:33:45Z","closed_at":"2026-06-04T18:33:45Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-ytt","title":"HIGH: str semantics — (str nil)='nil' should be '', (str :b)='b' should be ':b'","status":"closed","priority":2,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:56Z","created_by":"Yogthos","updated_at":"2026-06-04T18:33:45Z","closed_at":"2026-06-04T18:33:45Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-9sb","title":"HIGH: destructuring gaps (nested seq, :strs, nested map, fn-param)","status":"closed","priority":2,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:55Z","created_by":"Yogthos","updated_at":"2026-06-04T18:31:24Z","closed_at":"2026-06-04T18:31:24Z","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-9sb","title":"HIGH: destructuring gaps (nested seq, :strs, nested map, fn-param)","status":"closed","priority":2,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-04T18:09:55Z","created_by":"Yogthos","updated_at":"2026-06-04T18:31:24Z","closed_at":"2026-06-04T18:31:24Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-x3v","title":"core.async Phase 3: channel transducers","description":"Support (chan n xform) — apply a transducer to values flowing through a channel, plus dropping/sliding buffers.","status":"open","priority":3,"issue_type":"feature","owner":"yogthos@gmail.com","created_at":"2026-06-05T18:55:09Z","created_by":"Yogthos","updated_at":"2026-06-05T18:55:09Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-dxx","title":"clojure-test-suite: merge nil/empty + collection-key semantics","description":"merge.cljc has 18 fail / 11 err — highest in suite. Investigate nil/empty-map merge return values and collection-key behavior.","status":"closed","priority":3,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:17Z","created_by":"Yogthos","updated_at":"2026-06-05T13:22:29Z","started_at":"2026-06-05T13:18:50Z","closed_at":"2026-06-05T13:22:29Z","close_reason":"merge follows Clojure (when some identity) (reduce conj ...) semantics; nil/empty/conj-entry cases fixed","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-dxx","title":"clojure-test-suite: merge nil/empty + collection-key semantics","description":"merge.cljc has 18 fail / 11 err — highest in suite. Investigate nil/empty-map merge return values and collection-key behavior.","status":"closed","priority":3,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:17Z","created_by":"Yogthos","updated_at":"2026-06-05T13:22:29Z","started_at":"2026-06-05T13:18:50Z","closed_at":"2026-06-05T13:22:29Z","close_reason":"merge follows Clojure (when some identity) (reduce conj ...) semantics; nil/empty/conj-entry cases fixed","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-1uw","title":"clojure-test-suite: transient strictness gaps","description":"Transient ops (conj!/assoc!/disj!/pop!) don't throw on misuse (bad shapes, use-after-persistent!) where Clojure does. Surfaced by conj_bang/assoc_bang/pop_bang/persistent_bang/transient suite files (~50 fails). Lenient behavior; decide whether to enforce.","status":"closed","priority":3,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:16Z","created_by":"Yogthos","updated_at":"2026-06-05T13:29:38Z","started_at":"2026-06-05T13:22:51Z","closed_at":"2026-06-05T13:29:38Z","close_reason":"Fixed real gaps: invokable transients, assoc! odd args, use-after-persistent! invalidation, pop!-empty. Bad-shape type strictness left as accepted lenient divergence.","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-1uw","title":"clojure-test-suite: transient strictness gaps","description":"Transient ops (conj!/assoc!/disj!/pop!) don't throw on misuse (bad shapes, use-after-persistent!) where Clojure does. Surfaced by conj_bang/assoc_bang/pop_bang/persistent_bang/transient suite files (~50 fails). Lenient behavior; decide whether to enforce.","status":"closed","priority":3,"issue_type":"bug","assignee":"Yogthos","owner":"yogthos@gmail.com","created_at":"2026-06-05T13:05:16Z","created_by":"Yogthos","updated_at":"2026-06-05T13:29:38Z","started_at":"2026-06-05T13:22:51Z","closed_at":"2026-06-05T13:29:38Z","close_reason":"Fixed real gaps: invokable transients, assoc! odd args, use-after-persistent! invalidation, pop!-empty. Bad-shape type strictness left as accepted lenient divergence.","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"jolt-do7","title":"phm-typed maps as set elements / map keys hash by identity","description":"A map produced via hash-map/assoc (a Janet table/phm) used as a set element or map key hashes by identity, so #{{:a 1}} (struct elem) != #{(hash-map :a 1)} (phm elem) and such elements aren't retrievable by value. Map literals (structs) work. Needs value-based hashing for phm keys in PHM/PHS. Edge case; spec covers literal-map cases.","status":"closed","priority":3,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-05T04:07:50Z","created_by":"Yogthos","updated_at":"2026-06-05T05:02:01Z","closed_at":"2026-06-05T05:02:01Z","close_reason":"Fixed: PHM/PHS canonicalize collection keys for value-based hashing/comparison; map literals \u0026 assoc promote to phm on collection keys; frequencies/group-by use phm base; set = is value-based. Also fixed set literals not evaluating elements and the REPL phm printer. Spec cases added. (commit pushed next)","dependency_count":0,"dependent_count":0,"comment_count":0} {"_type":"issue","id":"jolt-do7","title":"phm-typed maps as set elements / map keys hash by identity","description":"A map produced via hash-map/assoc (a Janet table/phm) used as a set element or map key hashes by identity, so #{{:a 1}} (struct elem) != #{(hash-map :a 1)} (phm elem) and such elements aren't retrievable by value. Map literals (structs) work. Needs value-based hashing for phm keys in PHM/PHS. Edge case; spec covers literal-map cases.","status":"closed","priority":3,"issue_type":"bug","owner":"yogthos@gmail.com","created_at":"2026-06-05T04:07:50Z","created_by":"Yogthos","updated_at":"2026-06-05T05:02:01Z","closed_at":"2026-06-05T05:02:01Z","close_reason":"Fixed: PHM/PHS canonicalize collection keys for value-based hashing/comparison; map literals \u0026 assoc promote to phm on collection keys; frequencies/group-by use phm base; set = is value-based. Also fixed set literals not evaluating elements and the REPL phm printer. Spec cases added. (commit pushed next)","dependency_count":0,"dependent_count":0,"comment_count":0}

View file

@ -78,6 +78,7 @@ Jolt targets Clojure semantics but runs on Janet, not the JVM. The notable diver
- **Collections.** By default Jolt uses immutable persistent data structures: vectors are 32-way branching tries (structural-sharing persistent vectors with O(log₃₂ n) `conj`/`assoc`/`nth`), lists are persistent singly-linked cons cells (O(1) `conj`/`cons` prepend with structural sharing), and maps/sets are persistent hash structures. Value equality and sequence operations are Clojure-compatible, but hash-map/hash-set iteration order is unspecified and differs from Clojure — use `sorted-map`/`sorted-set` when order matters. - **Collections.** By default Jolt uses immutable persistent data structures: vectors are 32-way branching tries (structural-sharing persistent vectors with O(log₃₂ n) `conj`/`assoc`/`nth`), lists are persistent singly-linked cons cells (O(1) `conj`/`cons` prepend with structural sharing), and maps/sets are persistent hash structures. Value equality and sequence operations are Clojure-compatible, but hash-map/hash-set iteration order is unspecified and differs from Clojure — use `sorted-map`/`sorted-set` when order matters.
- **Mutable build mode.** Jolt can be compiled to use fast Janet-native *mutable* collections instead, via a build-time flag: `JOLT_MUTABLE=1 jpm build` (default `jpm build` is immutable). In mutable mode vectors and lists share one mutable array representation (so `conj` mutates in place and appends, and `vector?`/`list?` no longer distinguish them) — a performance/looseness trade-off. The default immutable build has full Clojure value semantics. - **Mutable build mode.** Jolt can be compiled to use fast Janet-native *mutable* collections instead, via a build-time flag: `JOLT_MUTABLE=1 jpm build` (default `jpm build` is immutable). In mutable mode vectors and lists share one mutable array representation (so `conj` mutates in place and appends, and `vector?`/`list?` no longer distinguish them) — a performance/looseness trade-off. The default immutable build has full Clojure value semantics.
- **Concurrency / STM.** Single-threaded. No refs, `dosync`, agents, or `send`; `locking` evaluates its body without real locking. Atoms, volatiles, and delays are supported. - **Concurrency / STM.** Single-threaded. No refs, `dosync`, agents, or `send`; `locking` evaluates its body without real locking. Atoms, volatiles, and delays are supported.
- **core.async.** `clojure.core.async` runs on Janet fibers and channels (`chan`, `go`, `go-loop`, `<!`/`>!`/`<!!`/`>!!`, `close!`, `alts!`, `timeout`, `put!`/`take!`). Because Janet fibers are stackful coroutines, a `go` block is just its body run in a fiber — no CPS/state-machine rewrite — so `<!`/`>!` work *anywhere*, including inside `try`, nested `fn`s, and loops (positions Clojure's `go` macro forbids). Go blocks are cooperatively scheduled on one OS thread, so parking (`<!`) and blocking (`<!!`) coincide; `thread` runs cooperatively too. Dynamic-var binding conveyance and channel transducers are not yet implemented.
- **Regex.** Compiled to Janet's PEG engine (Janet has no regex). Supported: capturing groups (`[whole g1 …]`), greedy and lazy quantifiers with backtracking, `(?:…)`, lookahead `(?=…)`/`(?!…)`, alternation, anchors `^ $ \b \B`, character classes, and the `(?i)` flag. Not supported: lookbehind, backreferences (`\1`), and named groups (`(?<name>…)`). - **Regex.** Compiled to Janet's PEG engine (Janet has no regex). Supported: capturing groups (`[whole g1 …]`), greedy and lazy quantifiers with backtracking, `(?:…)`, lookahead `(?=…)`/`(?!…)`, alternation, anchors `^ $ \b \B`, character classes, and the `(?i)` flag. Not supported: lookbehind, backreferences (`\1`), and named groups (`(?<name>…)`).
- **Arrays.** Java-style arrays map onto Janet's native types: `byte-array` is a Janet buffer (contiguous, C-backed); `object-array`/`int-array`/`double-array`/etc. are Janet arrays. `aget`/`aset`/`alength`/`aclone` work over both. - **Arrays.** Java-style arrays map onto Janet's native types: `byte-array` is a Janet buffer (contiguous, C-backed); `object-array`/`int-array`/`double-array`/etc. are Janet arrays. `aget`/`aset`/`alength`/`aclone` work over both.
- **Transients.** `transient`/`conj!`/`assoc!`/`dissoc!`/`disj!`/`pop!`/`persistent!` are real mutable scratch collections backed by Janet's native arrays and tables (vectors → arrays, maps/sets → tables), so building a collection with them avoids the per-step copying of the persistent path (notably for maps/sets). `persistent!` freezes back to a persistent value. - **Transients.** `transient`/`conj!`/`assoc!`/`dissoc!`/`disj!`/`pop!`/`persistent!` are real mutable scratch collections backed by Janet's native arrays and tables (vectors → arrays, maps/sets → tables), so building a collection with them avoids the per-step copying of the persistent path (notably for maps/sets). `persistent!` freezes back to a persistent value.

View file

@ -9,6 +9,7 @@
(use ./core) (use ./core)
(use ./compiler) (use ./compiler)
(use ./loader) (use ./loader)
(use ./async)
(defn normalize-pvecs (defn normalize-pvecs
"Deep-convert any sequential (pvec/tuple/array) to a Janet tuple. Test helper "Deep-convert any sequential (pvec/tuple/array) to a Janet tuple. Test helper
@ -37,6 +38,9 @@
# via JOLT_MUTABLE (see config.janet); init-core! registers vec/vector/conj/ # via JOLT_MUTABLE (see config.janet); init-core! registers vec/vector/conj/
# etc. that produce the mode-appropriate values, so nothing extra to load. # etc. that produce the mode-appropriate values, so nothing extra to load.
(init-core! ctx) (init-core! ctx)
# clojure.core.async (channels + go blocks on Janet fibers); pre-populated
# so (require '[clojure.core.async ...]) finds it and applies :as/:refer.
(install-async! ctx)
ctx)) ctx))
(defn eval-string (defn eval-string

145
src/jolt/async.janet Normal file
View file

@ -0,0 +1,145 @@
# clojure.core.async on Janet fibers.
#
# Janet fibers are stackful coroutines, so a `go` block is just "run the body in
# a fiber" — the body parks on a channel op by yielding to the event loop, and
# the whole interpreter call stack rides along on the fiber's stack. No CPS/state
# machine transform (unlike Clojure's `go` macro), so <! / >! work anywhere
# (inside try, nested fns, loops, …).
#
# A channel is a pair of Janet ev/chans wrapped in a tagged table: a `:ch` that
# carries values and a `:done` that is closed to signal channel close. A take is
# `(ev/select :ch :done)` — ev/select checks in order, so buffered values drain
# before the close signal is seen, giving Clojure's drain-then-nil semantics. We
# use a separate `:done` channel because Janet's ev/chan-close *discards* a
# channel's buffered values. close! just closes :done (idempotent, no fiber), so
# nothing leaks.
#
# Single OS thread: go blocks run cooperatively on the event loop, so <! (park)
# and <!! (block) are the same here.
(use ./types)
(use ./pv)
(use ./config)
(defn jolt-chan? [x] (and (table? x) (= :jolt/chan (get x :jolt/type))))
(defn- wrap [vc dc] @{:jolt/type :jolt/chan :ch vc :done dc :closed @[false]})
(defn- vchan [x]
(if (jolt-chan? x) (x :ch) (error (string "expected a channel, got " (type x)))))
# (chan) unbuffered, (chan n) fixed buffer of n. A transducer 3rd arg is
# accepted but ignored until Phase 3.
(defn async-chan [&opt n xform]
(wrap (if (and (number? n) (> n 0)) (ev/chan n) (ev/chan)) (ev/chan)))
(defn async-close! [ch]
(when (not (in (ch :closed) 0))
(put (ch :closed) 0 true)
(protect (ev/chan-close (ch :done))))
nil)
# <! / <!! — take, parking the fiber. Drains buffered values, then returns nil
# once the channel is closed and empty.
(defn async-take [ch]
(def r (ev/select (ch :ch) (ch :done)))
(if (= :take (in r 0)) (in r 2) nil))
# >! / >!! — put, parking the fiber. Returns true if delivered, false if the
# channel is closed. nil may not be put on a channel (it is the closed value).
(defn async-give [ch v]
(when (nil? v) (error "Can't put nil on a channel"))
(if (in (ch :closed) 0) false
(if (in (protect (ev/give (ch :ch) v)) 0) true false)))
# 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
# just closes. Buffered(1) so a fire-and-forget go leaves no parked fiber.
(defn async-go-spawn [thunk]
(def w (async-chan 1))
(ev/go (fn []
(def res (protect (thunk)))
(when (and (in res 0) (not (nil? (in res 1))))
(async-give w (in res 1)))
(async-close! w)))
w)
# (alts! [ch ...]) — take from whichever channel is ready first; returns
# [value channel] (value is nil if that channel closed). Take-only for v1.
(defn async-alts [chans]
(def cs (cond (pvec? chans) (pv->array chans)
(tuple? chans) chans
(array? chans) chans
(error "alts! expects a vector of channels")))
(def raws @[])
(def lookup @{}) # raw ev/chan -> [jolt-chan done?]
(each c cs
(array/push raws (c :ch)) (put lookup (c :ch) [c false])
(array/push raws (c :done)) (put lookup (c :done) [c true]))
(def r (ev/select ;raws))
(def info (get lookup (in r 1)))
(def jc (in info 0))
(def val (if (or (in info 1) (not= :take (in r 0))) nil (in r 2)))
(pv-from-indexed @[val jc]))
# (timeout ms) — a channel that closes after ms milliseconds.
(defn async-timeout [ms]
(def w (async-chan))
(ev/go (fn [] (ev/sleep (/ ms 1000)) (async-close! w)))
w)
# (put! ch v [cb]) — async put; (take! ch cb) — async take. Fire a fiber and
# call the optional callback with the result.
(defn async-put! [ch v &opt cb]
(ev/go (fn []
(def ok (async-give ch v))
(when (and cb (not (nil? cb))) (cb ok))))
nil)
(defn async-take! [ch cb]
(ev/go (fn []
(def val (async-take ch))
(when (and cb (not (nil? cb))) (cb val))))
nil)
# --- macros (Janet macro-fns that return forms) ---
(defn- sym [name &opt ns] {:jolt/type :symbol :ns ns :name name})
# (go body...) -> (go-spawn (fn* [] body...))
(defn async-go [& body]
@[(sym "go-spawn" "clojure.core.async") (array (sym "fn*") [] ;body)])
# (go-loop bindings body...) -> (go (loop bindings body...))
(defn async-go-loop [bindings & body]
@[(sym "go" "clojure.core.async") (array (sym "loop") bindings ;body)])
# (thread body...) — runs cooperatively in a fiber here (no OS thread); same
# shape as go (returns a result channel).
(defn async-thread [& body]
@[(sym "go-spawn" "clojure.core.async") (array (sym "fn*") [] ;body)])
(def- async-bindings
@{"chan" async-chan
"chan?" jolt-chan?
"close!" async-close!
"<!" async-take "<!!" async-take
">!" async-give ">!!" async-give
"alts!" async-alts "alts!!" async-alts
"timeout" async-timeout
"put!" async-put!
"take!" async-take!
"go-spawn" async-go-spawn
"go" async-go
"go-loop" async-go-loop
"thread" async-thread})
(def- async-macros @{"go" true "go-loop" true "thread" true})
(defn install-async!
"Create/populate the clojure.core.async namespace in ctx."
[ctx]
(let [ns (ctx-find-ns ctx "clojure.core.async")]
(loop [[name f] :pairs async-bindings]
(def v (ns-intern ns name f))
(when (get async-macros name) (put v :macro true)))
ns))

View file

@ -1557,6 +1557,7 @@
(set? v) (pr-render-seq buf (phs-seq v) "#{" "}") (set? v) (pr-render-seq buf (phs-seq v) "#{" "}")
(phm? v) (pr-render-pairs buf (phm-entries v)) (phm? v) (pr-render-pairs buf (phm-entries v))
(core-transient? v) (buffer/push-string buf (string "#<transient " (v :kind) ">")) (core-transient? v) (buffer/push-string buf (string "#<transient " (v :kind) ">"))
(and (table? v) (= :jolt/chan (get v :jolt/type))) (buffer/push-string buf "#<channel>")
(pvec? v) (pr-render-seq buf (pv->array v) "[" "]") (pvec? v) (pr-render-seq buf (pv->array v) "[" "]")
(plist? v) (pr-render-seq buf (pl->array v) "(" ")") (plist? v) (pr-render-seq buf (pl->array v) "(" ")")
(and (table? v) (get v :jolt/deftype)) (buffer/push-string buf (string v)) (and (table? v) (get v :jolt/deftype)) (buffer/push-string buf (string v))

View file

@ -104,6 +104,9 @@
(and (table? v) (= :jolt/transient (v :jolt/type))) (and (table? v) (= :jolt/transient (v :jolt/type)))
(push-str buf (string "#<transient " (v :kind) ">")) (push-str buf (string "#<transient " (v :kind) ">"))
(and (table? v) (= :jolt/chan (v :jolt/type)))
(push-str buf "#<channel>")
(phm? v) (phm? v)
(do (do
(push-str buf "{") (push-str buf "{")

View file

@ -0,0 +1,55 @@
# Specification: clojure.core.async on Janet fibers (Phase 1 — API layer).
# Each case is self-contained: it requires the ns, sets up channels/go blocks,
# and ends with a take that pumps the event loop and yields the value compared.
(use ../support/harness)
(def REQ
"(require '[clojure.core.async :refer [go go-loop chan <! >! close! alts! timeout put! take! chan?]]) ")
(defn- a [body] (string "(do " REQ body ")"))
(defspec "core.async / go & channels"
["go produce, <! consume"
"42" (a "(def c (chan)) (go (>! c (+ 40 2))) (<! c)")]
["go returns a result channel"
"42" (a "(<! (go (* 6 7)))")]
["go body nil -> channel closes"
"nil" (a "(<! (go nil))")]
["<! parks mid-expression"
"42" (a "(def x (chan)) (def y (chan)) (go (>! x 10)) (go (>! y 32)) (<! (go (+ (<! x) (<! y))))")]
["chan? true" "true" (a "(chan? (chan))")]
["chan? false" "false" (a "(chan? [1 2])")])
(defspec "core.async / buffering & close"
["buffered channel holds values"
"[1 2 3]"
(a "(def c (chan 5)) (go (>! c 1) (>! c 2) (>! c 3) (close! c)) (<! (go-loop [o []] (let [v (<! c)] (if (nil? v) o (recur (conj o v))))))")]
["closed channel drains then nil"
"true"
(a "(def c (chan 2)) (go (>! c :a) (close! c)) (<! (go (and (= :a (<! c)) (nil? (<! c)))))")]
[">! to a closed channel is false"
"false" (a "(def c (chan 1)) (close! c) (>! c 1)")]
["take from closed empty channel is nil"
"nil" (a "(def c (chan)) (close! c) (<! c)")])
(defspec "core.async / go-loop & pipelines"
["go-loop accumulates"
"6"
(a "(def in (chan 5)) (go (>! in 1) (>! in 2) (>! in 3) (close! in)) (<! (go-loop [acc 0] (let [v (<! in)] (if (nil? v) acc (recur (+ acc v))))))")]
["3-stage concurrent pipeline"
"50"
(a "(def in (chan)) (def mid (chan)) (def out (chan)) (go (>! mid (inc (<! in)))) (go (>! out (* 10 (<! mid)))) (go (>! in 4)) (<! out)")])
(defspec "core.async / alts! & timeout"
["alts! picks the ready channel"
"true"
(a "(def x (chan)) (def y (chan)) (go (>! y :v)) (<! (go (let [[v ch] (alts! [x y])] (and (= v :v) (= ch y)))))")]
["timeout wins over an idle channel"
"true"
(a "(def slow (chan)) (<! (go (let [[v ch] (alts! [slow (timeout 30)])] (nil? v))))")])
# Janet fibers are stackful, so <! works in positions Clojure's go macro forbids.
(defspec "core.async / parking anywhere"
["<! inside try/catch"
"99" (a "(<! (go (try (<! (go 99)) (catch :default e -1))))")]
["<! inside a nested fn called in a go"
"7" (a "(def c (chan)) (go (>! c 7)) (<! (go ((fn [] (<! c)))))")])