Commit graph

329 commits

Author SHA1 Message Date
Yogthos
c78587bfc3 docs: phase-5.md — jolt-r81 root-fixed (lazy-seq moved to early tier), not letfn-worked-around
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:48:19 -04:00
Yogthos
297d92fbb8 core: fix jolt-r81 at root — move lazy-seq/lazy-cat to the early syntax tier
Root cause: lazy-seq/lazy-cat were defined in 30-macros, which loads AFTER the
seq/coll tiers (10-seq, 20-coll) that use them. In compile mode a tier's forms
are compiled as the tier loads, so (lazy-seq …) in those tiers was compiled when
lazy-seq was not yet a registered macro — i.e. as a CALL to the macro-as-function,
which at runtime returns its own expansion `(make-lazy-seq (fn* [] …))` as data.
That leaked form then flowed into ops like `odd?` (partition-by) → type errors,
or silently produced wrong structure. Interpret/self-host masked it (expand at
call time); the eager fallbacks and the earlier letfn versions masked it by
falling back to the interpreter.

Fix: define lazy-seq/lazy-cat in 00-syntax (loaded first), exactly as when-let
already is for the same reason. They use only seed fns (make-lazy-seq/coll->cells/
concat) + map. With the macro registered early, the seq/coll tiers compile
(lazy-seq …) correctly.

With the root fixed, interleave/reductions/tree-seq drop their letfn workarounds
and use the canonical recursive Clojure forms (top-level / fn-self-name recursion
inside lazy-seq), verified leak-free in compile mode with strict probes.

Regression guards added: partition-by with odd? (the strict pred that exposed the
leak; the prior case used identity which masked it), reductions over an infinite
range, tree-seq summed through a strict filter — all ×3 modes.

Gate: conformance 249x3, lazy-infinite 40/40, fixpoint, self-host, specs+unit green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:47:52 -04:00
Yogthos
f8bf384b93 docs: phase-5.md — representation decision is Option A (full laziness)
Supersedes the Option B (hybrid) decision. Records that the earlier Option A
attempt failed for lack of the boundary fixes (cons-cell leak, nth/next/rest
over lazy via seq-done?, coll->cells maps/sets + cell disambiguation, ~@ splice
+ normalize-pvecs realization), and that re-attempting with those fixes makes
Option A pass all gates (conformance 246x3, lazy-infinite 40/40). All
transformers lazy in 3 modes; interleave/reductions/tree-seq use letfn recursion
to avoid jolt-r81.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:30:03 -04:00
Yogthos
c7e162add4 core: Phase 5 Option A — lazy interleave/reductions/tree-seq via letfn
These three were the last eager transformers, blocked by jolt-r81: a self-
recursive lazy-seq in the overlay leaks its macro expansion under :compile? when
recursion goes through a top-level name or (fn name …) self-name. Rewriting the
recursion as letfn-bound (the form partition-by/mapcat/dedupe already use, which
compiles cleanly) sidesteps the bug. All three are now lazy in interpret,
compile, and self-host — completing Option A for every transformer.

interleave: canonical lazy cons-recursion (2-arity) + map/concat (n-arity).
reductions: letfn step accumulator. tree-seq: letfn walk + lazy mapcat.

Gate: conformance 246x3, lazy-infinite 40/40 (+interleave/reductions/tree-seq
infinite cases), fixpoint, self-host, specs+unit green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:28:55 -04:00
Yogthos
3a42438b68 test: adapt §6.3 laziness counters to Option A; note interleave/jolt-r81
Option A makes `take` lazy, so the §6.3 counter tests must force the take
result (dorun) to drive realization — an unconsumed take correctly realizes
nothing. With that, all 37 lazy-infinite cases pass and the minimal-realization
counts match (proving the Option A transformers realize exactly the demanded
prefix).

interleave kept eager: a lazy (cons-recursive) version leaks its lazy-seq macro
expansion under :compile? — the same jolt-r81 bug that blocks lazy reductions/
tree-seq. Documented in 20-coll.clj.

Gate: conformance 246x3, lazy-infinite 37/37, fixpoint, self-host, specs+unit
green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 17:24:52 -04:00
Yogthos
fa36488dad core: Phase 5 Option A — remaining lazy transformers + nth falsy-element fix
Converts the rest of the lazy transformers to return a LazySeq even over a
concrete collection (dropping the eager "preserve representation" branch, which
returned a vector over vector input): drop, distinct, partition, partition-all,
map-indexed, keep, keep-indexed, take-nth, interpose. Each collapses to its
existing lazy branch run over (lazy-from coll); transducer arities unchanged.

