Commit graph

382 commits

Author SHA1 Message Date
Dmitri Sotnikov
19d59cba85
Merge pull request #52 from jolt-lang/leaf-shrink-batch4
core: Stage 3 — leaf batch 4: sort-by + rand family + char tables
2026-06-10 16:30:23 -04:00
Yogthos
3d7de8ff90 core: Stage 3 — leaf batch 4: sort-by + the rand family + char tables to the overlay
sort-by, rand-int, shuffle, random-uuid, char-escape-string, and
char-name-string move to 20-coll over the two host seams that stay (rand and
sort — they ARE the randomness/ordering primitives). Canonical upgrades ride
along: sort-by defaults its comparator to compare, so nil sorts FIRST (the
kernel fn used host ordering and put nil last); rand-int truncates toward
zero via int (the kernel fn floored, wrong for negative n); shuffle is a
pure-functional Fisher-Yates over vector assoc and rejects non-collections
(a string is seqable but not shuffleable, as on the JVM — the honest gate
caught that one); random-uuid builds over rand-int and validates through
parse-uuid; the char tables are char-keyed Clojure maps (Clojure's shape —
the seed keeps its private code-keyed copies for pr-render).

22 new spec rows. Gate: jpm test exit 0 verified, suite 4698 >= 4660, bench
parity with main back-to-back (4733 vs 4817).
2026-06-10 16:30:17 -04:00
Dmitri Sotnikov
17f2474be2
Merge pull request #51 from jolt-lang/early-defn-recompile
core: staged recompile for early defns; keys/vals/empty? leave the seed (jolt-4j3)
2026-06-10 16:16:31 -04:00
Yogthos
63eb6eca6e core: staged recompile for early defns; keys/vals/empty? leave the seed (jolt-4j3)
recompile-defns! is the defn analog of recompile-macros!: pre/at-kernel
overlay defns (00-syntax's destructure and friends; the kernel tier too in
interpret mode) load as interpreted closures, the evaluator stashes their fn
source on the var (:defn-src, scoped by a flag only api/load-core-overlay!
sets), and the end-of-init pass compiles them and swaps the var root. With
that in place, keys/vals/empty? — the fns the 00-syntax expanders call at
expansion time — move to the top of 00-syntax as raw fn* defs (canonical:
keys/vals project (seq m), so sorted maps come back in comparator order and
(keys {}) is nil; empty? keeps O(1) count dispatch with seq's cell check only
for the lazy/list fallback). The sorted tier drops its now-dead :keys/:vals
ops.

