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. |
||
|---|---|---|
| .. | ||
| _atomwatch.janet | ||
| _class.janet | ||
| _dotform.janet | ||
| _dynbind.janet | ||
| _insttime.janet | ||
| _io.janet | ||
| _ioreader.janet | ||
| _javastatic.janet | ||
| _ns.janet | ||
| _reader.janet | ||
| _seqpred.janet | ||
| _stdlib.janet | ||
| _str.janet | ||
| _strns.janet | ||
| _type.janet | ||
| _var_meta.janet | ||
| _walk.janet | ||
| bench-chez.ss | ||
| bench-pipeline.janet | ||
| bootstrap-test.janet | ||
| cli-test.janet | ||
| core-prelude-probe.janet | ||
| corpus.edn | ||
| emit-parity.janet | ||
| emit-test.janet | ||
| extract-corpus.janet | ||
| fixpoint-test.janet | ||
| known-divergences.edn | ||
| nil-names-probe.janet | ||
| reader-parity.janet | ||
| README.md | ||
| run-corpus-chez.janet | ||
| run-corpus-zero-janet.janet | ||
| run-corpus.janet | ||
| spine-test.janet | ||
| values-test.ss | ||
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— parsestest/spec/*.janet(defspec …)tables as data and writescorpus.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)printstrueat the CLI, or that a:throwscase exits non-zero. Pluggable target:janet test/chez/run-corpus.janet# default build/joltJOLT_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 —
:throwscases where interpret mode raises but compile mode returns (< nil,> with nil,neg? keyword,max/min-keyon non-numbers). Several are also non-canonical vs JVM Clojure. - invoke-collection-as-fn — the
transient / invokable lookupsuite 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,
letfn → letrec*, 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 —strreuses the printer,compareis the 3-way port, the symbol no-ns sentinel is#fto 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 persistentdisj.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/##NaNemitted to bareinf/nan(unbound in Chez); emit-const now lowers them to+inf.0/-inf.0/+nan.0, the-eprinter rendersinf/-inf/nanandstrrendersInfinity/-Infinity/NaN(Clojure). Plus variadicassoc!. (strof 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 wasapply non-procedure jolt-nil— core fns calling seed-native seq fns (src/jolt/core_coll.janet) that have no Chez shim, sovar-derefyields jolt-nil. A static scan of the assembled prelude found 52 referenced-but-undefinedclojure.corenames; 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), andreduced/reduced?— ajolt-reducedrecord inseq.ssthatreduceshort-circuits on andderefunwraps (sounreducedworks).identical?=jolt=(the seed's definition).list?was deferred: a Chez lazy seq and a list are bothcseq, 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/mapcatreturn a transducer(fn [rf] rf'), andintogets a 3-arg(into to xform from). These were thecdr () is not a pair/incorrect number of argumentscrash 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-lambdathe seq procedures andjolt-into. Thetd-*factories are ported from the seed (core_coll.janet); areducedstep stops the fold via reduce-seq's short-circuit.transduce/comp/completingare 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,nthsees 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) andclojure.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.ssimplements the multimethod runtime —defmulti/defmethodexpand todefmulti-setup/defmethod-setupcalls (+get-method/methods/remove-method/prefer-method/prefers). Ajolt-multifnrecord carries its dispatch fn and ajolt=-keyed method table;jolt-invokedispatches it (exact match, then isa?/hierarchy withprefer-method, then:default), reusing the overlay'sisa?/derive/make-hierarchy. The multifn's ns is set via a runtimechez-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):defmultiexpands 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 avar-refagainst the compile ns — the open-world semantics of-e. Set only by the Chezmake-ctx/jolt-chez; the main compiler keeps its strict behavior. - a
:varcall head now routes throughjolt-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:localknown-proc, so it stays direct. (Class-based dispatch —(class x)/String— is deferred; it needs the deftype/class subsystem.)
- late-bind (
-
inc 3r dynamic-var constants (jolt-9ls5):
host/chez/dynamic-vars.ssbinds 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 overlayns-name, plusstr/find-nssupport. -
inc 3x non-ASCII string literals (jolt-x0os):
emit.janet'schez-str-litreplaces the%jstring encoder. Janet's%jrenders a non-ASCII char as raw UTF-8 bytes (\xC3\xA9) and a control char / DEL as\xHHwith NO terminating semicolon — both forms Chez's reader rejects.chez-str-litUTF-8-decodes each char and emits a codepoint hex escape\x<cp>;(é->\xe9;,日->\x65e5;), keeping\n/\t/\r/\"/\\and being byte-identical to%jfor printable ASCII. Applied to every string-content site (string/keyword/symbol/var-name/regex-source). This unblocks thep{L} utf-8corpus case (the\p{L}translation was already correct). Note:count/subs/nthover 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 plainassocand Clojure'sassoc!— the former lenient nil-fill was non-Clojure and inconsistent with the seed's ownassoc. The 4assoc! odd argsspec 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.janetnow drops the redundantjolt-truthy?wrapper on an:iftest that provably yields a Scheme boolean (a native comparison/not, or a boolean const) — sound becausejolt-truthy?of#t/#fis 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.