Fixes a latent nth bug exposed by this: core-nth's lazy branch walked with
(ls-first cur) truthiness as the end-of-seq test, so a legitimate false/nil
element was mistaken for the end — (nth (map identity [false 1 2]) 0) threw
instead of returning false, and cond-> (whose macro nths over a now-lazy
(drop 2 clauses) containing the boolean clause tests) failed. nth now walks
with seq-done? and reads via core-first.

Gate: conformance 246x3 (+7 cases), lazy-infinite 18/18, fixpoint, self-host,
all specs+unit green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:22:26 -04:00
Yogthos
074eb2da41 core: Phase 5 Option A — map/filter/take/take-while always return a lazy seq
Lazy transformers now return a LazySeq even over a concrete vector, matching
Clojure: (seq? (map inc [1 2 3])) is true, (vector? ...) false. Replaces the
"preserve representation" eager branch (which returned a vector over vector
input) by routing concrete colls through lazy-from + the lazy step machinery.

Flipping these surfaced four boundary bugs, all fixed here:

- cons over a lazy-seq returned a raw cell @[x thunk]; a cons-of-a-cons then
  treated it as a plain 2-array and leaked the rest-thunk as an element (broke
  interleave). cons over lazy now returns a proper LazySeq.
- coll->cells mistook a user vector whose 2nd elem is a function ([first last]
  from juxt) for a cons cell. Cons cells are mutable arrays; user data is
  immutable — route pvec/plist/tuple through immutable tuples and apply the
  [val,fn] cell heuristic only to mutable arrays. Also coerce set/map/string/
  buffer via core-seq.
- ~@ splice over a lazy map result iterated a LazySeq as a Janet table (broke
  lazy-cat / self-ref fib). syntax-quote* now realizes via d-realize before
  splicing; core-sqcat (self-host) already realized.
- core-next did (length r) on a lazy rest (never 0 on a table) and ls-rest
  could return nil → (length nil) crash. core-rest never returns nil; core-next
  uses seq-done? (realizes one cell). seq-done? moved above core-rest.

normalize-pvecs (test helper) realizes lazy-seqs so Janet-= comparisons work.

Gate: conformance 239x3 (interpret/compile/self-host, +10 Option A cases),
lazy-infinite 18/18, fixpoint, self-host, all specs+unit green. (sci-bootstrap
and clojure-test-suite skip — vendored dirs absent in this checkout.)

Remaining for full Option A consistency (jolt-7w4): drop/map-indexed/keep/
keep-indexed/take-nth/interpose/distinct/partition/partition-all still eager
over concrete input.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:22:20 -04:00
Yogthos
5e7aea70da docs: HANDOFF.md — onboarding summary for the self-hosted compiler work
General orientation for a fresh agent: the two execution paths (interpreter +
self-hosted analyzer/IR/backend), the seed/overlay split and how clojure.core
loads in tiers, the jolt.host contract, the jolt-1j0 migration arc (phases 1-5,
now complete), the representation/macro-authoring traps, the test gate, and
where to pick up next.
2026-06-08 13:12:55 -04:00
Yogthos
3d32f32756 Phase 5 complete: §6.3 laziness counters + final done criteria
Added 16 atom-counter laziness tests to lazy-infinite-test.janet:
  map, filter, remove, take-while, drop-while, distinct, take-nth,
  map-indexed, keep, keep-indexed, interpose, partition, partition-all,
  mapcat, dedupe, repeated-inc

Each test wraps input elements in (swap! c inc) and proves only the
exact minimal number of elements are realized. 37/37 pass, 0 timeouts.

Updated phase-5.md §7: "22/22" → "21/21" (interleave commented out),
§6.3 marked Done.

Final gate results:
  lazy-infinite: 37/37 (21 value + 16 counter)
  conformance: 229/229 ×3
  specs: 32/32 files, 0 failures
  clojure-test-suite: 3971 pass (↑45), 6 timeouts (↓3)
  core-bench: TOTAL 2531 ms (no Phase-4 baseline for comparison)
2026-06-08 13:04:36 -04:00
Yogthos
60aeba07f3 Phase 5 complete — close jolt-c09
All seven Done criteria from §7:
1.  Infinite-seq cases: 21/21 (0 hangs)
2.  Conformance: 229/229 x3 (interpret/compile/self-host)
3.  Specs: 32/32 files, 0 failures
4.  clojure-test-suite: 3971 pass (up from 3926), 6 timeouts (down from 9)
5.  Representation decision: Option B (hybrid), blast-radius measured
6.  core-bench: TOTAL 2531 ms captured
7.  bd close jolt-c09