Correctness fixes that surfaced once the gate was run with a REAL exit code
(the previous 'jpm test | grep' gates reported grep's exit and masked spec
failures across #48-#50):
- map conj is strict again: a non-nil/non-map arg must be a 2-element vector
  ('Vector arg to map conj must be a pair'), and merge inherits it — the
  batch-2 canonical merge had silently dropped the validation
- conj onto a lazy seq prepends (it fell into the MAP fallback); upstream
  clojure.data/diff relies on (conj seq x) via set/union over keys, so diff
  now matches Clojure exactly
- (seq {}) / (seq #{}) / empty phm are nil, not ()
- key/val are strict (a plain vector is not an entry); find mints a REAL
  entry as the first entry of a one-entry map, nil values intact
- the sci avoid-method-too-large stub passes its registry map through
  instead of returning a raw host table (strict conj rejected it; sci's
  clojure-core registry is also no longer discarded)

Test updates: lazy-infinite pins take-nth realization at 5 (was 7 — the
canonical lazy impl realizes fewer); self-host asserts the analyzer IS loaded
in interpret mode (compiled expanders, PR #50) and is NOT in the
:compile-macros? false oracle. 18 new maps-spec rows.

Gate: jpm test exit 0 (verified directly, not through a pipe), conformance
326x3, suite 4698 >= 4660.
2026-06-10 16:16:23 -04:00
Dmitri Sotnikov
e6f562c175
Merge pull request #50 from jolt-lang/macro-expansion-path
core: compiled macro expansion in every mode (+123 suite passes)
2026-06-10 15:37:39 -04:00
Yogthos
dc1f6d9755 core: compiled macro expansion in every mode (+123 suite passes)
Macros are ordinary compiled fns in Clojure's model; compile mode has had
that since the staged bootstrap, but interpret mode — the conformance
battery's default — kept interpreted expanders, so every distinct (and ...)/
(cond ...) call form, and every fresh form produced by a recursive expansion,
ran an interpreted closure. ensure-macros-compiled! now runs in every mode:
interpret-mode init loads the tiers fast-interpreted, then one pass at the
end builds the analyzer (which itself stays interpreted there) and compiles
all stashed expanders; user defmacros after init compile too. The new
:compile-macros? opt (JOLT_INTERPRET_MACROS=1) preserves the fully-
interpreted oracle, and joins the ctx-image cache key.

Battery: 4700 pass / 90 clean files / 7 timeouts, from 4577 / 87 / 9 — two
macro-heavy files stopped timing out and 149 more assertions execute. The
compiled-expander delta proper is +67 passes (oracle mode on the same tree
measures 4633). Baselines raised 4540->4660, clean 86->88. Interpret init
grows 0.12s -> 1.12s for the analyzer build; init-cached amortizes it to ~5ms
per process.

New macro-expansion-test pins: expanders compiled in interpret mode (core +
post-init user defmacros), uncompilable bodies fall back interpreted and
still work, compile mode unchanged, oracle opt-out honored.

Follow-up filed (jolt-4j3): the same staged-recompile treatment for early
overlay DEFNS, which is what still pins keys/vals/empty? to the seed.
2026-06-10 15:37:32 -04:00
Dmitri Sotnikov
1ec2fa4adf
Merge pull request #49 from jolt-lang/leaf-shrink-batch3
core: Stage 3 — leaf batch 3: empty/assoc-in/update-in + interpose/take-nth
2026-06-10 15:26:47 -04:00
Yogthos
780b6474ff core: Stage 3 — leaf batch 3: empty/assoc-in/update-in + interpose/take-nth to the overlay
empty, assoc-in, and update-in move to 20-coll.clj as the canonical recursive
ports; interpose and take-nth move to the lazy tier WITH their canonical
transducer arities (volatile-based), so the seed's td-interpose/td-take-nth
helpers go too. (empty lazy-seq) is () now — the kernel fn returned a bare
host table for it.

keys/vals/empty? stay put for now: they're expander-coupled — 00-syntax's
when/and/or/cond/destructure expanders call them at expansion time, which
happens during the kernel-tier compile, before any later tier exists. They
move when early defns get the staged-recompile treatment macros already have.

26 new spec rows (incl. transducer arities through sequence/into and laziness
checks against (range)). Gate green: conformance 326x3, suite >= baseline,
full jpm test.
2026-06-10 15:26:41 -04:00
Dmitri Sotnikov
1340b7c52f
Merge pull request #48 from jolt-lang/leaf-shrink-batch2
core: Stage 3 — leaf batch 2: sixteen more seed fns to the overlay; retire MIGRATION.md
2026-06-10 15:17:07 -04:00
Yogthos
0e71b193e5 core: Stage 3 — leaf batch 2: sixteen more seed fns to the overlay; retire MIGRATION.md
key/val/select-keys/zipmap/merge/merge-with/get-in/memoize/partial/
trampoline/some?/true?/false?/max/min/reverse move to 20-coll.clj as the
canonical Clojure definitions, plus find — which was previously missing from
jolt entirely (select-keys/merge-with/memoize build on it). Two behavior
fixes ride along: memoize now caches nil results (the kernel fn re-computed
them — canonical find-based impl), and conj of nil onto a map is a no-op as
in Clojure (it errored; the canonical merge relies on it). max/min keep the
JVM NaN behavior by construction (pairwise >/<). not= stays: the kernel tier
(subvec) uses it.

One new tier-ordering rule, learned the hard way: a tier may only use macros
from tiers that load BEFORE it — memoize's if-let (30-macros) broke compiled
init while interpret mode passed, because compile expands macros at tier
load and the interpreter expands lazily. Now documented in the migration
workflow note.

MIGRATION.md is gone — task tracking lives in beads (jolt-ded; the per-batch
workflow, tier-order rules, perf wall, and remaining candidates are in bd
memory core-migration-workflow). The doc's candidate lists had gone stale
against the actual seed anyway.

43 new spec rows. Gate green: conformance 326x3, suite >= baseline, full
jpm test, bench at parity with main back-to-back (4851 vs 4831 TOTAL).
2026-06-10 15:16:47 -04:00
Dmitri Sotnikov
af34d94870
Merge pull request #47 from jolt-lang/leaf-shrink-batch
core: Stage 3 — leaf batch: nine more seed fns to the overlay
2026-06-10 14:57:10 -04:00
Yogthos
3faee14271 core: Stage 3 — leaf batch: complement/fnil/clojure-version/bigdec/numerator/denominator/supers/munge/test to the overlay
Nine more seed leaves move to 20-coll.clj (verified leaf-by-leaf: defn +
core-bindings entry only, no internal callers). fnil is upgraded to Clojure's
canonical 2/3/4-arity — it patches only the first 1-3 arguments; the old
kernel fn patched every position it had a default for, which Clojure does
not. The rest carry their kernel semantics over unchanged (bigdec is a
double, numerator/denominator throw, supers is #{}, munge rewrites dashes).

16 new spec rows incl. the fnil arity-contract cases. Gate green:
conformance 326x3, suite 4577, full jpm test (2:18 — first full run with the
ctx image cache on main).
2026-06-10 14:56:50 -04:00
Dmitri Sotnikov
ee6447adae
Merge pull request #46 from jolt-lang/in-reader-family
core: Stage 3 — the *in* reader family is Clojure (50-io tier)
2026-06-10 14:53:17 -04:00
Yogthos
c665b8eb9f core: Stage 3 — the *in* reader family is Clojure (50-io tier)
*in*, read-line, read, with-in-str, and line-seq land as a new overlay IO
tier (core/50-io.clj). *in* is a dynamic var holding a reader — a plain map
of two closures, :read-line-fn (next line, nil at EOF) and :read-fn (next
form, advancing past exactly that form). The default *in* reads real stdin
with a shared leftover buffer, so read and read-line interleave correctly;
with-in-str rebinds *in* to a string reader over one atom-held buffer —
(read) consumes its form, a following (read-line) returns the rest of that
line, as in Clojure. read has the 0/1/3 arities (EOF throws, or returns
eof-value when eof-error? is false).

The Janet seed grows two seams next to read-string: __stdin-read-line (one
line off stdin, newline stripped) and __parse-next (one form off a string ->
[form rest], nil at end of input) — and loses the line-seq stub.

Two traps hit and documented for future tiers: a map LITERAL with :jolt/type
as a key is read as a tagged form (don't tag overlay value maps), and a
leftover seed stub holding the same name breaks direct-linked self-recursion
— the overlay line-seq's recursive call bound to the stub's root, truncating
after one line. The stub's string-splitting behavior is kept as a documented
extension.

20 new io-spec rows (read-line EOF/interleave, read arities + eval round-trip,
line-seq incl. real-stdin paths). Gate green: conformance 326x3, suite 4577,
full jpm test.
2026-06-10 14:52:40 -04:00
Dmitri Sotnikov
101027a0cd
Merge pull request #44 from jolt-lang/sorted-colls-clojure
core: Stage 3 — sorted collections are pure Clojure (canonical port)
2026-06-10 14:51:50 -04:00
Dmitri Sotnikov
8cec486588
Merge pull request #43 from jolt-lang/transients-rfc
docs: RFC 0003 — transients design note
2026-06-10 14:51:23 -04:00
Dmitri Sotnikov
11691ddb2b
Merge pull request #42 from jolt-lang/ctx-image-cache
core: AOT context image — init-cached recovers the bootstrap cost across processes
2026-06-10 14:51:19 -04:00
Yogthos
55a3ebf93f core: Stage 3 — sorted collections are pure Clojure (canonical port)
sorted-map/sorted-map-by/sorted-set/sorted-set-by/sorted?/sorted-map?/
sorted-set?/subseq/rsubseq now live in their own overlay tier (25-sorted.clj).
A sorted coll is a tagged host table with a comparator-ordered :entries
vector, a 3-way :cmp, and the tier's op implementations ATTACHED to the value
(:ops map): the seed's conj/assoc/get/seq/count/... branches are each a
one-line call through (coll :ops), so the ops travel with the value — correct
across contexts, forks, and AOT images, no module-level hooks to re-wire.
The host surface grows by three minimal value primitives: jolt.host/
tagged-table, ref-put! (already there), and ref-get — a raw field read,
because plain get on a sorted coll IS the comparator lookup and reading
:entries with it recurses.

This fixes a pile of Clojure-correctness gaps the Janet kernel had:
- lookup/membership now go through the COMPARATOR: (contains? (sorted-set 1)
  1.0) was a deep= scan, (conj (sorted-set 1) 1.0) and assoc of a
  comparator-equal key now no-op/replace as in Clojure
- equality is representation-agnostic: (= (sorted-map :a 1) {:a 1}) and
  (= (sorted-set 1 2) #{1 2}) were false
- iteration was broken: (map inc (sorted-set 3 1 2)) errored
  (realize-for-iteration and coll->cells had no sorted branches)
- empty?/empty saw the host wrapper, not the collection: (empty? (sorted-map))
  was false, (empty sc) returned a bare table; it now keeps the comparator
- sorted colls canonicalize as map keys; comparator fns may be boolean
  predicates or 3-way (Clojure's fn->comparator)
- sorted-map throws on odd kv count; conj nil is a no-op

Also fixes jolt-h86 en passant: into-conj had no branch for sets (or sorted
colls) and silently returned the target unchanged — (into #{} [:a :b]) was
#{}. The fallback now folds conj. Regression rows in sets-spec.

sorted-spec grows to 77 rows (comparator-based membership, equality,
empty/rseq/printing, seq-fn interop, subseq/rsubseq on maps). Gate green:
conformance 326x3, suite 4577 (vs 4566 prior — the battery gained rows),
sorted+sets specs, full jpm test, bench at parity with main back-to-back
(4521ms vs 4619ms TOTAL under identical load).
2026-06-10 14:39:02 -04:00
Yogthos
d6c5552fda docs: RFC 0003 — transients semantics and why they stay in the Janet seed
Pins down what a transient is in Jolt (tagged table over a native Janet
array/table, canonical-keyed for maps/sets), where behavior deviates from the
JVM (O(n) transient/persistent! edges with O(1) native ops between, no
owner-thread check — same as Clojure 1.7+, transient-of-list leniency), and
the three reasons the machinery is seed-resident rather than a migration
candidate: it IS the mutation kernel, it sits under the seed's own dispatch,
and the value layer is declared irreducible. Exists so the kernel-shrink
ladder (jolt-tzo) doesn't revisit transients every round.
2026-06-10 13:58:47 -04:00
Yogthos
0e3584884f core: AOT context image — init-cached recovers the bootstrap cost across processes
init in compile mode is ~2.4 s (tier loading, analyzer self-compile, macro
recompilation), paid by every process that builds a ctx from source — each
jpm-test file, embedders, workers. init-cached marshals the built ctx to a
disk image (same root-env dicts as snapshot/fork) and later processes
unmarshal it in ~5 ms, any process: nothing from the baking process is
needed at load.

The cache key fingerprints the embedded .clj stdlib (which covers jolt-core:
analyzer, IR, core tiers), the .janet seed sources next to the module, the
janet version, the init opts, and the env knobs that shape a ctx (JOLT_PATH/
MUTABLE/AOT_CORE/FEATURES) — any change rebuilds. Corrupt or non-ctx images
fall back to a rebuild (unmarshal of garbage can 'succeed' with a scalar, so
the shape is checked, not just the throw). Writes are atomic (tmp + rename)
so racing cold starts never publish a torn image. JOLT_NO_IMAGE_CACHE=1
opts out; JOLT_IMAGE_CACHE_DIR overrides the location (default TMPDIR).

Test consumers switch to init-cached (harness, suite-worker, conformance,
the behavioral unit/integration tests); tests that validate the bootstrap
itself (bootstrap-fixpoint, staged-bootstrap, aot round-trip, direct-linking)
and the deps tests (tmp-dir :paths would fragment the key) keep real init.
Full jpm test: 2:46 -> 1:58 (~29%). New ctx-image-test covers cold/warm,
cross-process load (subprocess runs defn/redef/macros/protocols/multimethods
off the baked image), per-opts keying, and corrupt-image fallback.
2026-06-10 13:57:37 -04:00
Dmitri Sotnikov
bf885078f9
Merge pull request #41 from jolt-lang/stage3-retire-bootstrap
Stage 3: retire the bootstrap compiler
2026-06-10 13:32:21 -04:00
Yogthos
cbab7f66df core: Stage 3 — retire the bootstrap compiler (compiler.janet deleted)
The 1104-line Janet bootstrap compiler existed to build jolt.ir/jolt.analyzer
and the kernel tier before the self-hosted analyzer could exist. It is
replaced by the interpreter + one fixpoint turn:

1. bootstrap-load-source loads the compiler sources INTERPRETED (the
   evaluator can run the analyzer — it always could).
2. After the overlay is up, self-compile-compiler! re-runs the kernel tier,
   jolt.ir, and jolt.analyzer through the SELF-HOSTED pipeline — the
   interpreted analyzer compiles itself, and steady state runs compiled with
   no bootstrap compiler involved.

Measured: init {:compile? true} 1093 -> ~2400 ms (the one-time interpreted
pass + self-compilation), but steady-state compilation is 2.8x FASTER
(100 forms: 134 -> 48 ms) — the self-hosted pipeline emits better code than
the bootstrap did. An AOT image for init cost is future work (aot.janet's
machinery is the natural vehicle).

The bootstrap's runtime kernel moves to backend.janet (jolt-runtime-env,
ctx-janet-env, build-map-literal); aot imports it from there. The
uncompilable-error? punt check unwraps the interpreter's exception struct
(the interpreted analyzer's throw arrives wrapped). compile-string/
compile-file (the bootstrap's source-text emitter API, no callers outside
the bootstrap's own unit test) are removed with it, as is compiler-test.

Gate green across everything incl. fixpoint stage1==2==3, AOT round-trip,
uberscript, CLI; conformance 326x3; suite 4566 >= 4540; bench in band.
2026-06-10 13:32:14 -04:00
Dmitri Sotnikov
90a35a5dc0
Merge pull request #40 from jolt-lang/stage3-shrink2-hierarchy
Stage 3: the hierarchy system is pure Clojure (canonical port)
2026-06-10 13:19:12 -04:00
Yogthos
c1a54fa2de core: Stage 3 — the hierarchy system is pure Clojure (canonical port)
make-hierarchy/derive/underive/isa?/parents/ancestors/descendants are now
Clojure in the overlay — Clojure's own pure-map implementation: a hierarchy
is {:parents {tag #{..}} :ancestors {..} :descendants {..}}, the 3-arity
forms are PURE (derive returns a new hierarchy), and the 1/2-arity forms swap
a private global-hierarchy atom.

This fixes three correctness gaps the Janet kernel had: multi-parent derive
(the kernel's :parents held a single parent per tag), TRANSITIVE descendants
(the kernel tracked direct children only), and vector-pair isa?
((isa? [child1 child2] [parent1 parent2])). Cyclic and duplicate derives now
behave like Clojure (throw / no-op).

Multimethod dispatch was the kernel's only internal caller: defmulti-setup's
dispatch closure now calls the overlay's isa? through a lazily-resolved var
(cached per multimethod); a :hierarchy option is an atom (deref per dispatch,
matching Clojure's var semantics) or a plain hierarchy map. The Janet kernel
(types.janet) and the core-* wrappers are deleted.

20 new hierarchy spec rows (pure 3-arity incl. cycle/duplicate edges, global
+ dispatch incl. custom :hierarchy atoms). Gate green: conformance 326x3,
suite 4566 >= 4540, all batteries, bench in the session band.
2026-06-10 13:19:05 -04:00
Dmitri Sotnikov
232e4b137c
Merge pull request #39 from jolt-lang/stage3-tier-shrink
Stage 3 tier shrink: 26 pure-over-core leaves move to the overlay
2026-06-10 13:10:37 -04:00
Yogthos
d4c065edfe core: Stage 3 tier shrink — 26 pure-over-core leaves move to the overlay
The representation predicates (sequential?/associative?/counted?/indexed?/
reversible?/seqable?, boolean?/double?/float?/infinite?, the qualified/simple
ident predicates), the realization boundaries doall/dorun, list*, find,
realized?/force, pop, the print-str family, and rand-nth/random-sample are
now Clojure in 20-coll, expressed over the overlay's own predicates and
primitives — no Janet representations referenced.

Several got MORE correct in the move: dorun honors its bounded-n arity (the
seed ignored n); find works on vectors by index (contains? gives it free);
indexed? no longer claims seq results; seqable? no longer claims arbitrary
tagged tables (atoms were "seqable"); realized? reads the tagged slots via
the established get pattern.

Seed: 3427 -> ~3200 lines, 371 -> ~345 core-* fns. Gate green (conformance
326x3, suite 4569 >= 4540, stdlib battery, all specs+unit); bench in noise.
2026-06-10 13:10:30 -04:00
Dmitri Sotnikov
6699ef47d3
Merge pull request #38 from jolt-lang/stage3-ns-var
*ns*: the current-namespace dynamic var
2026-06-10 13:05:22 -04:00
Yogthos
817495dd51 core: *ns* — the current-namespace dynamic var
*ns* is interned in clojure.core holding the current NAMESPACE OBJECT, kept
in sync by ctx-set-current-ns through a var table cached on the env (one
table put on the hot path; core-bench A/B neutral). A thread binding
(binding [*ns* ...]) shadows the root through var-get as usual. in-ns now
returns the namespace object (Clojure); str renders a namespace as its name
and pr-str as #namespace[name]. The ns-designator helper accepts namespace
objects (they are tagged STRUCTS — the old table?-based check never matched
them), so (ns-aliases *ns*) / (ns-unalias *ns* 'a) work — SCI and the jank
syntax-quote corpus use exactly that shape.

Known divergence (documented): inside an interpreted fn, *ns* reflects the
fn's defining ns (jolt's resolution model rebinds current-ns per call);
top-level and load-time reads match Clojure.

Gate green (conformance 326x3, suite 4572 >= 4540, all batteries,
specs+unit +8 *ns* rows); bench neutral.
2026-06-10 13:05:15 -04:00
Dmitri Sotnikov
4f872fce94
Merge pull request #37 from jolt-lang/stage3-turn2b
Stage 3 turn 2b: host IO, ns introspection, thread-binding family
2026-06-10 12:54:12 -04:00
Yogthos
d61c86a068 core: Stage 3 turn 2b — host IO, ns introspection, thread-binding family
The names turn 2a's leak removal exposed as honestly missing, now proper:

- slurp/spit/flush (host-classified): path-based IO over Janet files; spit
  takes :append; flush flushes *out*. printf prints formatted (no newline)
  over the existing format. file-seq walks paths via two host dir primitives
  through the overlay's tree-seq.
- ns-map / ns-unmap / ns-refers (ctx fns). ns-refers required fixing the
  refer MODEL: refer/use/:refer now map the SOURCE VAR into the target ns
  (the Clojure model) instead of copying its value into a new var — so
  source-ns redefinitions propagate, the :macro flag travels for free, and
  refers are identifiable by the var's home :ns.
- Thread-binding family: with-bindings*/with-bindings, bound-fn*/bound-fn,
  bound?, thread-bound?, get-thread-bindings. The captured binding map is a
  Janet struct keyed by the var tables — the exact frame representation
  var-get reads — so it re-pushes correctly (a phm frame is invisible to
  var lookup).
- load-string and eval interned as VALUES at the api layer (they need the
  loader's compile-or-interpret routing); the eval special form still
  handles direct calls.

Suite 4532 -> 4572 (baseline floor 4540 across timeout variance, clean 86),
conformance 326x3, stdlib battery, all specs+unit (+21 turn-2b rows).
Coverage: missing-portable 27 -> 10 (left: the *in*-model readers, the
with-local-vars/with-precision/extenders tail).
2026-06-10 12:53:47 -04:00
Dmitri Sotnikov
a75a26860b
Merge pull request #36 from jolt-lang/stage3-turn2
Stage 3 turn 2a: close the implicit Janet root-env leak
2026-06-10 12:43:36 -04:00
Dmitri Sotnikov
c6ccdcf4b1
Merge pull request #35 from jolt-lang/ci-node24-bump
CI: bump checkout/cache actions to v5 (Node 24)
2026-06-10 12:43:33 -04:00
Yogthos
c7b0ad9d84 core: Stage 3 turn 2a — close the implicit Janet root-env leak
resolve-sym's last resort silently resolved any unknown Clojure symbol
against Janet's root environment — leaking Janet builtins with JANET
semantics into Clojure code: (type 1) was Janet's :number, (gensym) returned
Janet symbols (the long-documented (symbol (str (gensym))) macro landmine
existed BECAUSE of this), compare/slurp/int?/any? likewise. The explicit
janet/ prefix is the deliberate interop channel; the implicit fallback is
gone — an unresolved symbol is an error.

What the leak was masking, now proper interned vars:
- gensym: jolt's own (already existed, never interned) — returns real jolt
  symbols; the macro landmine is dead
- compare: full Clojure total order (nil-first, numbers, strings, keywords,
  symbols by ns/name, booleans, chars, uuid/inst, vectors by length then
  elementwise; cross-type throws)
- type: :type metadata override, deftype/record tag as symbol, else a
  taxonomy keyword (host-classified)
- int?: core-integer? — which had a latent bug the leak hid: (integer?
  ##Inf) was true (floor of inf is inf); NaN/infinities now excluded
- any?: constantly true (Clojure 1.9; SCI's namespaces.cljc needs it)
- jolt.interop/janet-type now uses the explicit (janet/type x) channel
- evaluator-test uses init (a bare make-ctx resolved EVERYTHING via the leak)

Suite 4470 -> 4532+ pass / 86-87 clean (proper compare unlocks the sort
files); baselines raised. Conformance 326x3 (+5 rows), +22 predicate spec
rows, stdlib battery green, all specs+unit. Coverage dashboard now counts
previously-leak-resolvable names honestly (missing-portable 19 -> 27).
2026-06-10 12:43:08 -04:00
Yogthos
da69d00b2c ci: bump checkout/cache actions to v5 (Node 24)
GitHub forces Node 24 for JavaScript actions starting June 16 2026; the v4
actions run on deprecated Node 20. v5 of both run on Node 24.
2026-06-10 12:31:44 -04:00
Dmitri Sotnikov
d0c605ac9d
Merge pull request #34 from jolt-lang/fix-edn-regression
Fix strict-map? regression in clojure.edn; apply EDN built-in tags
2026-06-10 12:26:05 -04:00
Yogthos
d8ffe386e6 edn: fix strict-map? regression in edn->value; apply EDN built-in tags
CI caught what the local per-change gate missed (clojure-stdlib-suite-test
wasn't in the habitual gate list): strict map? (PR #28) correctly stopped
treating tagged structs as maps, which silently disabled edn->value's
(and (map? x) (= :jolt/set ...)) set-form branch — edn/read-string returned
raw reader forms for #{...}. Reader FORMS are now detected by :jolt/type
directly, never via map?.

While here, EDN's built-in tagged elements are applied properly: a
:jolt/tagged form routes through the registered data reader (__read-tagged,
a ctx-capturing core fn over :data-readers), so (edn/read-string "#uuid ...")
yields a real UUID value and #inst an instant — per the EDN spec. The
read_string battery goes 47 -> 50 (above the old 49 baseline: the uuid
asserts pass now); baseline raised to 50.
2026-06-10 12:25:45 -04:00
Dmitri Sotnikov
517f7ba762
Merge pull request #33 from jolt-lang/preflight-inst-sq
Pre-flight: #inst instant values + syntax-quote literal collapse
2026-06-10 12:19:54 -04:00
Yogthos
e58be2fbd2 core: #inst instant values + syntax-quote literal collapse (spec 2.3/2.4)
#inst (jolt-rnh): an instant is an immutable tagged struct
{:jolt/type :jolt/inst :ms <epoch-millis>} — equality and map-key hashing by
INSTANT, so different offsets denoting the same moment are =. The reader
parses RFC3339 with Clojure's partial-timestamp defaults (#inst "2020" is
2020-01-01T00:00:00.000Z) and errors on malformed input; inst?/inst-ms in
the overlay; pr-str prints the canonical
#inst "yyyy-MM-ddThh:mm:ss.fff-00:00" and round-trips; str gives the bare
RFC3339 string. Self-evaluating in the evaluator (like uuid/chars).

Syntax-quote (jolt-l2a): a syntax-quoted self-evaluating literal (string,
number, boolean, nil, keyword, char) collapses to the literal at READ time,
matching Clojure's reader — so nested/adjacent backticks over literals are
inert: (= "meow" ```"meow") is true (jank pass-adjacent). Symbols still
qualify and collections still template. General nested syntax-quote over
non-literals remains UNVERIFIED in spec S25.

Tests: inst-spec (18 cases incl. partial defaults, offsets, round-trip),
+10 literal-collapse reader rows, +5 conformance rows (321x3). Spec
02-reader S20/S25 updated to normative. Suite stable 4470/86.
2026-06-10 12:19:23 -04:00
Dmitri Sotnikov
094152a8c3
Merge pull request #32 from jolt-lang/fix-doc-refs
Fix doc/ -> docs/ references missed in the consolidation
2026-06-10 12:12:12 -04:00
Yogthos
b3f2b19bf7 docs: fix doc/ -> docs/ references missed in the consolidation merge 2026-06-10 12:11:53 -04:00
Dmitri Sotnikov
99f822b91a
Merge pull request #31 from jolt-lang/reader-feature-keys
Spec §2 (reader) + RFC 0002 feature-key decision + docs consolidation
2026-06-10 12:10:47 -04:00
Yogthos
808ce6a725 docs: consolidate doc/ into docs/
One documentation root: the prose docs (building-and-deps, self-hosting
architecture/compiler, tools-deps, grammar.ebnf) join the spec and RFCs under
docs/. References in README and deps-conformance-test updated.
2026-06-10 12:10:28 -04:00
Yogthos
fdfd086df6 reader: feature set #{:jolt :default}, clause-order matching (RFC 0002)
jolt no longer satisfies :clj in reader conditionals. The shortcut was a
measured net liability: :clj branches carry JVM interop and JVM-specific
test expectations jolt fails, and they shadowed :default branches jolt
passes. A/B over the suite: clj,default = 4967 assertions / 4324 pass / 119
errors; jolt,default = 5069 / 4470 / 81 (+146 pass, -38 errors, +8 clean
files). Baselines raised to 4470/86.

Matching is now by CLAUSE order like Clojure — the first clause whose key is
in the feature set wins (#?(:default 5 :clj 6) is 5 everywhere); the old code
scanned for :clj first, then :default, regardless of position.

Foreign clj-targeted libraries are a property of the LOADING CONTEXT, not the
platform: reader-features-set! opts a load into a compatibility set, and the
SCI bootstrap/runtime tests load SCI under ["jolt" "clj" "default"] (its
.cljc selects implementations via :clj with no :jolt branches).
JOLT_FEATURES remains the process-wide override.

RFC 0002 records the decision with the measured data; spec 02-reader S18 is
now normative (clause order, documented feature set, per-context override).
Reader tests updated to the portable set + an opt-in round-trip.
2026-06-10 11:40:06 -04:00
Yogthos
2224e40afc docs: spec §2 (reader) — grammar, reader-macro catalog, syntax-quote contract
The lexical-syntax chapter, granularity modeled on jank's 62-file
per-construct reader corpus: token grammar (whitespace/comments, collections
with read-time duplicate checks, numbers incl. the N/M tower question,
symbols/keywords incl. ::auto-resolution, strings/chars), the quote-family
sugars, the full #-dispatch catalog with normative entries (anonymous fn
%-derivation, discard composition, reader conditionals, symbolic floats,
tagged literals), and the syntax-quote contract (core/alias/current-ns
qualification, template-stable gensyms, ~' idiom, distribution through
collections).

Adapting the corpus surfaced and filed three findings, recorded as labeled
divergences/UNVERIFIED in the chapter: nested syntax-quote doesn't collapse
(S25, (= "meow" ```"meow") is false), #inst reads as a bare string (identity
data reader, no instant type), and jolt satisfies :clj in reader
conditionals (feature-key policy under review).

reader-forms-spec gains 11 chapter-cited rows (discard stacking, ##Inf/
##-Inf/##NaN, :default conditionals, qualified var-quote identity, gensym
stability within vs across templates) — all passing.
2026-06-10 11:23:38 -04:00
Dmitri Sotnikov
4d4402b75a
Merge pull request #30 from jolt-lang/spec-35-vars-batch-a
Spec 35-var batch A: 1.11 parsers, map/partition variants, with-redefs, ns fns (+3 bug fixes)
2026-06-10 23:17:26 +08:00
Yogthos
eb7a9f1b20 core: spec 35-var batch A — 1.11 parsers, map/partition variants, with-redefs, ns fns
Fifteen vars from the spec coverage gap (docs/spec/coverage.md):
parse-long/parse-double/parse-boolean (strict validation; scan-number alone
accepts 0x10), newline, current-time-ms (host clock for time), update-keys/
update-vals (PHM base, collisions last-wins), partitionv/partitionv-all/
splitv-at (lazy seqs of vectors; splitv-at's tail stays a seq, matching the
reference), with-redefs/with-redefs-fn (roots restored on throw), time,
macroexpand (expand-1 to fixpoint), alias/ns-unalias (write BOTH alias stores
— require :as uses string-keyed :imports while ns-aliases reads :aliases;
split filed), ns-publics (symbol-keyed map; publics == interns, no privacy).

Three pre-existing bugs fixed along the way:
- (partition n step pad coll) misparsed pad as the coll and returned ()
- (require 'bare.symbol) rejected — only vector specs were accepted
- analyze-form leaked the interpreted analyzer's ns on a punt: a throw out of
  the analyzer left current-ns=jolt.analyzer and :compile-ns set, so the
  fallback interpretation resolved user vars against the wrong namespace
  (bit (var user-sym) under compile mode)

And one overlay-authoring landmine documented in-code: a 20-coll fn must not
use 30-macros macros (with-redefs-fn's dotimes compiled as a forward ref that
resolved to the macro fn at runtime) — loop/recur instead.

Gate: conformance 316x3 (+14 rows), suite 4324 pass / 78 clean (was 4081/72;
parse_*/update_* files now contribute), baselines raised, all specs+unit,
fixpoint, self-host, sci, staged. Coverage: missing-portable 35 -> 20.
2026-06-10 11:16:54 -04:00
Dmitri Sotnikov
894af34b4c
Merge pull request #29 from jolt-lang/language-spec-rfc
RFC 0001: a specification for the Clojure language (+ spec skeleton, coverage dashboard)
2026-06-10 22:57:09 +08:00
Yogthos
7003926eda docs: language specification RFC + spec skeleton with normative exemplars
RFC 0001 proposes a normative, implementation-independent Clojure language
spec (the reader, evaluation model, special forms, data types, seq/laziness
contracts, namespaces/vars, and the portable clojure.core surface) to the
standard of R7RS/Racket — Clojure has none, and every alternative
implementation re-derives semantics from the reference and folklore. The
spec is executable-first: every numbered normative statement cites its
conformance test or is marked UNVERIFIED.

docs/spec/ carries the front matter (conformance terms, entry format, host
classification), the special-form catalog with worked normative entries for
if and let*, the core-library entry format with worked entries for first,
reduce, and parse-uuid, and a generated coverage dashboard over the 694-var
ClojureDocs inventory (tools/spec_coverage.py cross-references the surface
against jolt's interned+resolvable vars and the test suites).

Measured baseline: 380 implemented+tested, 154 implemented-untested, 35
portable-but-missing (filed), 22 resolvable-but-not-interned (filed — seed
fns invisible to resolve/ns-publics), rest classified host/JVM/concurrency.
2026-06-10 10:53:44 -04:00
Dmitri Sotnikov
bd5142abac
Merge pull request #28 from jolt-lang/strict-map-coll-predicates
Strict map?/coll?: tagged structs are values, sorted colls are colls
2026-06-10 22:35:17 +08:00
Yogthos
526de12ad1 core: strict map?/coll? — tagged structs are values, sorted colls are colls
map? treated ANY struct as a map, so (map? 'sym), (map? \a), and
(map? (random-uuid)) were all true; coll? had the same wart. Meanwhile both
were FALSE for sorted maps (and coll? for sorted sets and records), which are
collections in Clojure. Now: a map is a plain struct literal, a phm, a sorted
map, or a record; coll? additionally includes sorted sets. Tagged structs
(anything with :jolt/type) are values.

The loose-map? dependents (destructure's clause ordering, defn's attr-map
guard) already guarded with symbol? first, so nothing relied on the wart —
full gate green, and the suite gained: 4074 -> 4081 pass / 72 clean files
(map_qmark/coll_qmark assertions now correct); baselines raised. 22 new
strictness spec cases.
2026-06-10 10:34:59 -04:00