Chez Phase 1 (increment 2): live analyzer -> Chez, var cells, RT, mandelbrot

Wire the real pipeline end to end: host/chez/driver.janet boots a compile-mode
jolt ctx, runs the EXISTING Janet-hosted analyzer on actual Clojure source to
real IR, feeds it to the Scheme emitter, and runs the result on Chez. Analysis
stays on Janet (the analyzer ports to Chez in Phase 2); execution is on Chez.

emit.janet now consumes live IR (pv/phm-normalized like the Janet backend) and
covers what the analyzer actually emits, not the hand-built inc-1 shapes:
- core ops arrive as :var clojure.core/+ etc., not :rt — lowered to native
  Scheme via a native-ops table (mirrors backend.janet's), `=` to jolt=.
- var cells (host/chez/rt.ss): :def -> def-var!, :var -> var-deref. Late binding
  so cross-var calls (run -> count-point) and the entry crossing resolve at use.
- named fns (defn / fn self-name) bind via letrec so self-recursion resolves.
- unsupported stdlib/host refs (no core on Chez yet) are rejected at EMIT time
  (clean out-of-subset signal) instead of deref'ing to nil and failing at runtime.

Number model: jolt is all-doubles (no ratios; (/ 1 2) is 0.5), so literals emit
as flonums — matches the Janet host and keeps Chez out of exploding exact
rationals (mandelbrot). jolt-num->string prints integer-valued without ".0".

Two real bugs found via the corpus probe and fixed (regression rows added):
- loop bound in parallel (Scheme named-let) but Clojure loop is sequential — a
  later init must see earlier bindings; wrap a let* around the loop.
- #(...) shorthand gensyms params with a trailing `#`, invalid in Scheme — munge
  it to `_`.

Gate: test/chez/emit-test.janet runs the real analyzer -> Chez for (+ 1 2),
fib(30)=832040, mandelbrot run(40), and the two regressions, parity-checked
against the Janet oracle (6/6). First parity number via the new subset probe
(test/chez/run-corpus-chez.janet, JOLT_CHEZ_CORPUS=1): 182/182 compiled corpus
cases pass, 0 divergences; 2473/2655 out of subset pending core on Chez. Full
jpm/run-tests gate green (125 files). Chez tests skip cleanly without `chez`.

Perf note (unchanged plan): emitted fib(30) ~23ms vs hand-Scheme ~5ms — the
jolt-truthy? wrapper (~3x) plus flonum (not fixnum) arithmetic, both Phase-4
type-specialization levers.
This commit is contained in:
Yogthos 2026-06-17 13:59:57 -04:00
parent 874e3c7cf2
commit 9bbcc07c8f
6 changed files with 410 additions and 97 deletions

View file

@ -0,0 +1,58 @@
# Phase 1 (jolt-cf1q.2) — FIRST parity number for the Chez back end.
#
# The full 0b gate (test/chez/run-corpus.janet) drives an `-e`-capable jolt
# binary; that needs all of clojure.core bootstrapped onto Chez, which is Phase 2.
# Until then, this probe reports parity for the subset the back end can ALREADY
# compile: each corpus case `(= expected actual)` is run through the live
# analyzer -> Scheme emitter -> Chez. Cases that reference unimplemented core fns
# fail to EMIT (a clean compile-time signal) and are counted "out of subset",
# not as divergences. The number to watch is parity WITHIN the compiled subset.
# janet test/chez/run-corpus-chez.janet
# JOLT_CORPUS_LIMIT=400 … (every-Nth stride, fast)
(import ../../host/chez/driver :as d)
# Slow reporting tool (~20s: a Chez subprocess per compiled case), not a pass/fail
# unit test — gate it out of the default suite like the benches (JOLT_BENCH).
(unless (os/getenv "JOLT_CHEZ_CORPUS")
(print "skip: set JOLT_CHEZ_CORPUS=1 to run the Chez subset parity probe")
(os/exit 0))
(unless (d/chez-available?)
(print "skip: chez not on PATH")
(os/exit 0))
(def corpus (parse (slurp "test/chez/corpus.edn")))
(def cases
(if-let [n (os/getenv "JOLT_CORPUS_LIMIT")]
(let [stride (max 1 (math/floor (/ (length corpus) (scan-number n))))]
(seq [i :range [0 (length corpus) stride]] (in corpus i)))
corpus))
(def ctx (d/make-ctx))
(var compiled 0) (var pass 0) (var out-of-subset 0)
(def diverged @[])
(each row cases
(def {:expected e :actual a :label l} row)
# :throws cases need error-semantics we don't model yet — skip.
(if (= e :throws)
(++ out-of-subset)
(let [src (string "(= " e " " a ")")
# compile-program can throw (unsupported op/core fn) or the analyzer can
# punt; either way the case is outside the compilable subset.
res (try (d/run-on-chez ctx src) ([err] :uncompilable))]
(if (= res :uncompilable)
(++ out-of-subset)
(let [[code out] res]
(++ compiled)
(cond
(not= code 0) (array/push diverged [l (string "exit " code)])
(= out "true") (++ pass)
(array/push diverged [l (string "got " out)])))))))
(printf "\nChez subset parity: %d/%d compiled cases pass (%d/%d corpus out of subset)"
pass compiled out-of-subset (length cases))
(when (> (length diverged) 0)
(printf "%d divergence(s) within the compiled subset:" (length diverged))
(each [l m] (slice diverged 0 (min 25 (length diverged)))
(printf " [%s] %s" l m)))
(flush)