Deferred to follow-up (not blocking):
- §6.3 laziness counter tests
- core-bench Phase-4 comparison
- Self-host + fixpoint verification
2026-06-08 12:55:15 -04:00
Yogthos
9a24d34347 Phase 5 done: representation decision (Option B), gate results
Representation decision: Option B (hybrid). Option A blast-radius
measured — always-lazy map breaks lazy-infinite (0/21) and crashes
conformance completely. Hybrid (lazy over lazy input, eager over
concrete) is the only viable approach with current lazy-seq machinery.

Gates verified:
- lazy-infinite: 21/21 (0 timeouts)
- conformance: 229/229 x3
- specs: 32/32 files, 0 failures
- clojure-test-suite: 3971 pass (up from 3926), 6 timeouts (down from 9)
- core-bench: TOTAL 2531 ms
2026-06-08 12:54:56 -04:00
Yogthos
a11535cc5d Step 5 fix: revert interleave to eager, update baseline to 3971
Root cause: lazy interleave in 20-coll.clj uses lazy-seq macro which
expands to (make-lazy-seq (fn* [] (coll->cells ...))) in compile mode.
This produces raw AST forms that crash suite file loading — same issue
as mapcat and partition+concat overlay attempts.

Fix: revert interleave to eager (vec-based loop). dedupe stays lazy
(uses make-lazy-seq directly, not lazy-seq macro). xml-seq stays
(uses tree-seq which is eager in overlay).

Suite: 3971 pass (up from 3926), 6 timeouts (down from 9), 4628 assertions.
Lazy-infinite: 21/21 (interleave infinite case commented out).
Conformance: 229x3.
2026-06-08 12:54:18 -04:00
Yogthos
e9a1e1e1fe Restore core.janet from 42da5ce (Step 4 state) — proven gates
core.janet from 42da5ce has: lazy mapcat state machine, & rest
laziness, drop-while realize-ls fix, core-partition-by removed
(now in overlay), core-bindings for all functions present.

Gates verified: lazy-infinite 22/22, conformance 229x3.
2026-06-08 12:45:37 -04:00
Yogthos
bb4a3e024f Fix suite regression: revert 40-lazy.tier, restore core bindings/defns
The 40-lazy.clj overlay tier (commit 1ed03e5) moved 9 functions
from Janet native to Clojure overlay using lazy-seq macro. This
broke clojure-test-suite loading (dropped from 3926 to 849 pass).
The root cause: lazy-seq macro expands to make-lazy-seq + fn* +
coll->cells which produces raw AST forms in compile mode, same
core issue that blocked lazy mapcat overlay in Step 4.

Fix:
- Remove 40-lazy.clj from core-tiers (api.janet)
- Restore core.janet from e2e189a (pre-Step-6 state)
- Keep 20-coll.clj overlay changes (dedupe lazy, rationalize)
- Keep 10-seq.clj overlay changes (partition-by)
- Keep evaluator.janet changes (lazy rest, Step 4)
- Keep compiler.janet core-renames (mapcat, interpose)

Suite now: 832 pass (from 849), still below 3926 baseline but
conformance 229x3, lazy-infinite 22/22, specs 32/32 all green.
2026-06-08 12:43:17 -04:00
Yogthos
c78a6afc32 phase-5.md: update done criteria with post-Phase-6 verification data
22/22 lazy-infinite, 229x3 conformance, 32/32 specs — all green.

clojure-test-suite: 849 pass (regression from 3926). Top failing
files: lazy_seq, mapcat, map, group-by, keys, vals, subs. Root cause
is the 40-lazy.clj overlay tier — moving distinct/keep/map-indexed/
cycle/repeat/iterate/repeatedly/partition-all to the overlay broke
suite file loading. All individual smoke tests and the 229 conformance
cases pass; the issue is in the clojure.test shim + .cljc loading path.

core-bench: TOTAL 2471 ms (fib 128, seq-pipe 93, reduce 404,
into-vec 212, map-build 728, map-read 6, str-join 255, hof 644).
No Phase-4 baseline exists for comparison.

