From 2a33da87d852047959102f6ccdba4cc51bab2691 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 19 Jun 2026 18:52:33 -0400 Subject: [PATCH] Chez Phase 3 inc 4: swap jolt-chez to the jolt.backend-scheme emitter; full corpus parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- host/chez/driver.janet | 46 +++++++++++++++++++++++++------ host/chez/jolt-chez.janet | 3 +- jolt-core/jolt/backend_scheme.clj | 7 +++-- test/chez/emit-parity.janet | 3 ++ test/chez/emit-test.janet | 2 +- 5 files changed, 48 insertions(+), 13 deletions(-) diff --git a/host/chez/driver.janet b/host/chez/driver.janet index 0800c69..6526ccd 100644 --- a/host/chez/driver.janet +++ b/host/chez/driver.janet @@ -15,8 +15,34 @@ (import ../../src/jolt/reader :as r) (import ../../src/jolt/evaluator :as evlr) (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) +# 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? "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 @@ -35,6 +61,7 @@ call) lowers to a var-deref instead of failing to compile (jolt-9ls5)." (def ctx (api/init {:compile? true})) (put (get ctx :env) :late-bind-unresolved? true) + (ensure-clj-emitter ctx) ctx) (defn- parse-all [src] @@ -53,6 +80,7 @@ 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." [ctx src] + (ensure-clj-emitter ctx) (def forms (parse-all src)) (assert (> (length forms) 0) "compile-program: empty program") (def n (length forms)) @@ -61,9 +89,9 @@ (def f (in forms i)) # 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. - (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)) - (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)) # 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)." [ctx &opt core-dir] (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)) (tctx/ctx-set-current-ns ctx "clojure.core") (def out @[]) @@ -179,7 +208,7 @@ # already carry explicit ns names). Macros have no runtime value either. (unless (or ns-form? (macro-form? f)) (++ total) - (def res (protect (emit/emit (backend/analyze-form ctx f)))) + (def res (protect (cemit ctx (backend/analyze-form ctx f)))) (when (res 0) (++ emitted) # Tolerant load guard: a form that fails to LOAD (currently only the 8 @@ -197,7 +226,7 @@ (each [ns-name path] stdlib-ns-files (emit-ns-forms ns-name (slurp path))) (tctx/ctx-set-current-ns ctx prev-ns) - (emit/set-prelude-mode! false) + (cset-prelude! ctx false) [(string/join out "\n") emitted total]) (defn program-with-prelude @@ -224,11 +253,12 @@ resolves via var-deref. Returns [code stdout stderr], or [:emit-err msg \"\"] if the user form itself can't be emitted." [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)) (scan-eval-requires! ctx form) - (def res (protect (emit/emit (backend/analyze-form ctx form)))) - (emit/set-prelude-mode! false) + (def res (protect (cemit ctx (backend/analyze-form ctx form)))) + (cset-prelude! ctx false) (if (not (res 0)) [:emit-err (string (res 1)) ""] (let [prog (program-with-prelude prelude-path (res 1)) diff --git a/host/chez/jolt-chez.janet b/host/chez/jolt-chez.janet index 80fc484..4dc0d3f 100644 --- a/host/chez/jolt-chez.janet +++ b/host/chez/jolt-chez.janet @@ -17,7 +17,8 @@ (def parts @[]) (each tf d/core-tier-files (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/atoms.ss" "host/chez/predicates.ss" "host/chez/regex.ss" "host/chez/ns.ss" "host/chez/post-prelude.ss" "host/chez/natives-meta.ss" diff --git a/jolt-core/jolt/backend_scheme.clj b/jolt-core/jolt/backend_scheme.clj index fb5f338..42759dd 100644 --- a/jolt-core/jolt/backend_scheme.clj +++ b/jolt-core/jolt/backend_scheme.clj @@ -148,9 +148,10 @@ (keyword? v) (if-let [kns (namespace v)] (str "(keyword " (chez-str-lit kns) " " (chez-str-lit (name v)) ")") (str "(keyword #f " (chez-str-lit (name v)) ")")) - ;; jolt char value {:ch :jolt/type :jolt/char} - (and (map? v) (= :jolt/char (:jolt/type v))) - (str "(integer->char " (:ch v) ")") + ;; jolt char value {:ch :jolt/type :jolt/char}. Use the host + ;; contract form-char? — a :jolt/type-tagged struct is not a plain map? in + ;; 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)) {})))) ;; Emit a call `(ctor a0 a1 ...)` with the args evaluated LEFT-TO-RIGHT. Chez's diff --git a/test/chez/emit-parity.janet b/test/chez/emit-parity.janet index b013f40..912b0e1 100644 --- a/test/chez/emit-parity.janet +++ b/test/chez/emit-parity.janet @@ -170,6 +170,9 @@ (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 "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)" (string `` diff --git a/test/chez/emit-test.janet b/test/chez/emit-test.janet index 96ef2c3..4e8bb08 100644 --- a/test/chez/emit-test.janet +++ b/test/chez/emit-test.janet @@ -81,7 +81,7 @@ # 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 # 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] ["if > elides wrapper" "(fn [n] (if (> n 2) 1 2))" false] ["if = elides wrapper" "(fn [n] (if (= n 2) 1 2))" false]