Chez Phase 3 inc 4: swap jolt-chez to the jolt.backend-scheme emitter; full corpus parity

driver.janet now compiles IR via the portable Clojure emitter (jolt.backend-
scheme) instead of emit.janet, at every entry point (compile-program, emit-core-
prelude, eval-e-with-prelude). The emitter is loaded into the ctx and called like
the analyzer. emit.janet stays only as the emit/program string-wrapper until
program assembly ports to Clojure with compile-from-source; its emit fn is no
longer called anywhere (emit-test's truthy-elision helper now uses the new
d/scheme-emit too). This takes the IR->Scheme emitter off Janet.

A form-by-form diff of the two emitters over the whole prelude found one gap:
emit-const missed char literals because a :jolt/type-tagged struct is not a plain
jolt map? — switched to the form-char? host contract. Diff then 0.

jolt-chez prelude fingerprint now includes backend_scheme.clj + host_iface.janet.

Gate: full prelude corpus 2280/2494, NEW divergence 0, same buckets as the Phase-2
emit.janet floor (36 emit-fail, 170 crash) — the Clojure emitter is byte-for-
behavior identical. emit-test 331/331 (now via the Clojure emitter), emit-parity
58/58. jolt-duot.
This commit is contained in:
Yogthos 2026-06-19 18:52:33 -04:00
parent f9a665b3c4
commit 2a33da87d8
5 changed files with 48 additions and 13 deletions

View file