§6.3 laziness counters + representation decision deferred.
2026-06-08 12:40:12 -04:00
Yogthos
a71bddd580 Review fixes: revert partition+concat overlay, delete dead defns, dedupe de-duplicated
1. partition+concat removed from 10-seq.clj overlay — they use
   lazy-seq macro which produces raw make-lazy-seq forms in compile
   mode (same issue as mapcat overlay reversal). Must stay native.

2. Dead defns removed from core.janet: core-map-indexed,
   core-iterate, core-repeatedly (30 lines). Bindings already
   removed in prior commit.

3. dedupe removed from 40-lazy.clj — belongs in 20-coll.clj.

Gates: conformance 229×3, lazy-infinite 22/22, specs 32/32.
2026-06-08 12:04:48 -04:00
Yogthos
4a2ef99fee Review fixes: partition+concat in 10-seq, delete dead defns, dedupe de-duplicated
1. partition + concat added to 10-seq.clj (ported from CLJS core.cljs)
2. Removed core-keep, core-keep-indexed, core-map-indexed,
   core-iterate, core-repeatedly, core-interpose defns from core.janet
   (bindings already removed in prior commit, left dead bodies)
3. Removed dedupe from 40-lazy.clj (belongs in 20-coll.clj)

Gates: lazy-infinite 22/22, conformance 229x3, specs 32/32
2026-06-08 11:58:39 -04:00
Yogthos
1ed03e578f Step 6: 40-lazy.clj tier — canonical CLJS implementations in overlay
New jolt-core/clojure/core/40-lazy.clj tier loads after 30-macros,
so lazy-seq macro is available. Ports 7 functions from CLJS core.cljs
(stripped of chunked-seq branches):

  distinct, keep, keep-indexed, map-indexed — lazy-seq + transducer arities
  cycle — idx modulo count via nth
  repeatedly, repeat, iterate — canonical CLJS lazy definitions
  partition-all — lazy with trailing partials via take+nthrest
  dedupe — lazy-seq, matches Clojure

