Commit graph

55 commits

Author SHA1 Message Date
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
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
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
Yogthos
023fe637ce core: move for to the syntax tier (and fix multi-group :when)
Port of core-for: desugar a comprehension to nested map/mapcat over the binding
colls, :let -> let*, :while -> take-while on the coll. Lives in 00-syntax because
doseq (already there) expands to it; the macro body uses only kernel/seed fns so it
runs at analyzer-build time. doseq no longer depends on a Janet macro.

Fixed a latent bug the Janet macro had: :when wrapped the inner form in (list ...)
unconditionally, but for an outer binding group the inner form is already a seq, so
mapcat produced a seq-of-seqs instead of flattening. e.g.
  (for [x [0 1] :when (odd? x) y [:a :b]] [x y])
gave ((... ...)) instead of ([1 :a] [1 :b]). Now the (list ...) wrap is only applied
to the last group's scalar body; outer groups contribute their seq directly. Added
:let+:when, multi-group :when, and destructuring regression cases.

conformance 228x3, fixpoint, clojure-test-suite 3930, full suite green, bench flat.
2026-06-07 16:55:21 -04:00
Yogthos
40b0f525f3 core: move doseq to the syntax tier
Same shortcut as the prior Janet macro — realize a for comprehension with count
for side effects, return nil — so for keeps handling :when/:let/:while and multiple
bindings. Lives in 00-syntax because the analyzer uses doseq (analyze-try). Added
:when/:while/:let/returns-nil regression cases.

conformance 228x3, fixpoint, clojure-test-suite 3930, full suite green.
2026-06-07 16:40:24 -04:00
Yogthos
646744ca1d self-host: gate the analyzer build on the kernel tier; move case/cond-> to the overlay
The real bug behind the case/cond-> fixpoint regression wasn't gensym — it was
build ordering. A top-level defn in a pre-kernel tier (the fresh-sym helper these
macros need) gets compiled in compile mode, and that lazily builds the self-hosted
analyzer via ensure-analyzer. 00-syntax loads before 00-kernel, so the analyzer
was built against a core missing mapv/second/peek/...: its references to them were
interned as forward-ref nil cells in jolt.analyzer. Those nil cells then shadowed
the real clojure.core defs when the analyzer rebuilt itself (stage2), so the
analyzer's own mapv calls went to nil — 'Cannot call nil as a function'. Only
variadic-heavy paths (juxt+mapv) surfaced it.

build-compiler! already documented that the kernel must be loaded first; nothing
enforced it. Gate ensure-analyzer on a :kernel-ready? flag set after the kernel
tier loads. A pre-kernel compile now falls back to the interpreter (compile-and-eval
already handles that) instead of building the analyzer too early. fresh-sym ends up
interpreted during 00-syntax load, which is fine.

