jolt/test/chez
Yogthos da775802d6 Source the conformance corpus from JVM Clojure; retire the prelude gate
corpus.edn :expected is now the value reference JVM Clojure produces, set by the
new test/conformance/regen-corpus.clj (one JVM process, per-row thread watchdog).
167 rows moved to the JVM value: ratios (/ 1 2)=>1/2, doubles (double 3)=>3.0,
shared-heap concurrency (the future/pmap/agent cases), clojure.math doubles. The
JVM is the spec; jolt is measured against it.

known-divergences.edn shrinks to the rows whose JVM value is an opaque host object
that can't round-trip to source (Java arrays, transients, atoms, beans, proxies,
chunks all print as #object[..@addr]) plus (fn* foo) and a few racy concurrency
cases (:flaky). The zero-janet gate's allowlist becomes the set of host gaps vs the
JVM spec (no Class/array/BigDecimal, :jolt reader, jolt's own printing).

Math/clojure.math sqrt/pow/floor/trig now return doubles (Chez returns exact for
exact args, e.g. (sqrt 9)=>3); JVM always returns a double.

extract-corpus.janet no longer writes corpus.edn unless asked (the test runner
imported it and was silently overwriting the JVM corpus with the spec sources'
placeholder answers). The prelude parity gate is deleted — the zero-janet spine +
certify.clj are the oracles.

zero-janet 2678 (0 new divergences), certify 0 new / 0 stale, emit-test 330/330.
2026-06-21 01:45:04 -04:00
..
_atomwatch.janet Chez Phase 2 (inc S): atom watches + validators (jolt-mn9o) 2026-06-19 04:33:07 -04:00
_class.janet Chez Phase 2 (inc T): class native + bare class-token resolution (jolt-13zk) 2026-06-19 08:41:45 -04:00
_dotform.janet Chez Phase 2 (inc R): . / .-field dot-form desugar (jolt-kuic) 2026-06-19 00:32:30 -04:00
_dynbind.janet Chez Phase 2 (inc M): dynamic var binding (jolt-2o7x) 2026-06-18 18:29:55 -04:00
_insttime.janet Chez Phase 2 (inc X): #inst / #uuid literals + java.time (jolt-at0a) 2026-06-19 11:05:22 -04:00
_io.janet Chez Phase 2 (inc V): java.io.File + slurp/spit/flush/file-seq (jolt-yyud) 2026-06-19 10:02:12 -04:00
_ioreader.janet Chez Phase 2 (inc W): reader-coupled io (jolt-at0a) 2026-06-19 10:40:19 -04:00
_javastatic.janet Chez Phase 2 (inc Q): Java class statics + constructors (jolt-avt6) 2026-06-18 23:24:01 -04:00
_ns.janet Chez Phase 2 (inc K): namespace value model (jolt-yxqm) 2026-06-18 16:49:35 -04:00
_reader.janet Chez numeric tower: exact ints / Ratio / double for JVM parity (jolt-n6al) 2026-06-20 23:09:27 -04:00
_seqpred.janet Chez Phase 2 (inc N): type (jolt-fmm4) 2026-06-18 19:40:17 -04:00
_stdlib.janet Chez Phase 2: clojure.set + clojure.math + pprint (jolt-j5vg, jolt-22vo) 2026-06-19 17:06:26 -04:00
_str.janet Chez Phase 2 (inc O): host String methods (jolt-nfca) 2026-06-18 20:36:39 -04:00
_strns.janet Chez Phase 2 (inc P): clojure.string namespace (jolt-nfca) 2026-06-18 21:38:13 -04:00
_type.janet Chez Phase 2 (inc N): type (jolt-fmm4) 2026-06-18 19:40:17 -04:00
_var_meta.janet Chez Phase 2 (inc L): var def-time metadata (jolt-zikh) 2026-06-18 17:13:46 -04:00
_walk.janet Chez Phase 2 (inc U): list? + map-entry-as-vector + clojure.walk (jolt-75sv) 2026-06-19 09:30:18 -04:00
bench-chez.ss Chez Phase 4: perf probe (test/chez/bench-chez.ss) 2026-06-20 14:30:36 -04:00
bench-pipeline.janet Chez Phase 1 close-out: truthy? elision + end-to-end compute bench (jolt-nkcb) 2026-06-18 09:05:54 -04:00
bootstrap-test.janet Chez Phase 3 inc9a: self-contained Chez bootstrap (no Janet in the loop) 2026-06-20 06:46:05 -04:00
cli-test.janet Chez concurrency pt.3: async agents (per-agent serialized dispatch) 2026-06-20 14:22:37 -04:00
core-prelude-probe.janet Chez Phase 1 (increment 3i): regex via vendored irregex 2026-06-17 19:44:18 -04:00
corpus.edn Source the conformance corpus from JVM Clojure; retire the prelude gate 2026-06-21 01:45:04 -04:00
emit-parity.janet Chez Phase 3 inc 4: swap jolt-chez to the jolt.backend-scheme emitter; full corpus parity 2026-06-19 18:52:33 -04:00
emit-test.janet Source the conformance corpus from JVM Clojure; retire the prelude gate 2026-06-21 01:45:04 -04:00
extract-corpus.janet Source the conformance corpus from JVM Clojure; retire the prelude gate 2026-06-21 01:45:04 -04:00
fixpoint-test.janet Chez Phase 3 inc8: prelude fixpoint + fully-Chez-emitted system 2026-06-20 05:21:23 -04:00
known-divergences.edn Chez Phase 0a+0b: value model + host-neutral contract gate 2026-06-17 13:06:35 -04:00
nil-names-probe.janet Chez Phase 2 (inc F): jolt.host ref primitives + sorted collections 2026-06-18 13:14:13 -04:00
reader-parity.janet Chez Phase 3 inc 5c: reader dispatch (#) in jolt.reader 2026-06-19 20:00:57 -04:00
README.md Chez Phase 1 close-out: truthy? elision + end-to-end compute bench (jolt-nkcb) 2026-06-18 09:05:54 -04:00
run-corpus-chez.janet Chez Phase 1 (increment 3b): seq tier + dynamic IFn dispatch on the Chez RT 2026-06-17 15:19:18 -04:00
run-corpus-zero-janet.janet Source the conformance corpus from JVM Clojure; retire the prelude gate 2026-06-21 01:45:04 -04:00
run-corpus.janet Chez Phase 0a+0b: value model + host-neutral contract gate 2026-06-17 13:06:35 -04:00
spine-test.janet Chez Phase 3 inc6b: runtime macros on the zero-Janet spine 2026-06-19 23:21:13 -04:00
values-test.ss Chez Phase 0c + 0a hardening: collections decision + value-model fixes 2026-06-17 13:10:19 -04:00

Chez port — Phase 0 test contract harness

The host-neutral correctness gate for the Chez re-host (epic jolt-cf1q). The spec corpus is data, so the SAME contract validates every host.

Files

  • extract-corpus.janet — parses test/spec/*.janet (defspec …) tables as data and writes corpus.edn (2655 [label expected actual] cases). The file is valid as BOTH EDN (a future Chez-jolt runner) and Janet data (the runner below). Regenerate: janet test/chez/extract-corpus.janet.
  • corpus.edn — the extracted contract (generated; checked in for convenience).
  • run-corpus.janet — drives a TARGET jolt binary, one fresh subprocess per case (fresh ctx = per-case isolation), checking (= expected actual) prints true at the CLI, or that a :throws case exits non-zero. Pluggable target:
    • janet test/chez/run-corpus.janet # default build/jolt
    • JOLT_BIN=build/jolt-chez janet test/chez/run-corpus.janet # Phase 1+
    • JOLT_CORPUS_LIMIT=400 … # every-Nth stride, fast
  • known-divergences.edn — allowlist of cases that diverge at the CLI boundary. The gate fails only on a NEW divergence; known ones are reported but tolerated.
  • values-test.ss / ../../host/chez/values.ss — Phase 0a value model + tests.

The reference baseline (2026-06-17, Janet build/jolt, compile mode)

2641/2655 pass; 14 known divergences. They split into:

  • interpret-vs-compile leniency:throws cases where interpret mode raises but compile mode returns (< nil, > with nil, neg? keyword, max/min-key on non-numbers). Several are also non-canonical vs JVM Clojure.
  • invoke-collection-as-fn — the transient / invokable lookup suite invokes transients/collections as fns (((transient {:x 7}) :x)); compile mode (and JVM Clojure) reject it.
  • xml-seq walks — one structural case.

The compile-only Chez host (JVM-canonical oracle) should MATCH OR FIX these. The gate's job is to catch regressions the port introduces, not to bless these.

Why the CLI boundary

The runner tests through jolt -e, exactly how the Chez host will be exercised — not the in-process eval-string the Janet defspec harness uses. The two differ on a handful of cases (the allowlist), and the CLI boundary is the portable one.

Phase 1 — first parity number (subset probe)

The full run-corpus.janet gate drives an -e-capable jolt binary; the Chez host can't answer arbitrary -e until all of clojure.core is bootstrapped onto Chez (Phase 2). Until then, run-corpus-chez.janet reports parity for the subset the Phase-1 back end (host/chez/emit.janet) can already compile: each case is run through the live analyzer → Scheme emitter → Chez via host/chez/driver. Cases that reference unimplemented stdlib/host fns fail to EMIT (a clean compile-time signal) and are counted "out of subset", not as divergences.

JOLT_CHEZ_CORPUS=1 janet test/chez/run-corpus-chez.janet

Baseline after inc 3g (letfn + declare): 672/672 compiled cases pass, 0 divergences; 1986/2658 out of subset (await clojure.core on Chez). Inc 3e (throw/try + ex-info) was 632/632; inc 3f's quote support + a seq.ss fix (empty map/filter results are () not nil, matching Clojure) reached 664/664; inc 3g (letfn -> Scheme letrec*, declare/def-no-init -> a reserved var cell) pulled 8 more corpus cases into the subset. emit-fn lowers multi-arity fns to a Scheme case-lambda and variadic fns to a rest-arg lambda (rest list coerced to a jolt seq, nil when empty).

Phase 1 — clojure.core prelude emission (inc 3d, jolt-ocvi)

The -e-capable jolt-chez path: emit the clojure.core tiers (jolt-core/clojure/core/NN-*.clj) through the same analyzer → emit pipeline as a Scheme PRELUDE of def-var! forms, so user code's (var-deref "clojure.core" …) resolves the fn at runtime. emit/set-prelude-mode! flips a switch: in the default (subset) mode a non-native clojure.core ref is rejected ("out of subset"); in prelude mode it lowers to a runtime var-deref so core fns chain through each other. Host interop (:host) and unhandled IR ops still error in both modes — those are the real gaps that need a hand-written RT shim or new emit support.

core-prelude-probe.janet (gated behind JOLT_CHEZ_PRELUDE=1) measures reach and catalogs the gaps; macros are skipped (analyze-time only, not a runtime value):

JOLT_CHEZ_PRELUDE=1 janet test/chez/core-prelude-probe.janet

Baseline after inc 3i (regex): 355/355 non-macro core forms emit to Scheme — the whole non-macro clojure.core now lowers. inc 3i closed the last gap, the regex literal in parse-uuid: a #"…" literal lowers to a :regex IR node and the Chez emitter emits a jolt-regex value over vendored irregex (Alex Shinn, BSD, vendor/irregex submodule) — a portable Scheme regex with PCRE/Java-style string patterns. re-pattern/re-matches/re-find/re-seq/regex? are def-var!'d into clojure.core (host/chez/regex.ss); they stay OUT of the subset native-ops (irregex's Unicode/property-class semantics differ from the seed's byte-PEG approximation), so they resolve in prelude mode — the path the assembled prelude takes — without dragging engine-difference divergences into the subset corpus. The Janet back end punts :regex to the interpreter (the seed compiles #"…" to a Janet PEG). Prior incs: inc 3h .method:host-call (jolt-host-call for .write/.isDirectory/.listFiles); :quote, :throw, :try, ex-info, letfnletrec*, declare/def-no-init → reserved var cell. The probe has a regression floor (355) — every non-macro core form must keep emitting.

Phase 1 — the assembled prelude: -e-capable jolt-chez (inc 3j, jolt-9ziu)

Once the whole non-macro clojure.core emits (inc 3i), the milestone is to ASSEMBLE it: driver/emit-core-prelude emits every non-macro core form across the dependency-ordered tiers as a def-var! (prelude mode), concatenated into a Scheme prelude. bin/jolt-chez -e EXPR loads rt.ss + that prelude + a post-prelude override, emits the user expression in prelude mode, and runs it on Chez — an -e-capable jolt-chez (analysis on Janet, execution on Chez). The prelude is cached on disk keyed by a fingerprint of the core sources + the RT.

run-corpus-prelude.janet is the full parity gate this opens (the prelude-backed sibling of run-corpus-chez.janet): it assembles the prelude once, then runs every corpus case with all of core present, bucketing the result —

JOLT_CHEZ_PRELUDE_CORPUS=1 janet test/chez/run-corpus-prelude.janet
JOLT_CORPUS_LIMIT=200 …    (every-Nth stride, fast)

Parity baseline: inc 3j 1220/2497; 3k (converters, jolt-t6cr) 1326; 3l (transients, jolt-kl2l) 1382; 3m (numeric-edge emit + variadic assoc!, jolt-q3w8) 1407/2497, 0 NEW divergences (14 allowlisted: dynamic vars *ns*/*clojure-version*/*unchecked-math*, var/*ns* rendering, class names, eval-order, with-open — all deferred Phase-2 / dynamic-var gaps).

  • inc 3k host/chez/converters.ss: str/subs/vec/keyword/symbol/compare/ int/double/gensym (seed natives — str reuses the printer, compare is the 3-way port, the symbol no-ns sentinel is #f to match emit's quoted-symbol lowering so (= 'x (symbol "x")) holds).

  • inc 3l host/chez/transients.ss: transient/persistent!/conj!/assoc!/ dissoc!/disj!/pop! as copy-on-write over the persistent collections (correct semantics, no in-place perf), plus persistent disj. get/count/contains? are redefined to see THROUGH a transient (frequencies/group-by do (get tm k) on a transient map); vector? on a transient vector is false, which group-by relies on.

  • inc 3m: ##Inf/##-Inf/##NaN emitted to bare inf/nan (unbound in Chez); emit-const now lowers them to +inf.0/-inf.0/+nan.0, the -e printer renders inf/-inf/nan and str renders Infinity/-Infinity/NaN (Clojure). Plus variadic assoc!. (str of inf inside a collection still wants the long form — the Phase-2 recursive str renderer — so [inf inside coll] is allowlisted.)

  • inc 3n host/chez/natives-seq.ss (jolt-y6mv): the dominant prelude-parity crash bucket was apply non-procedure jolt-nil — core fns calling seed-native seq fns (src/jolt/core_coll.janet) that have no Chez shim, so var-deref yields jolt-nil. A static scan of the assembled prelude found 52 referenced-but-undefined clojure.core names; this increment shims the safe, high-value seq fns: mapcat/ take-while/drop-while/partition (collection arities — the 1-arg transducer forms are jolt-kxsr), sort (compare default; a comparator may return a 3-way number or a boolean less-than), and reduced/reduced? — a jolt-reduced record in seq.ss that reduce short-circuits on and deref unwraps (so unreduced works). identical? = jolt= (the seed's definition). list? was deferred: a Chez lazy seq and a list are both cseq, so it can't be told apart without a distinct list type (a real divergence risk).

  • inc 3o transducer arities (jolt-kxsr): the 1-arg map/filter/remove/take/ drop/take-while/drop-while/mapcat return a transducer (fn [rf] rf'), and into gets a 3-arg (into to xform from). These were the cdr () is not a pair / incorrect number of arguments crash bucket: the emitter lowers (map f) / (into to xf from) at the wrong arity to the bare native procedure (the value-position path), so the fix is RT-side — case-lambda the seq procedures and jolt-into. The td-* factories are ported from the seed (core_coll.janet); a reduced step stops the fold via reduce-seq's short-circuit. transduce/comp/ completing are overlay and compose over these for free.

  • inc 3p misc seq/regex gaps (jolt-y1zq): 0-arg (conj) -> [], 0-arg (conj!) -> a fresh transient vector, nth sees through a transient (like get/count/contains?), and irregex \p{...}/\P{...} property classes translate to the seed's ASCII char classes (regex.ss; \p{L}->[a-zA-Z], \p{N}->[0-9], \p{Ps}->[([{], …) — ASCII-only (the seed counts UTF-8 high bytes as letters, which a Unicode-char Scheme string can't reproduce; the corpus tests ASCII, where they agree) and clojure.math/PI (missing ns).

    Two pre-existing bugs surfaced here, since fixed (inc 3x / 3y below).

  • inc 3q multimethod dispatch + late-bind (jolt-9ls5, Phase 2): host/chez/ multimethods.ss implements the multimethod runtime — defmulti/defmethod expand to defmulti-setup/defmethod-setup calls (+ get-method/methods/ remove-method/prefer-method/prefers). A jolt-multifn record carries its dispatch fn and a jolt=-keyed method table; jolt-invoke dispatches it (exact match, then isa?/hierarchy with prefer-method, then :default), reusing the overlay's isa?/derive/make-hierarchy. The multifn's ns is set via a runtime chez-current-ns (default "user"; the prelude load sets "clojure.core").

    Two emit-side changes made this work:

    • late-bind (:late-bind-unresolved? ctx flag, default OFF): defmulti expands to a bare-symbol setup call, so the analyzer doesn't intern the name and a forward reference ((area …) after (defmulti area …) in one form) was "Unable to resolve symbol". The strict compiler punts these to the interpreter; the Chez back end has none, so the flag makes an unresolved symbol lower to a var-ref against the compile ns — the open-world semantics of -e. Set only by the Chez make-ctx/jolt-chez; the main compiler keeps its strict behavior.
    • a :var call head now routes through jolt-invoke (not a direct application), since a late-bound var can hold a multifn (or a keyword/coll IFn), not just a procedure. Transparent for procedures; the hot self-recursive call is a :local known-proc, so it stays direct. (Class-based dispatch — (class x)/String — is deferred; it needs the deftype/class subsystem.)
  • inc 3r dynamic-var constants (jolt-9ls5): host/chez/dynamic-vars.ss binds the seed natives *clojure-version* (the {:major 1 :minor 11 …} map) and *unchecked-math* (false), removing their two parity allowlist entries. *ns* is deferred (jolt-b4kl): it needs a namespace value that is not a map (map? false) yet answers (get ns :name) for the overlay ns-name, plus str/find-ns support.

  • inc 3x non-ASCII string literals (jolt-x0os): emit.janet's chez-str-lit replaces the %j string encoder. Janet's %j renders a non-ASCII char as raw UTF-8 bytes (\xC3\xA9) and a control char / DEL as \xHH with NO terminating semicolon — both forms Chez's reader rejects. chez-str-lit UTF-8-decodes each char and emits a codepoint hex escape \x<cp>; (é->\xe9;, ->\x65e5;), keeping \n/\t/\r/ \"/\\ and being byte-identical to %j for printable ASCII. Applied to every string-content site (string/keyword/symbol/var-name/regex-source). This unblocks the p{L} utf-8 corpus case (the \p{L} translation was already correct). Note: count/ subs/nth over a multibyte string index by BYTES in the seed but by codepoints on Chez — a separate semantic gap, not addressed here.

  • inc 3y seed assoc! odd-args (jolt-ea9k): core-assoc! (src/jolt/core_extra.janet) now throws on an odd key/val count, like plain assoc and Clojure's assoc! — the former lenient nil-fill was non-Clojure and inconsistent with the seed's own assoc. The 4 assoc! odd args spec rows became 3 :throws + 1 even-args positive; corpus.edn regenerated. The Chez host already threw, so this only realigns the corpus contract.

  • Phase 1 close-out (jolt-nkcb): truthy? elision + end-to-end compute bench. emit.janet now drops the redundant jolt-truthy? wrapper on an :if test that provably yields a Scheme boolean (a native comparison/not, or a boolean const) — sound because jolt-truthy? of #t/#f is identity; the hot fib/mandelbrot tests are all comparisons, so this is a direct ceiling lever (fib 30 end-to-end 24.0 -> 14.4 ms). bench-pipeline.janet (JOLT_CHEZ_BENCH=1, opt-in) times fib(30) + mandelbrot(200) through the real pipeline vs the spike ceiling: mandelbrot 87 ms runs AT the generic-flonum ceiling (98 ms); fib is 2x its hand-flonum ceiling, the residual being jolt's all-double number model (typed fl*/fx* emission = Phase 4). Compile-only is total for the compute subset (every form emits; Chez has no interpreter fallback).

The remaining buckets are the punch-list the next increments chase: ~361 emit-fail (genuine host interop — qualified Java/Janet refs, runtime defmacro/eval, out of the analyzer's subset) and the runtime crashes still dominated by apply jolt-nil (more host-coupled natives without a shim: meta/with-meta, format, the clojure.string natives, bit ops, var/volatile/future/thread-binding ops), plus the small jolt-y1zq tail (0-arg conj, \p{} regex classes, a few one-offs) and multimethod dispatch (Phase 2 jolt-9ls5).

Two host shims landed with the prelude. host/chez/atoms.ss: atom/deref/swap!/ reset! (+ compare-and-set!/swap-vals!/reset-vals!) — host-coupled mutable cells the overlay assumes are native; needed at the prelude's LOAD time (global-hierarchy = (atom (make-hierarchy))). host/chez/predicates.ss: the type predicates + name/namespace/boolean the overlay assumes are seed natives (nil?/number?/string?/map?/vector?/set?/seq?/coll?/fn?/…), matching the seed's strict semantics. host/chez/post-prelude.ss re-asserts char?/atom? AFTER the prelude — the overlay implements those two by reading a value's :jolt/type key (a Janet-host assumption that's false for Chez-native chars/atoms), and its def-var! would otherwise clobber the correct native shims.

The 8 print-method/print-dup defmulti/defmethod forms (50-io) can't LOAD yet (no multimethod runtime on Chez — Phase 2); a silent load guard in the assembled prelude lets the rest load and turns them into lazy gaps.

Prior, inc 3b (seq tier + dynamic IFn, jolt-5pso): 595/595 compiled, 0 divergences, 2060/2655 out of subset. The seq tier brought up a list/lazy-seq type with first/rest/next/seq/cons/list, map/filter/reduce/into/remove, range/take/drop/concat/apply, keys/vals, and nth/peek/pop over seqs; dynamic IFn dispatch (a keyword/vector/coll held in a local and called as a fn) routes through the jolt-invoke fallback, closing the 3 ex-known divergences. The probe exits non-zero on any NEW divergence.

(Prior, inc 3a: 433/436 compiled, 3 known IFn divergences, 2219 out of subset. Inc 2: 182/182 compiled, 0 divergences, 2473 out of subset.)

It's a slow report (a Chez subprocess per case), so it's gated behind JOLT_CHEZ_CORPUS out of the default suite, like the benches. test/chez/emit-test.janet is the fast Phase-1 unit gate (real analyzer → Chez parity for fib/mandelbrot + collections + regressions); both skip cleanly when chez isn't on PATH.