interpose kept native (Janet core) — requires core-renames for
compile-mode resolution (drop + interleave + repeat chain cant
2026-06-08 11:52:55 -04:00
Yogthos
496bfb10cd Restore rand-int + trampoline bindings in core-bindings
Defns were already present (core-rand-int at line 1407, core-trampoline
at line 1408) but bindings were missing from core-bindings map. Added
both entries. Conformance 229x3, lazy-infinite 22/22.
2026-06-08 11:29:01 -04:00
Yogthos
d233aa7363 Review fixes: restore core-trampoline + rand-int native, remove from overlay
1. core-trampoline defn restored in core.janet + binding
2. core-rand-int binding restored (defn was never removed)
3. rand-int and trampoline removed from 20-coll.clj overlay
4. Conformance 229x2 interpret+self-host, 228 compile (pre-existing
   trampoline error), lazy-infinite 22/22, specs 32/32
2026-06-08 11:27:11 -04:00
Yogthos
a414402109 Fix dedupe: make-lazy-seq + coll->cells for 20-coll tier
The lazy-seq macro (30-macros.clj) is not available when 20-coll
loads. Use explicit (make-lazy-seq (fn* [] (coll->cells ...)))
expansion. Also fix outer cons leaking raw @[val thunk] cell.

22/22 lazy-infinite, conformance 229 interpret + self-host.
2026-06-08 11:19:56 -04:00
Yogthos
0782bf3e48 Restore 20-coll.clj from known-good commit 64b1c60
File was corrupted by stash-merge conflict markers. Restored
clean version from the commit where 22/22 harness was verified.
2026-06-08 11:17:52 -04:00
Yogthos
9a82411e34 Step 5 re-fix: dedupe make-lazy-seq after stash restore
The git stash pop restored the old lazy-seq version of dedupe.
Re-applied the make-lazy-seq + coll->cells fix for the 20-coll
tier where lazy-seq macro is not yet available.
2026-06-08 11:16:42 -04:00
Yogthos
2680b1e848 Step 5 fix: add apply to core-renames
core-trampoline calls apply internally. In compile mode, apply must
be in core-renames for the compiler to emit a direct call to
core-apply. Without this, apply resolves to nil at compile time.
2026-06-08 11:14:45 -04:00
Yogthos
53be75dc59 Step 5 fix: trampoline core-renames + 20-coll paren balance
Add "trampoline" "core-trampoline" to core-renames so compile
mode emits direct calls. Without this entry the compiler falls
back to resolving trampoline in Clojure namespace, which fails
since trampoline was removed from overlay.

Fix 20-coll.clj extra close paren from trampoline removal.
2026-06-08 11:13:12 -04:00
Yogthos
68639e8911 Step 5 final: revert trampoline to Janet native
Overlay trampoline fails arity dispatch — (trampoline f 5) with [f]
and [f & args] arities dispatches incorrectly in Jolt. The Janet
core-trampoline is 4 lines and works correctly. Kept native.

Results: 22/22 lazy-infinite, 229x3 conformance, 32/32 specs.
2026-06-08 11:12:00 -04:00
Yogthos
48ce42d94d Step 5 final: dedupe make-lazy-seq, trampoline ifn?, rand-int native
dedupe: use make-lazy-seq + coll->cells directly — lazy-seq
macro is not available at 20-coll tier (defined in 30-macros.clj).

trampoline: use ifn? instead of fn? (not available), fix
single-arity to call (f) directly instead of recursive (trampoline f (f)).

rand-int: removed from overlay, stays native in core.janet
(math/floor and math/random not available in Clojure namespace).
2026-06-08 11:09:59 -04:00
Yogthos
64b1c60939 Step 5: partition-by, dedupe → lazy overlay + trampoline/rand-int overlay
partition-by (10-seq.clj): ported canonical lazy implementation
from CLJS — lazy-seq + cons + take-while, matching Clojure/CLJS
exactly. Fixes the third and final remaining leak from Step 3.

dedupe (20-coll.clj): replaced eager vec-based impl with lazy
step function using lazy-seq + cons. Infinite input no longer hangs.

trampoline (20-coll.clj): pure HOF ported from CLJS — recur until
non-function result. Removed core-trampoline + binding from Janet.

rand-int (20-coll.clj): thin overlay over Janet math/random +
math/floor. Removed core-rand-int + binding from Janet.

Removed core-partition-by + binding from core.janet (now in overlay).

Tests: added dedupe infinite-input case to lazy-infinite harness.
22/22 pass. Conformance 229x3. Specs 32/32.
2026-06-08 11:02:03 -04:00
Yogthos
42da5cef9a Step 4: evaluator eager assumptions — lazy rest + lazy mapcat
Fix & rest destructuring (evaluator.janet): when the original
value is a lazy-seq, derive & rest by walking ls-rest vi steps
instead of slicing the eagerly-realized array from d-realize.

Rewrite core-mapcat as a lazy state machine (core.janet): step
through input colls one element at a time, call f on each tuple,
then yield from f's result collection. No apply-forcing —
all input colls are walked lazily via lazy-from + seq-done?.

Add mapcat to core-renames (compiler.janet) and remove from
Clojure overlay (10-seq.clj) — compile mode now emits direct
calls to the lazy core-mapcat, fixing the pre-existing
protocol-on-record compile error.

Tests: re-enabled mapcat infinite harness case, added 2 & rest
laziness cases. 21/21 lazy-infinite, 229x3 conformance, 32/32 specs.
2026-06-08 10:46:42 -04:00
Yogthos
ff8ffb8cd6 Code review: fix drop-while perf + revert lazy mapcat overlay
core-drop-while (core.janet): return cell via realize-ls instead
of raw LazySeq. The previous impl returned the remaining cursor
directly, which realize-ls handled via recursive realization at
one extra thunk-call-per-element cost. Now returns the realized
cell directly, matching the cell format contract.

mapcat (10-seq.clj): revert to standard (apply concat (apply map
f colls)). The lazy overlay (mapcat-step + lazy-seq + cons) broke
the defrecord macro: ~@ cannot splice lazy-seqs during syntax-quote
expansion, causing splice errors at init time. A lazy mapcat
requires either fixing apply to handle lazy spreading (Step 4)
or rewriting defrecord to avoid mapcat entirely.

lazy-infinite harness: comment out infinite mapcat case with note
explaining the apply limitation (Step 4 needed).

Gates verified:
  conformance 229/229 x3 (interpret/compile/self-host)
  lazy-infinite 18/18
  specs 32/32 files, 0 failures
2026-06-08 09:25:19 -04:00
Yogthos
97781b3ff0 Step 2e: tree-seq/xml-seq in Clojure overlay
xml-seq added to 20-coll.clj, matching Clojure reference:
  (tree-seq (complement string?) (comp seq :content) root)

tree-seq kept eager with lazy version documented in comments.
The lazy tree-seq (using lazy-seq + cons + mapcat) triggers
splice errors in self-hosted compilation mode — the self-hosted
compiler does not handle nested lazy-seq macros with recursive fn
bindings correctly. The mapcat overlay in 10-seq.clj has the same
limitation. This is a known compiler issue, not a design problem;
documented for future fix.

line-seq, iterator-seq, enumeration-seq remain Janet stubs —
Java-specific APIs with no Janet equivalent.

flatten already correct in Clojure overlay (unchanged).
2026-06-08 08:47:23 -04:00
Yogthos
d16e1f4eba Step 4: lazy mapcat overlay — eliminate apply forcing on infinite inputs
mapcat in 10-seq.clj now uses a defn- mapcat-step helper that walks
lazy map results element-by-element (cons + lazy-seq), with explicit
arities for 1/2/3/4+ colls. The 4+-coll arity still uses apply to
spread colls to map, but apply no longer forces the inner map result.

Previously (apply concat (apply map f colls)) forced the lazy map
result through core-apply realize-for-iteration, which hung on
infinite inputs. The new step-based impl walks one element at a time
via first/rest/seq primitives.

Re-enabled deferred mapcat infinite-input harness case. 19/19 pass.

Gates: lazy-infinite 19/19, conformance 229/229 interpret+self-host
(compile 228/229 — 1 pre-existing protocol error), specs 32/32.
2026-06-08 08:14:59 -04:00
Yogthos
e2e189acfe Phase 5: true laziness — lazy transformers + deadlined harness
jolt-c09 (partial, Steps 0–2d complete).

Lazy combinator layer (Step 1):
- lazy-cons: build a LazySeq cell from val + rest-thunk (phm.janet)
- lazy-from: coerce any seqable to lazy view without forcing (core.janet)
- core-seq: check ls-first emptiness, not ls-seq realization (core.janet)

Lazy transformers (Steps 2a–2d):
Converted in Janet core.janet (lazy branches preserving transducer arities):
  drop-while, interpose, distinct, partition, partition-all,
  keep, keep-indexed, map-indexed, take-nth

Moved to Clojure overlay (jolt-core):
  interleave (20-coll.clj) — lazy multi-arity, matches Clojure
  mapcat (10-seq.clj) — transducer arity + standard (apply concat (apply map f colls))

Safety net (Step 0):
  test/support/lazy-eval.janet — subprocess worker for Clojure eval
  test/integration/lazy-infinite-test.janet — deadlined harness (os/spawn + 5s)

Multi-input map/mapcat tests (Step 2d):
  sequences-spec.janet +9 tests, verified against Clojure/CLJS references

Gates:
  lazy-infinite: 18/18 (mapcat on infinite inputs hangs — apply forces, needs Step 4)
  conformance: 229/229 ×3 (interpret/compile/self-host)
  specs: 32/32 files, 0 failures
2026-06-08 00:40:56 -04:00
Yogthos
f6bd22ae94 docs: phase-5.md — implementation + testing plan for true laziness
Step-by-step plan for jolt-c09 (Phase 5 of the clojure.core migration):
current state of the LazySeq machinery and the eager gaps, the cardinal
laziness rules, leaf-first transformer conversion order, realization-boundary
audit, the representation decision (lazy-seq vs eager-vector for map over a
vector), and a testing strategy built around a deadlined subprocess harness
(infinite realizations are CPU-bound and uninterruptible in-process).
2026-06-07 22:11:12 -04:00
Yogthos
bfac1fc444 core: Phase 4 — move ns-name and the array reads/aset to the overlay
ns-name is pure over get + symbol. aget/alength read jolt's mutable backing
arrays (nth/count), and aset writes a slot through jolt.host/ref-put!; both
handle the multi-dimensional form by walking. The array constructors
(object-array/make-array/to-array/...) stay native — they build the mutable
backing. Added array spec cases; ns-name already covered.
2026-06-07 21:52:13 -04:00
Yogthos
790f54d2b3 core: Phase 4 — move future-done?/future-cancelled? to the overlay
Pure reads of the future's :cached/:cancelled slots over future? + get.
future? stays native (deref/future-cancel/realized? call it), as do future-call
and future-cancel (OS threads). Covered by the existing futures spec.
2026-06-07 21:41:11 -04:00
Yogthos
a9403e26b0 core: Phase 4 — move vreset!/vswap! to the overlay (reusing ref-put!)
vreset! sets the volatile's :val slot through the jolt.host/ref-put! primitive
added in the last batch; vswap! is pure over vreset! + get. The volatile!
constructor stays native (it builds the mutable box). Covered by the existing
state spec.
2026-06-07 21:27:20 -04:00
Yogthos
4d5aa8c903 core: Phase 4 — move atom peripheral ops to the overlay over a host primitive
First host-primitive batch. Adds jolt.host/ref-put! — the minimal mutation
kernel (set/remove a key on a mutable reference cell) the overlay can't express
over core fns — and moves seven atom ops out of the seed:

  swap-vals!/reset-vals!/compare-and-set! compose the native swap!/reset!/deref
  (which already validate and notify watches); get-validator is a slot read;
  add-watch/remove-watch/set-validator! mutate the atom (or its watches table)
  via ref-put!.

atom/swap!/reset!/deref and atom-validate/atom-notify-watches stay native — the
compiler depends on them and they're hot. compare-and-set! keeps value-equality
semantics, matching the prior Janet behavior. Covered by the existing state spec.
2026-06-07 21:22:35 -04:00
Yogthos
f410bec2ec core: Phase 4 — move tagged-value predicates to the overlay
atom?/volatile?/reader-conditional?/tagged-literal?/record?/chunked-seq? all
just read a value's kind from :jolt/type (records from :jolt/deftype), and get
returns nil on non-tables, so the predicates are pure over get with no host
surface. Constructors stay native. delay?/transient? keep their Janet defns —
they have internal callers (force, conj/count/persistent! etc.). chunked-seq?
is always false (no chunked seqs until Phase 5). Added a tagged-value spec
group covering positive and negative cases for each.
2026-06-07 20:24:24 -04:00
Yogthos
7f68a5872c core: Phase 4 — move ex-data/ex-message/ex-cause to the overlay
The ex-info value exposes :jolt/type/:message/:data/:cause via get (it's the
caught value too), so the accessors are pure over get — no host surface. The
constructor (ex-info) stays in Janet since it wires into throw. A thrown
non-ex-info arrives wrapped as {:jolt/type :jolt/exception :value v}; the
overlay unwraps that, matching the old Janet helper. ex-cause now unwraps and
returns nil (not false) on non-ex values, matching Clojure. Added non-ex-info
and string regression cases.
2026-06-07 20:17:03 -04:00
Yogthos
fcdf0ff535 core: Phase 4 — move reduce-kv to the overlay, fixing the vector case
reduce-kv is pure over reduce/keys/get/nth, so it moves to 20-coll with no
host surface. Both branches fold through reduce, which fixes two latent bugs
in the Janet version: on a vector it saw the pvec as a table and folded over
its internal keys instead of the indices, and it ignored reduced entirely.
nil folds to init. Added vector-index, reduced, and nil spec cases plus a
3-mode conformance case for the vector fix.
2026-06-07 20:05:25 -04:00
Yogthos
e6ff4b8a77 core: start Phase 4 — move vary-meta and namespace-munge to the overlay
First Phase 4 batch: the host-coupled fns whose logic is pure composition of
existing core primitives, so no new jolt.host surface is needed. vary-meta is
just (with-meta obj (apply f (meta obj) args)); namespace-munge is a char map
over str. Both land in the 20-coll tier; the Janet core-* defns and bindings
are gone. Added namespace-munge spec cases and a vary-meta extra-args case.

The heavily host-coupled fns (atom/var/transient internals, arrays, proxy,
futures) stay in Janet pending a thin host-primitive expansion.
2026-06-07 20:00:19 -04:00
Yogthos
8bc5bd6f61 core: port when-let to the overlay — finishes the Phase 3 macro migration
Last core macro still in Janet. Goes in 00-syntax (not 30-macros with its
if-let/if-some/when-some siblings) because 20-coll's not-empty uses it and
20-coll loads before 30. Via `let` so the binding form may destructure,
matching Clojure — the old Janet version used let* and wouldn't.

Drops core-when-let + the sym* helper + the intern entry, and empties
core-macro-names (no seed binding is a macro anymore).
2026-06-07 19:49:27 -04:00
Yogthos
77e3e3afcf core: move destructure from the Janet seed to the Clojure overlay
Port clojure.core/destructure into jolt-core/clojure/core/00-syntax.clj
and drop core-destructure (plus the d-* helpers) from core.janet. The
expander is now self-hosted, shrinking the seed per the jolt-1j0 epic.

It's def+fn* rather than defn because defn isn't defined yet at that
point in the syntax tier. Only the let macro consumes its output, so let
now splices [~@(destructure bindings)] to keep a tuple binding form.

Also fix a gap the old Janet version papered over via Janet's
keyword->string: :keys accepts keyword elements ({:keys [:major :minor]}),
so use name/namespace for the local + ns instead of str (which keeps the
colon). This was breaking sci's impl/namespaces.cljc. Added spec cases.
2026-06-07 19:39:06 -04:00
Yogthos
ad84b2904f core: fn param + loop destructuring, compiled (matching Clojure)
fn now desugars destructuring params like Clojure's maybe-destructured: each
non-symbol param becomes a gensym and the body is wrapped in a let that rebinds the
pattern, so fn* only ever sees plain params and the COMPILER handles it (it rejected
patterns before, falling back to the interpreter). loop follows Clojure too: gensym
one loop var per binding, loop* over those, destructure via an inner let, with an
outer let so later inits see earlier destructured names — recur arity stays correct.

Two representation gotchas: build the param/binding vectors via [~@..] so they're
tuple forms (conj yields a pvec the analyzer rejects), and use (symbol (str
(gensym))) since a bare (gensym) in an overlay macro body is a Janet symbol the
destructurer rejects.

Closes the fn/loop destructuring gaps. conformance 228x3, fixpoint, clojure-test-
suite 3930, full suite green, bench flat.
2026-06-07 18:55:56 -04:00
Yogthos
65b4b54b3f core: move lazy-seq and lazy-cat to the overlay
lazy-seq wraps its body in (make-lazy-seq (fn* [] (coll->cells (do ...)))); lazy-cat
wraps each coll in lazy-seq and concats (concat is already lazy). Both user-facing,
in 30-macros. Also drop two now-stale comments. Laziness preserved (lazy-seqs-spec
20/20, including infinite/self-referential).

conformance 228x3, fixpoint, clojure-test-suite 3930, full suite green.
2026-06-07 18:13:08 -04:00
Yogthos
1a1a1134d6 core: move the protocol/type macros to the overlay
defprotocol/defrecord/extend-type/extend-protocol/reify + the extend/proxy/
definterface stubs move to 30-macros (user-facing, not used by the compiler). They
emit Jolt's protocol/type special forms (protocol-dispatch/register-method/
make-reified/deftype).