With ordering fixed, case and cond-> move to 00-syntax (they need a real gensym, so
syntax-quote auto-gensym alone won't do). conformance 228x3, fixpoint stage1==2==3,
clojure-test-suite 3930, full suite green, bench flat.
2026-06-07 16:29:50 -04:00
Yogthos
d0d48f0ebd core: move ->/->>/declare to the syntax tier
Threading macros (recursive; the expand-once cache makes that free) and declare
(a no-op on Jolt — forward refs resolve via pending cells). They live in 00-syntax
because the analyzer itself uses -> and declare; validated by conformance 228x3
(the bootstrap-compiled analyzer expands them).

conformance 228/228 x3, clojure-test-suite 3930.
2026-06-07 10:18:56 -04:00
Yogthos
613aaa5451 core: move and/or/cond/when-not to the overlay (enabled by expand-once cache)
The hot control macros now live in the Clojure syntax tier. This was blocked
before: as interpreted overlay macros they re-expanded on every eval, timing out
a battery file (3930->3911). With the expand-once macro cache (prior commit) they
expand a single time with zero runtime cost, so moving them is free.

conformance 228/228 x3, clojure-test-suite 3930 (9 timeouts, not 10), full suite
green, bench flat (overlay vs janet macros within noise).
2026-06-07 10:08:10 -04:00
Yogthos
ed118b9ce6 migration doc: phase 3 status + the hot-macro expansion-speed finding 2026-06-07 08:47:04 -04:00
Yogthos
d2d33a2ea9 core: syntax tier — move when to the overlay ahead of the kernel (jolt-1j0 phase 3)
New 00-syntax tier loaded FIRST (before 00-kernel), interpreted defmacros, so the
control macros the compiler and every later tier depend on can live in Clojure.
Validated by moving when: the kernel tier, self-hosted analyzer and seq/coll
tiers all compile against the overlay when. Constraint: syntax-tier macros may use
only special forms + core-renames seed primitives (not second/peek/kernel fns).

conformance 228/228 x3, full suite green.
2026-06-07 08:37:19 -04:00
Yogthos
3dafa60e65 core: move binding macro to overlay
Phase 3 batch 6 (jolt-1j0). binding installs an array-map var->value thread
frame and restores it on exit via try/finally. Dynamic rebinding is seen by
called fns. This completes the cleanly-portable safe macros (19 total).

conformance 228/228 x3, full suite green.
2026-06-07 01:13:22 -04:00
Yogthos
394bbe07c3 core: move condp macro to overlay
Phase 3 batch 5 (jolt-1j0). condp with a recursive emit building the nested if
chain, including the test :>> result-fn form and the trailing default. This
exhausts the cleanly-portable safe macros.

conformance 228/228 x3, full suite green.
2026-06-07 01:08:44 -04:00
Yogthos
24b217d314 core: move cond->>/assert/delay/future/letfn macros to overlay
Phase 3 batch 4 (jolt-1j0), 5 macros. cond->> (thread-last) is safe; cond->
stays (compiler uses it). delay/future expand to make-delay/future-call host
fns; letfn builds its fn* binding via a template (cons/list in a macro body make
a plist the evaluator can't call as a form).

conformance 228/228 x3, clojure-test-suite 3930, full suite green, bench flat.
2026-06-07 01:04:21 -04:00
Yogthos
31a257ce70 core: move as->/some->/some->>/doto/when-first macros to overlay
Phase 3 batch 3 (jolt-1j0), 5 macros. Establishes the macro-body toolkit:
fresh-sym = (symbol (str (gensym))) for a shared explicit jolt symbol (a bare
(gensym) returns a Janet symbol the destructurer rejects), and splicing binding
pairs into a TEMPLATE vector so core-let sees a tuple form, not a runtime pvec.

when-first stays first-based (lazy-safe): Clojure's (seq coll) form realizes an
infinite coll like (repeat nil) under Jolt's eager evaluator and hangs — the
per-batch battery caught it (when_first.cljc). Clojure-correct seq form waits on
Phase 5 laziness.

conformance 228/228 x3, clojure-test-suite 3930, full suite green, bench flat.
2026-06-07 00:55:19 -04:00
Yogthos
e611034550 core: move if-let/if-some/when-some/while/dotimes macros to overlay
Phase 3 batch 2 (jolt-1j0), 5 user-facing macros as defmacro + syntax-quote.
The conditional-binding macros use the auto-gensym temp# idiom so the name binds
only in the taken branch (the else/empty branch sees the outer scope, carrying
forward the earlier scope fix). when-let stays in Janet for now — 20-coll uses
it, so it must remain available before the macro tier loads.

Note: gensym in a macro body resolves to Janet's 0-arity builtin, so explicit
(gensym "prefix") fails — use template auto-gensym (foo#) instead.

conformance 228/228 x3, full suite green (incl. the if-let scope regression spec,
now exercising the overlay macro), clojure-test-suite 3930, bench A/B flat.
2026-06-07 00:38:02 -04:00
Yogthos
330a3a23d9 core: start macro tier — move comment/if-not to the Clojure overlay (jolt-1j0 phase 3)
New 30-macros tier (registered after 20-coll) for user-facing macros expressed as
defmacro + syntax-quote instead of hand-built Janet form-transformers. Validated
end to end: overlay-defined macros expand in interpret, compile AND self-host
modes (conformance 228/228 x3).

Moved comment and if-not; removed their Janet core-X fns + core-macro-names
entries. Note: Jolt defmacro is single-arity, so multi-arglist macros become one
arglist with & rest / destructuring (if-not uses [test then & [else]]). Macros
used by the compiler or earlier tiers (and/or/when/cond/case/cond->/->/declare/
doseq/when-let) stay in Janet until a load-order story exists for them.

full suite green, clojure-test-suite 3930.
2026-06-07 00:29:34 -04:00
Yogthos
f0e111563b core: move dedupe + seq-to-map-for-destructuring to overlay (+ tests)
Eighth pure-fn batch (jolt-1j0 phase 2), 2 leaf fns. dedupe is eager consecutive
dedup (no transducer arity, as before); seq-to-map-for-destructuring is the
internal &{:keys} helper. Largely exhausts the easy pure-eager tier.

conformance 228/228 x3, full suite green (incl. destructuring).
2026-06-07 00:14:40 -04:00
Yogthos
fec5a10ccf core: move some/flatten/interleave/rationalize to overlay (+ spec tests)
Seventh pure-fn batch (jolt-1j0 phase 2), 4 leaf fns. flatten is the canonical
tree-seq form, which also flattens lists (sequential?) — the prior Janet impl's
seqish? predicate missed them. some/interleave eager; rationalize identity (no
ratio type).

conformance 228/228 x3, clojure-test-suite 3929 -> 3930 (flatten lists fix),
full suite green, bench A/B flat.
2026-06-07 00:09:27 -04:00
Yogthos
921b395dfd core: move comparator/reductions/tree-seq to overlay (+ spec tests)
Sixth pure-fn batch (jolt-1j0 phase 2), 3 leaf fns, now with regression tests
per the per-batch workflow. reductions is the canonical form, so (reductions f [])
calls (f) -> [0] instead of the prior []; tree-seq is eager pre-order DFS.

conformance 228/228 x3, clojure-test-suite 3929, full suite green, bench A/B flat.
2026-06-06 23:58:11 -04:00
Yogthos
c35cf7bdbc migration doc: make regression tests part of the per-batch workflow 2026-06-06 23:51:40 -04:00
Yogthos
f22eb660d3 fix nthrest empty-list semantics + if-let/when-let/if-some/when-some scoping
Two correctness bugs found while migrating:

- nthrest returned nil where Clojure returns () : for n>0 the walk yields
  (seq xs) wrapped in (or ... ()), so an exhausted/nil walk is () not nil
  (only n<=0 returns coll). Both the prior Janet impl and the first overlay
  port got this wrong. nthrest.cljc 13/1/0 -> 14/0/0.

- if-let/when-let/if-some/when-some leaked their binding into the else branch:
  they wrapped the whole if in (let* [name val] ...), so (let [x 5] (if-let
  [x nil] x x)) returned nil instead of 5. Fixed to bind a fresh temp around the
  if and rebind the name only inside the taken branch, matching Clojure.

conformance 218/218 x3, clojure-test-suite 3928 -> 3929.
2026-06-06 23:42:08 -04:00
Yogthos
185cf30c1d core: move nthrest/abs/NaN?/object?/undefined?/keyword-identical? to overlay
Fifth pure-fn batch (jolt-1j0 phase 2), 6 leaf fns. nthrest uses the canonical
loop form (same neutral 13/1/0 on nthrest.cljc as the prior Janet version; the
1 fail is a pre-existing platform-conditional case). object?/undefined? are
always false on Jolt.

conformance 218/218 x3, clojure-test-suite 3928, core-bench A/B noise-only.
2026-06-06 23:34:31 -04:00
Yogthos
4aec21e603 core: move distinct?/replace/nthnext/bounded-count/run!/completing to overlay
Fourth pure-fn batch (jolt-1j0 phase 2), 6 leaf fns. distinct? now uses
value-semantics (the prior Janet impl keyed a table by identity, so equal
collections compared distinct); nthnext uses the canonical loop form, which
fixes (nthnext nil nil) => nil and the nil-count cases the prior pre-check threw
on. replace preserves nil values via get-with-default.

conformance 218/218 x3, clojure-test-suite 3927 -> 3928 (canonical nthnext),
core-bench A/B flat. Per-batch gate caught a transient -1 from the first
nthnext rewrite; fixed before commit.
2026-06-06 23:26:36 -04:00
Yogthos
b1daa4bcbd core: move numeric predicates + replicate/take-last/drop-last to overlay
Third pure-fn batch (jolt-1j0 phase 2), 9 leaf fns: ratio?/decimal? (always
false on Jolt), rational?/nat-int?/neg-int?/pos-int? (reduce to int?), replicate,
take-last, drop-last. Canonical Clojure defs over already-frozen primitives.

conformance 218/218 x3, clojure-test-suite 3927. core-bench is load-sensitive in
absolute terms; A/B under identical load shows this batch == prior (no regression).
2026-06-06 23:14:49 -04:00
Yogthos
681b007b7a core: move split-at/split-with/ident?/qualified-ident?/simple-ident? to overlay
Second pure-fn batch (jolt-1j0 phase 2). All leaf fns; canonical Clojure defs.
split-with now returns seqs for the parts (drop-while is a seq), matching
Clojure rather than the prior all-vector result — value-equal either way.

conformance 218/218 x3, clojure-test-suite 3927, core-bench 2333ms (~baseline).
2026-06-06 23:05:24 -04:00