@ -15,8 +15,34 @@
(import ../../src/jolt/reader :as r) (import ../../src/jolt/reader :as r)
(import ../../src/jolt/evaluator :as evlr) (import ../../src/jolt/evaluator :as evlr)
(import ../../src/jolt/types_ctx :as tctx) (import ../../src/jolt/types_ctx :as tctx)
(import ../../src/jolt/types_ns :as tns)
(import ../../src/jolt/types_var :as tvar)
(import ./emit :as emit) (import ./emit :as emit)
# Chez Phase 3 (jolt-duot): the IR->Scheme emitter is now the PORTABLE Clojure
# jolt.backend-scheme (jolt-core), not emit.janet. It's loaded into the ctx and
# called from here the same way the analyzer is. emit.janet stays only as the
# program-string wrapper (emit/program) until program assembly ports to Clojure
# with compile-from-source. This is the step that takes the emitter off Janet.
(defn- ensure-clj-emitter [ctx]
(def env (ctx :env))
(unless (get env :clj-emit-fn)
(def src (get (get env :embedded-sources @{}) "jolt.backend-scheme"))
(assert src "jolt.backend-scheme not embedded (check stdlib_embed)")
(backend/bootstrap-load-source ctx "jolt.backend-scheme" src)
(def ns (tctx/ctx-find-ns ctx "jolt.backend-scheme"))
(put env :clj-emit-fn (tvar/var-get (tns/ns-find ns "emit")))
(put env :clj-set-prelude-fn (tvar/var-get (tns/ns-find ns "set-prelude-mode!"))))
ctx)
# Emit IR -> Scheme via the Clojure emitter (returns a Janet string).
(defn- cemit [ctx ir] (string ((get (ctx :env) :clj-emit-fn) ir)))
(defn- cset-prelude! [ctx on] ((get (ctx :env) :clj-set-prelude-fn) on))
# Public: emit IR -> Scheme via the portable Clojure emitter (jolt.backend-scheme).
# The single seam tests use so emit.janet's emit fn is no longer exercised.
(defn scheme-emit [ctx ir] (ensure-clj-emitter ctx) (cemit ctx ir))
(defn chez-available? (defn chez-available?
"True when a `chez` binary is on PATH — lets the chez tests skip cleanly on "True when a `chez` binary is on PATH — lets the chez tests skip cleanly on
hosts without it (CI without Chez), like the clojure-test-suite skips when its hosts without it (CI without Chez), like the clojure-test-suite skips when its
@ -35,6 +61,7 @@
call) lowers to a var-deref instead of failing to compile (jolt-9ls5)." call) lowers to a var-deref instead of failing to compile (jolt-9ls5)."
(def ctx (api/init {:compile? true})) (def ctx (api/init {:compile? true}))
(put (get ctx :env) :late-bind-unresolved? true) (put (get ctx :env) :late-bind-unresolved? true)
(ensure-clj-emitter ctx)
ctx) ctx)
(defn- parse-all [src] (defn- parse-all [src]
@ -53,6 +80,7 @@
treated as defs (also interned in the ctx so later forms resolve their vars), treated as defs (also interned in the ctx so later forms resolve their vars),
and the last form is the expression whose value the program prints." and the last form is the expression whose value the program prints."
[ctx src] [ctx src]
(ensure-clj-emitter ctx)
(def forms (parse-all src)) (def forms (parse-all src))
(assert (> (length forms) 0) "compile-program: empty program") (assert (> (length forms) 0) "compile-program: empty program")
(def n (length forms)) (def n (length forms))
@ -61,9 +89,9 @@
(def f (in forms i)) (def f (in forms i))
# emit the def, then intern it (interpreted) so a later form's reference to # emit the def, then intern it (interpreted) so a later form's reference to
# this var resolves to a :var node rather than an unresolved symbol. # this var resolves to a :var node rather than an unresolved symbol.
(array/push def-scm (emit/emit (backend/analyze-form ctx f))) (array/push def-scm (cemit ctx (backend/analyze-form ctx f)))
(evlr/eval-form ctx @{} f)) (evlr/eval-form ctx @{} f))
(def final-scm (emit/emit (backend/analyze-form ctx (in forms (- n 1))))) (def final-scm (cemit ctx (backend/analyze-form ctx (in forms (- n 1)))))
(emit/program def-scm final-scm)) (emit/program def-scm final-scm))
# Drain a pipe to EOF. A single (ev/read pipe N) can return BEFORE the child has # Drain a pipe to EOF. A single (ev/read pipe N) can return BEFORE the child has
@ -158,7 +186,8 @@
assembly via emit/program or program-with-prelude)." assembly via emit/program or program-with-prelude)."
[ctx &opt core-dir] [ctx &opt core-dir]
(default core-dir "jolt-core/clojure/core/") (default core-dir "jolt-core/clojure/core/")
(emit/set-prelude-mode! true) (ensure-clj-emitter ctx)
(cset-prelude! ctx true)
(def prev-ns (tctx/ctx-current-ns ctx)) (def prev-ns (tctx/ctx-current-ns ctx))
(tctx/ctx-set-current-ns ctx "clojure.core") (tctx/ctx-set-current-ns ctx "clojure.core")
(def out @[]) (def out @[])
@ -179,7 +208,7 @@
# already carry explicit ns names). Macros have no runtime value either. # already carry explicit ns names). Macros have no runtime value either.
(unless (or ns-form? (macro-form? f)) (unless (or ns-form? (macro-form? f))
(++ total) (++ total)
(def res (protect (emit/emit (backend/analyze-form ctx f)))) (def res (protect (cemit ctx (backend/analyze-form ctx f))))
(when (res 0) (when (res 0)
(++ emitted) (++ emitted)
# Tolerant load guard: a form that fails to LOAD (currently only the 8 # Tolerant load guard: a form that fails to LOAD (currently only the 8
@ -197,7 +226,7 @@
(each [ns-name path] stdlib-ns-files (each [ns-name path] stdlib-ns-files
(emit-ns-forms ns-name (slurp path))) (emit-ns-forms ns-name (slurp path)))
(tctx/ctx-set-current-ns ctx prev-ns) (tctx/ctx-set-current-ns ctx prev-ns)
(emit/set-prelude-mode! false) (cset-prelude! ctx false)
[(string/join out "\n") emitted total]) [(string/join out "\n") emitted total])
(defn program-with-prelude (defn program-with-prelude
@ -224,11 +253,12 @@
resolves via var-deref. Returns [code stdout stderr], or [:emit-err msg \"\"] resolves via var-deref. Returns [code stdout stderr], or [:emit-err msg \"\"]
if the user form itself can't be emitted." if the user form itself can't be emitted."
[ctx src prelude-path &opt scheme-out] [ctx src prelude-path &opt scheme-out]
(emit/set-prelude-mode! true) (ensure-clj-emitter ctx)
(cset-prelude! ctx true)
(def form (in (r/parse-next src) 0)) (def form (in (r/parse-next src) 0))
(scan-eval-requires! ctx form) (scan-eval-requires! ctx form)
(def res (protect (emit/emit (backend/analyze-form ctx form)))) (def res (protect (cemit ctx (backend/analyze-form ctx form))))
(emit/set-prelude-mode! false) (cset-prelude! ctx false)
(if (not (res 0)) (if (not (res 0))
[:emit-err (string (res 1)) ""] [:emit-err (string (res 1)) ""]
(let [prog (program-with-prelude prelude-path (res 1)) (let [prog (program-with-prelude prelude-path (res 1))

View file

@ -17,7 +17,8 @@
(def parts @[]) (def parts @[])
(each tf d/core-tier-files (each tf d/core-tier-files
(array/push parts (slurp (string "jolt-core/clojure/core/" tf ".clj")))) (array/push parts (slurp (string "jolt-core/clojure/core/" tf ".clj"))))
(each f ["host/chez/emit.janet" "host/chez/driver.janet" "host/chez/rt.ss" (each f ["jolt-core/jolt/backend_scheme.clj" "src/jolt/host_iface.janet"
"host/chez/emit.janet" "host/chez/driver.janet" "host/chez/rt.ss"
"host/chez/values.ss" "host/chez/collections.ss" "host/chez/seq.ss" "host/chez/values.ss" "host/chez/collections.ss" "host/chez/seq.ss"
"host/chez/atoms.ss" "host/chez/predicates.ss" "host/chez/regex.ss" "host/chez/atoms.ss" "host/chez/predicates.ss" "host/chez/regex.ss"
"host/chez/ns.ss" "host/chez/post-prelude.ss" "host/chez/natives-meta.ss" "host/chez/ns.ss" "host/chez/post-prelude.ss" "host/chez/natives-meta.ss"

View file

@ -148,9 +148,10 @@
(keyword? v) (if-let [kns (namespace v)] (keyword? v) (if-let [kns (namespace v)]
(str "(keyword " (chez-str-lit kns) " " (chez-str-lit (name v)) ")") (str "(keyword " (chez-str-lit kns) " " (chez-str-lit (name v)) ")")
(str "(keyword #f " (chez-str-lit (name v)) ")")) (str "(keyword #f " (chez-str-lit (name v)) ")"))
;; jolt char value {:ch <codepoint> :jolt/type :jolt/char} ;; jolt char value {:ch <codepoint> :jolt/type :jolt/char}. Use the host
(and (map? v) (= :jolt/char (:jolt/type v))) ;; contract form-char? — a :jolt/type-tagged struct is not a plain map? in
(str "(integer->char " (:ch v) ")") ;; jolt, so a native map? test misses it.
(form-char? v) (str "(integer->char " (get v :ch) ")")
:else (throw (ex-info (str "emit-const: unsupported literal " (pr-str v)) {})))) :else (throw (ex-info (str "emit-const: unsupported literal " (pr-str v)) {}))))
;; Emit a call `(ctor a0 a1 ...)` with the args evaluated LEFT-TO-RIGHT. Chez's ;; Emit a call `(ctor a0 a1 ...)` with the args evaluated LEFT-TO-RIGHT. Chez's

View file

@ -170,6 +170,9 @@
(check "inst eq" `(= #inst "2020-01-01" #inst "2020-01-01")`) (check "inst eq" `(= #inst "2020-01-01" #inst "2020-01-01")`)
(check "uuid eq" `(= #uuid "00000000-0000-0000-0000-000000000000" #uuid "00000000-0000-0000-0000-000000000000")`) (check "uuid eq" `(= #uuid "00000000-0000-0000-0000-000000000000" #uuid "00000000-0000-0000-0000-000000000000")`)
(check "regex smoke" `(do (def r #"[0-9]+") true)`) (check "regex smoke" `(do (def r #"[0-9]+") true)`)
(check "char eq" `(= \a \a)`)
(check "char int" `(do (def c \newline) (= c \newline))`)
(check "quoted char" `(= (quote \z) \z)`)
(check "mandelbrot run(20)" (check "mandelbrot run(20)"
(string `` (string ``

View file

@ -81,7 +81,7 @@
# wrapper — (if (jolt-truthy? (< a b)) …) === (if (< a b) …). Sound because # wrapper — (if (jolt-truthy? (< a b)) …) === (if (< a b) …). Sound because
# jolt-truthy? of #t/#f is identity. The hot fib/mandelbrot tests are all # jolt-truthy? of #t/#f is identity. The hot fib/mandelbrot tests are all
# comparisons, so this is a direct ceiling lever. Inspect the emitted Scheme. # comparisons, so this is a direct ceiling lever. Inspect the emitted Scheme.
(defn- emit-scm [src] (emit/emit (backend/analyze-form ctx (in (r/parse-next src) 0)))) (defn- emit-scm [src] (d/scheme-emit ctx (backend/analyze-form ctx (in (r/parse-next src) 0))))
(each [label src wrapped?] [["if < elides wrapper" "(fn [n] (if (< n 2) 1 2))" false] (each [label src wrapped?] [["if < elides wrapper" "(fn [n] (if (< n 2) 1 2))" false]
["if > elides wrapper" "(fn [n] (if (> n 2) 1 2))" false] ["if > elides wrapper" "(fn [n] (if (> n 2) 1 2))" false]
["if = elides wrapper" "(fn [n] (if (= n 2) 1 2))" false] ["if = elides wrapper" "(fn [n] (if (= n 2) 1 2))" false]