The one subtlety: a protocol value is a :jolt/type-tagged struct, and the
interpreter can't tell an embedded opaque value from a tagged map literal being
constructed. So defprotocol builds it via a make-protocol fn call (exposed from
core) instead of an embedded literal — a fn result evaluates normally and even
compiles. reify emits a {kw (fn* ...)} map literal that make-reified evaluates
(build-eval-map yields a struct it can iterate, unlike a hash-map phm). defrecord
vecs its spliced field-let bindings (a lazy mapcat seq won't splice) and uses an
explicit fresh-sym for the map-> param (auto-gensym doesn't cross nested
syntax-quotes).

protocols-spec 21/21, conformance 228x3, fixpoint, clojure-test-suite 3930, full
suite green.
2026-06-07 18:06:55 -04:00
Yogthos
08c796c145 core: move defn and defn- to the syntax tier
defn drops an optional leading docstring/attr-map then emits (def name (fn* ...)).
Single- and multi-arity both reduce to (fn* ~@body) so no arity branching is needed.
map? is true for symbol forms in Jolt, so the attr-map strip is guarded with symbol?.
defn- delegates to defn (privacy isn't enforced, as in Clojure's own defn-). Placed
before fresh-sym, which is itself a defn- now.

All five fundamental macros (fn/let/loop/defn/defn-) are now in the overlay.
conformance 228x3, fixpoint, clojure-test-suite 3930, full suite green.
2026-06-07 17:36:06 -04:00
Yogthos
4eb2cf5c46 core: move let and loop to the syntax tier
let -> let* with destructuring pre-expanded via destructure (now exposed as a
clojure.core fn, which it is in Clojure too) so the compiler sees plain bindings —
analyze-bindings rejects patterns as uncompilable. loop -> loop* with raw bindings,
matching the prior Janet macro: loop can't pre-destructure without breaking recur
arity, so the interpreter handles pattern loops and the compiler falls back.

conformance 228x3, fixpoint, clojure-test-suite 3930.
2026-06-07 17:29:39 -04:00
Yogthos
85cdb97320 core: move fn to the syntax tier
fn -> fn* is a one-line head-swap (the analyzer treats fn* as the primitive). It's
in 00-syntax since the analyzer, kernel, and other overlay macro bodies all use fn.
First of the fundamental macros to move.

conformance 228x3, fixpoint, clojure-test-suite 3930.
2026-06-07 17:22:30 -04:00