diff --git a/host/chez/driver.janet b/host/chez/driver.janet new file mode 100644 index 0000000..9316733 --- /dev/null +++ b/host/chez/driver.janet @@ -0,0 +1,73 @@ +# Phase 1 (jolt-cf1q.2) — live-analyzer -> Chez driver. +# +# Boots a real jolt ctx, runs the EXISTING Janet-hosted analyzer on actual +# Clojure source to produce host-neutral IR, feeds that IR to the Scheme emitter +# (emit.janet), and assembles a runnable Chez program. This is the Option-2 +# backend swap end to end: same front end, Scheme back end, run on Chez. +# +# Analysis still happens on Janet here (the analyzer is portable Clojure but not +# yet bootstrapped onto Chez — that's Phase 2); EXECUTION happens on Chez. The +# point of this increment is to validate that the real IR the analyzer emits +# compiles to correct, fast Scheme. + +(import ../../src/jolt/api :as api) +(import ../../src/jolt/backend :as backend) +(import ../../src/jolt/reader :as r) +(import ../../src/jolt/evaluator :as evlr) +(import ./emit :as emit) + +(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 + corpus dir is absent." + [] + (def r (protect (let [p (os/spawn ["chez" "--version"] :p {:out :pipe :err :pipe})] + (ev/read (p :out) 1024) + (ev/read (p :err) 1024) + (os/proc-wait p)))) + (and (r 0) (zero? (r 1)))) + +(defn make-ctx [] + "A compile-mode jolt ctx (the analyzer pipeline is only built under :compile?)." + (api/init {:compile? true})) + +(defn- parse-all [src] + (def out @[]) + (var s src) + (while (> (length (string/trim s)) 0) + (def parsed (r/parse-next s)) + (set s (in parsed 1)) + (def f (in parsed 0)) + (unless (nil? f) (array/push out f))) + out) + +(defn compile-program + "Compile a Clojure program string to a runnable Chez program. Every top-level + form is analyzed to real IR and emitted to Scheme; all but the LAST form are + 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] + (def forms (parse-all src)) + (assert (> (length forms) 0) "compile-program: empty program") + (def n (length forms)) + (def def-scm @[]) + (for i 0 (- n 1) + (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))) + (evlr/eval-form ctx @{} f)) + (def final-scm (emit/emit (backend/analyze-form ctx (in forms (- n 1))))) + (emit/program def-scm final-scm)) + +(defn run-on-chez + "Compile `src` and run it on Chez; returns [exit-code stdout stderr]." + [ctx src &opt scheme-out] + (def prog (compile-program ctx src)) + (def path (or scheme-out "/tmp/chez-jolt-prog.ss")) + (spit path prog) + (def proc (os/spawn ["chez" "--script" path] :p {:out :pipe :err :pipe})) + (def out (ev/read (proc :out) 0x100000)) + (def err (ev/read (proc :err) 0x100000)) + (def code (os/proc-wait proc)) + [code (string/trim (if out (string out) "")) (string/trim (if err (string err) ""))]) diff --git a/host/chez/emit.janet b/host/chez/emit.janet index 18698b4..8905762 100644 --- a/host/chez/emit.janet +++ b/host/chez/emit.janet @@ -1,29 +1,64 @@ # Phase 1 — jolt IR -> Chez Scheme emitter (jolt-cf1q.2). # # The new back end: consumes the SAME host-neutral IR (jolt.ir, see -# jolt-core/jolt/ir.clj) the analyzer produces and the Janet backend consumes, -# but emits Scheme source text instead of Janet. `host/compile` (Chez `eval`) -# turns that into a procedure. This increment covers the pure-functional subset -# (const/local/var/rt/if/do/let/fn/invoke/def/loop/recur) — enough to run -# fib/mandelbrot-shaped code through the REAL IR. Globals are early-bound here; -# var-cell late binding is the next increment. +# jolt-core/jolt/ir.clj) the live analyzer produces and the Janet backend +# consumes, but emits Scheme source text instead of Janet. `host/compile` (Chez +# `eval`) turns that into a procedure. Covers the pure-functional + numeric +# subset (const/local/var/host/if/do/let/fn/invoke/def/loop/recur) — enough to +# run fib/mandelbrot-shaped code through the REAL analyzer. # -# IR nodes are plain :op-tagged structs/tables (keyword keys), matching ir.clj. +# IR access mirrors the Janet backend: live IR fields are jolt VALUES — vectors +# are persistent (pv), and a nil-valued node densifies to a phm. `nn`/`vv` below +# normalize both into Janet structs/arrays, so the same code drives hand-built +# IR (the unit tests) and live analyzer output (the driver). -(def rt-map - # jolt RT primitive name -> Scheme. = is the exactness-aware jolt= from - # values.ss; inc/dec/quot get preamble shims. Arithmetic/compare are native. +(import ../../src/jolt/pv :as pv) +(import ../../src/jolt/phm :as phm) + +# Normalize a node (phm -> struct) and a vector field (pvec -> array view); both +# pass plain Janet values through untouched, so hand-built IR still works. +(defn- nn [n] (if (phm/phm? n) (phm/phm-to-struct n) n)) +(defn- vv [x] (if (pv/pvec? x) (pv/pv->array x) x)) + +# Hot clojure.core primitives lowered to native Scheme, mirroring the Janet +# backend's native-ops (documented numbers-only relaxation). `=` is the +# exactness-aware jolt= from values.ss; inc/dec/not are rt shims; mod/rem/quot +# map to Scheme's (correct: Scheme has all three, unlike Janet which lacked quot). +(def- native-ops {"+" "+" "-" "-" "*" "*" "/" "/" "<" "<" ">" ">" "<=" "<=" ">=" ">=" - "=" "jolt=" "inc" "jolt-inc" "dec" "jolt-dec" - "mod" "modulo" "quot" "quotient" "rem" "remainder"}) + "=" "jolt=" "inc" "jolt-inc" "dec" "jolt-dec" "not" "jolt-not" + "min" "min" "max" "max" + "mod" "modulo" "rem" "remainder" "quot" "quotient"}) + +# Unary ops only legal at arity 1; binary at arity 2. Others (arith/compare) are +# variadic in both Scheme and jolt, so any arity is fine. +(def- unary-ops {"inc" true "dec" true "not" true}) +(def- binary-ops {"mod" true "rem" true "quot" true}) + +# If fnode is a clojure.core (or host) ref to a native-op primitive, return the +# Scheme op string — only at an arity where the Scheme op and the jolt fn agree. +(defn- native-op [fnode nargs] + (def nm (case (get fnode :op) + :var (when (= "clojure.core" (get fnode :ns)) (get fnode :name)) + :host (get fnode :name) + nil)) + (def op (and nm (get native-ops nm))) + (cond + (nil? op) nil + (and (get unary-ops nm) (not= nargs 1)) nil + (and (get binary-ops nm) (not= nargs 2)) nil + op)) (var- recur-target nil) (var- gensym-n 0) (defn- fresh-label [prefix] (string prefix (++ gensym-n))) -# MVP: jolt local/var names are valid Scheme identifiers (inc, even?, + all are). -(defn- munge [name] name) +# Most jolt names are already valid Scheme identifiers (inc, even?, +, ->str all +# are — Scheme allows ! $ % & * + - . / : < = > ? @ ^ _ ~). The one that isn't is +# `#`, which jolt auto-gensyms use as a suffix (e.g. p1__0000X4# from #(...) +# shorthand) — `#` starts a datum in Scheme, so replace it with `_`. +(defn- munge [name] (string/replace-all "#" "_" name)) (var emit nil) # forward declaration (mutual recursion with the helpers below) @@ -31,76 +66,126 @@ (cond (nil? v) "jolt-nil" (boolean? v) (if v "#t" "#f") - (number? v) (string v) + # jolt models every number as a double (no ratios/bignums; see reader.janet). + # Emit flonums so arithmetic matches the Janet host and Chez doesn't fall into + # exploding exact rationals (mandelbrot). Integer-valued -> append ".0". + (number? v) (let [s (string v)] + (if (or (string/find "." s) (string/find "e" s) (string/find "n" s)) + s + (string s ".0"))) (string? v) (string/format "%j" v) # quoted+escaped string literal (errorf "emit-const: unsupported literal %p" v))) (defn- emit-binding [b] + (def b (vv b)) (string "(" (munge (get b 0)) " " (emit (get b 1)) ")")) (defn- emit-let [node] - (string "(let* (" (string/join (map emit-binding (get node :bindings)) " ") ") " + (string "(let* (" (string/join (map emit-binding (vv (get node :bindings))) " ") ") " (emit (get node :body)) ")")) (defn- emit-loop [node] (def label (fresh-label "loop")) - (def bs (string/join (map emit-binding (get node :bindings)) " ")) + (def pairs (map vv (vv (get node :bindings)))) + (def names (map |(munge (get $ 0)) pairs)) + # inits are evaluated in the OUTER scope (recur-target unchanged) and, like + # Clojure loop/let, SEQUENTIALLY — a later init sees earlier bindings. Scheme's + # named `let` binds in parallel, so wrap a sequential let* around the loop. + (def inits (map |(emit (get $ 1)) pairs)) + (def seq-bs (string/join (map (fn [n i] (string "(" n " " i ")")) names inits) " ")) + (def rebinds (string/join (map (fn [n] (string "(" n " " n ")")) names) " ")) (def prev recur-target) (set recur-target label) (def body (emit (get node :body))) (set recur-target prev) - (string "(let " label " (" bs ") " body ")")) + (string "(let* (" seq-bs ") (let " label " (" rebinds ") " body "))")) (defn- emit-recur [node] (unless recur-target (error "emit: recur outside a loop/fn target")) - (string "(" recur-target " " (string/join (map emit (get node :args)) " ") ")")) + (string "(" recur-target " " (string/join (map emit (vv (get node :args))) " ") ")")) (defn- emit-fn [node] - (def arities (get node :arities)) + (def arities (map nn (vv (get node :arities)))) (when (not= 1 (length arities)) (error "emit: multi-arity fn not in this increment")) (def a (first arities)) (when (get a :rest) (error "emit: variadic fn not in this increment")) - (def params (map munge (get a :params))) + (def params (map munge (vv (get a :params)))) # wrap the body in a named let so fn-level `recur` rebinds the params (def label (fresh-label "fnrec")) (def prev recur-target) (set recur-target label) (def body (emit (get a :body))) (set recur-target prev) - (string "(lambda (" (string/join params " ") ") " - "(let " label " (" (string/join (map (fn [p] (string "(" p " " p ")")) params) " ") ") " - body "))")) + (def lambda + (string "(lambda (" (string/join params " ") ") " + "(let " label " (" (string/join (map (fn [p] (string "(" p " " p ")")) params) " ") ") " + body "))")) + # A named fn (defn / (fn self [..])) references itself by name — the analyzer + # binds that name as a :local in the body. letrec makes the name visible to the + # lambda so self-calls resolve (recur stays a separate self-call to the arity). + (if-let [nm (get node :name)] + (let [m (munge nm)] (string "(letrec ((" m " " lambda ")) " m ")")) + lambda)) + +# The Clojure stdlib (clojure.core, clojure.math, clojure.string, …) and host +# interop (Math/sqrt etc.) have no implementation on Chez yet (Phase 2+). A +# reference to one — except a clojure.core call lowered to a native op — is +# genuinely uncompilable here. Reject it at emit time (a clean "out of subset" +# signal) rather than emitting a var-deref that resolves to nil and fails +# confusingly at runtime. +(defn- stdlib-var? [n] + (and (= :var (get n :op)) (string/has-prefix? "clojure." (or (get n :ns) "")))) + +(defn- emit-invoke [node] + (def fnode (nn (get node :fn))) + (def args (map emit (vv (get node :args)))) + (def nop (native-op fnode (length args))) + (cond + # zero-arg + / * : Scheme's identity is the EXACT 0 / 1, but jolt models every + # number as a double, so emit the flonum identity to keep (= 0 (+)) true. + (and nop (empty? args) (= nop "+")) "0.0" + (and nop (empty? args) (= nop "*")) "1.0" + nop (string "(" nop " " (string/join args " ") ")") + (stdlib-var? fnode) + (errorf "emit: unsupported stdlib fn `%s/%s` (no core on Chez yet)" (get fnode :ns) (get fnode :name)) + (= :host (get fnode :op)) + (errorf "emit: unsupported host call `%s` (no host interop on Chez yet)" (get fnode :name)) + (string "(" (emit fnode) " " (string/join args " ") ")"))) (set emit (fn emit [node] + (def node (nn node)) (case (get node :op) :const (emit-const (get node :val)) :local (munge (get node :name)) - :var (munge (get node :name)) # early-bound (MVP) - :rt (or (get rt-map (get node :name)) - (errorf "emit: unmapped rt primitive %s" (get node :name))) - :host (get node :name) + # late-bound var: read the cell's current root at use time. A value-position + # ref to a stdlib var (e.g. passing `inc` to (map inc xs)) needs a real fn, + # which native-op lowering doesn't provide — so it's out of subset regardless. + :var (if (stdlib-var? node) + (errorf "emit: unsupported stdlib ref `%s/%s` (no core on Chez yet)" (get node :ns) (get node :name)) + (string "(var-deref " (string/format "%j" (get node :ns)) " " + (string/format "%j" (get node :name)) ")")) + :host (errorf "emit: unsupported host ref `%s` (no host interop on Chez yet)" (get node :name)) :if (string "(if (jolt-truthy? " (emit (get node :test)) ") " (emit (get node :then)) " " (emit (get node :else)) ")") :do (string "(begin " - (string/join (map emit (get node :statements)) " ") - (if (empty? (get node :statements)) "" " ") + (string/join (map emit (vv (get node :statements))) " ") + (if (empty? (vv (get node :statements))) "" " ") (emit (get node :ret)) ")") - :invoke (string "(" (emit (get node :fn)) " " - (string/join (map emit (get node :args)) " ") ")") + :invoke (emit-invoke node) :let (emit-let node) :loop (emit-loop node) :recur (emit-recur node) :fn (emit-fn node) - :def (string "(define " (munge (get node :name)) " " (emit (get node :init)) ")") + :def (string "(def-var! " (string/format "%j" (get node :ns)) " " + (string/format "%j" (get node :name)) " " (emit (get node :init)) ")") (errorf "emit: unhandled op %p" (get node :op))))) -# Wrap emitted top-level forms into a runnable Chez program: preamble (value -# model + rt shims) then the forms, then print `final` (a Scheme expr string). +# Wrap emitted top-level forms into a runnable Chez program: load the RT, then +# the def forms, then print `final` (an emitted Scheme expr string) via jolt's +# number/value printing. (defn program [forms-scheme final] (string "(import (chezscheme))\n" - "(load \"host/chez/values.ss\")\n" - "(define (jolt-inc x) (+ x 1))\n" - "(define (jolt-dec x) (- x 1))\n" + "(load \"host/chez/rt.ss\")\n" (string/join forms-scheme "\n") "\n" - "(printf \"~a\\n\" " final ")\n")) + "(printf \"~a\\n\" (jolt-pr-str " final "))\n")) diff --git a/host/chez/rt.ss b/host/chez/rt.ss new file mode 100644 index 0000000..24e136b --- /dev/null +++ b/host/chez/rt.ss @@ -0,0 +1,52 @@ +;; Phase 1 (jolt-cf1q.2) — the minimal Chez RT the emitted Scheme rests on. +;; +;; Sits above the value model (values.ss) and below an emitted program. Adds the +;; two things the back end's output references that aren't in the value layer: +;; 1. the var-cell late-binding registry (Clojure vars — a global root that a +;; reference reads at call time, so redefinition / mutual recursion work); +;; 2. the rt primitive shims the emitter names (jolt-inc/dec/not) and jolt's +;; number printing (all jolt numbers model Clojure doubles; integer-valued +;; print without a trailing ".0", matching the Janet host). +;; +;; Emitted programs do `(load "host/chez/rt.ss")`; this loads values.ss in turn. + +(load "host/chez/values.ss") + +;; --- rt arithmetic / logic shims (named in emit.janet's native-ops) ---------- +(define (jolt-inc x) (+ x 1)) +(define (jolt-dec x) (- x 1)) +;; jolt `not`: only nil and false are falsey. +(define (jolt-not x) (if (jolt-truthy? x) #f #t)) + +;; --- var cells: late-bound global roots (Clojure vars) ----------------------- +;; A var is a mutable cell keyed by "ns/name". A `:def` sets the root; a `:var` +;; reference reads it at use time (late binding), so a forward/mutually-recursive +;; reference resolves to whatever the cell holds when the call actually runs. +(define-record-type var-cell (fields ns name (mutable root)) (nongenerative var-cell-v1)) +(define var-table (make-hashtable string-hash string=?)) +(define (jolt-var ns name) + (let ((k (string-append ns "/" name))) + (or (hashtable-ref var-table k #f) + (let ((c (make-var-cell ns name jolt-nil))) + (hashtable-set! var-table k c) + c)))) +(define (var-deref ns name) (var-cell-root (jolt-var ns name))) +(define (def-var! ns name v) (var-cell-root-set! (jolt-var ns name) v) v) + +;; --- jolt number printing ---------------------------------------------------- +;; jolt models every number as a Clojure double: integer-valued values print +;; without a ".0" (the Janet host prints (* 1.0 5) as "5", (/ 1 2) as "0.5"). +(define (jolt-num->string x) + (if (and (rational? x) (integer? x)) + (number->string (exact x)) + (number->string x))) + +;; Minimal pr-str for the program's final value (full printer is Phase 2). +(define (jolt-pr-str x) + (cond + ((jolt-nil? x) "nil") + ((eq? x #t) "true") + ((eq? x #f) "false") + ((number? x) (jolt-num->string x)) + ((string? x) x) + (else (format "~a" x)))) diff --git a/test/chez/README.md b/test/chez/README.md index ec25814..59f958e 100644 --- a/test/chez/README.md +++ b/test/chez/README.md @@ -36,3 +36,21 @@ gate's job is to catch *regressions* the port introduces, not to bless these. 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 (2026-06-17): **182/182 compiled cases pass, 0 divergences**; 2473/2655 +out of subset (await core on Chez). 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 + regressions); both skip cleanly when +`chez` isn't on PATH. diff --git a/test/chez/emit-test.janet b/test/chez/emit-test.janet index a737a8b..bc8133b 100644 --- a/test/chez/emit-test.janet +++ b/test/chez/emit-test.janet @@ -1,73 +1,100 @@ -# Phase 1 — IR -> Scheme emitter tests. Hand-built IR in the real ir.clj shapes, -# emitted to Scheme, compiled+run on Chez, results + fib speed checked. +# Phase 1 (jolt-cf1q.2) — REAL pipeline end to end: actual Clojure source -> +# Janet-hosted analyzer -> host-neutral IR -> Scheme emitter -> run on Chez. +# Correctness is checked by parity against the SAME program evaluated by the +# Janet host (jolt's own oracle), so a divergence is the back end's, not the +# program's. # janet test/chez/emit-test.janet (from repo root) -(import ../../host/chez/emit :as e) +(import ../../src/jolt/api :as api) +(import ../../src/jolt/backend :as backend) +(import ../../src/jolt/reader :as r) +(import ../../host/chez/driver :as d) +(import ../../host/chez/emit :as emit) -(defn run-chez [src] - (spit "/tmp/emit-prog.ss" src) - (def proc (os/spawn ["chez" "--script" "/tmp/emit-prog.ss"] :p {:out :pipe :err :pipe})) - (def out (ev/read (proc :out) 0x100000)) - (def err (ev/read (proc :err) 0x100000)) - (def code (os/proc-wait proc)) - [code (string/trim (if out (string out) "")) (string/trim (if err (string err) ""))]) +(unless (d/chez-available?) + (print "skip: chez not on PATH") + (os/exit 0)) (var total 0) (var fails 0) -(defn ok [name pred] (++ total) (unless pred (++ fails) (printf "FAIL: %s" name))) +(defn ok [name pred &opt extra] + (++ total) + (if pred (printf "ok: %s" name) + (do (++ fails) (printf "FAIL: %s %s" name (or extra ""))))) -# --- IR builders (ir.clj shapes) --- -(defn rt [name & args] {:op :invoke :fn {:op :rt :name name} :args args}) -(defn lcl [n] {:op :local :name n}) -(defn k [v] {:op :const :val v}) +# Janet-host oracle: evaluate the same program, stringify its value the way jolt +# prints it at the CLI (so "832040" not "832040.0", "0.5" not 1/2, etc.). +(def oracle-ctx (api/init {:compile? true})) +(defn oracle [src] (string (api/load-string oracle-ctx src))) -# 1) (+ 1 2) -(def add-ir (rt "+" (k 1) (k 2))) -(let [[code out err] (run-chez (e/program [] (e/emit add-ir)))] - (ok "(+ 1 2) = 3" (and (= code 0) (= out "3"))) - (when (not= code 0) (printf " err: %s" err))) +(def ctx (d/make-ctx)) -# 2) fib def + (fib 30) -(defn fib-call [arg] {:op :invoke :fn {:op :var :ns "user" :name "fib"} :args [arg]}) -(def fib-def - {:op :def :ns "user" :name "fib" - :init {:op :fn :name "fib" - :arities [{:params ["n"] - :body {:op :if - :test (rt "<" (lcl "n") (k 2)) - :then (lcl "n") - :else (rt "+" (fib-call (rt "-" (lcl "n") (k 1))) - (fib-call (rt "-" (lcl "n") (k 2))))}}]}}) -(let [prog (e/program [(e/emit fib-def)] (e/emit (fib-call (k 30)))) - [code out err] (run-chez prog)] - (ok "(fib 30) = 832040" (and (= code 0) (= out "832040"))) - (when (not= code 0) (printf " err: %s" err))) +# 1) constant-folded arithmetic: (+ 1 2) -> the analyzer folds to const 3. +(let [[code out err] (d/run-on-chez ctx "(+ 1 2)")] + (ok "(+ 1 2) = 3" (and (= code 0) (= out "3") (= out (oracle "(+ 1 2)"))) (string out " | " err))) -# 3) loop/recur sum 1..5 = 15 -(def loop-ir - {:op :loop - :bindings [["i" (k 1)] ["acc" (k 0)]] - :body {:op :if - :test (rt ">" (lcl "i") (k 5)) - :then (lcl "acc") - :else {:op :recur :args [(rt "inc" (lcl "i")) (rt "+" (lcl "acc") (lcl "i"))]}}}) -(let [[code out err] (run-chez (e/program [] (e/emit loop-ir)))] - (ok "loop/recur sum = 15" (and (= code 0) (= out "15"))) - (when (not= code 0) (printf " err: %s" err))) +# 2) fib: var-cell def + named-fn self-recursion + native arith, via real IR. +(let [src "(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) (fib 30)" + [code out err] (d/run-on-chez ctx src)] + (ok "(fib 30) = 832040" (and (= code 0) (= out "832040") (= out (oracle src))) (string out " | " err))) -# 4) speed: emitted fib(30) should hit ~the spike ceiling (hand-Scheme ~5ms), -# proving the IR->Scheme path adds no overhead vs hand-written Scheme. -(def timed-fib - (string (e/emit fib-def) "\n" - "(define (now-ns) (let ((t (current-time 'time-monotonic))) (+ (* (time-second t) 1000000000) (time-nanosecond t))))\n" - "(fib 24)(fib 24)\n" - "(let* ((t0 (now-ns)) (r (fib 30)) (ms (/ (- (now-ns) t0) 1000000.0)))\n" - " (printf \"~a ~a\\n\" r (exact->inexact ms)))")) -(let [[code out err] (run-chez (string "(import (chezscheme))\n(load \"host/chez/values.ss\")\n(define (jolt-inc x) (+ x 1))\n" timed-fib))] +# 3) mandelbrot kernel: loop/recur, let, or-expansion, cross-var call +# (run -> count-point), flonum compute. Parity vs the Janet host on run(40). +(def mandel-defs `` +(defn count-point [cr ci cap] + (loop [i 0 zr 0.0 zi 0.0] + (if (or (>= i cap) (> (+ (* zr zr) (* zi zi)) 4.0)) + i + (recur (inc i) + (+ (- (* zr zr) (* zi zi)) cr) + (+ (* 2.0 (* zr zi)) ci))))) +(defn run [n] + (let [cap 200 + nd (* 1.0 n)] + (loop [y 0 acc 0] + (if (< y n) + (let [ci (- (/ (* 2.0 y) nd) 1.0) + row (loop [x 0 a 0] + (if (< x n) + (let [cr (- (/ (* 2.0 x) nd) 1.5)] + (recur (inc x) (+ a (count-point cr ci cap)))) + a))] + (recur (inc y) (+ acc row))) + acc)))) +``) +(let [src (string mandel-defs "\n(run 40)") + [code out err] (d/run-on-chez ctx src)] + (ok "mandelbrot run(40) parity" (and (= code 0) (= out (oracle src))) + (string "chez=" out " janet=" (oracle src) " | " err))) + +# 3b) regressions found via the corpus probe: +# - loop binds SEQUENTIALLY (Scheme named-let is parallel); b must see a. +# - #(...) shorthand gensyms params with a trailing `#` (invalid in Scheme). +(each [label src] [["loop sequential init" "(loop [a 1 b (+ a 10)] (+ a b))"] + ["#() shorthand" "(#(+ %1 %2) 1 2)"]] + (let [[code out err] (d/run-on-chez ctx src)] + (ok label (and (= code 0) (= out (oracle src))) (string "chez=" out " janet=" (oracle src) " | " err)))) + +# 4) perf signal: emitted fib(30) in-Scheme timing (excludes Chez startup), to +# track against the spike ceiling (hand-Scheme fib ~5ms). Informational — the +# jolt-truthy? wrapper (~3x) and flonum modeling are known Phase-4 levers. +(let [fib-ir (backend/analyze-form ctx (in (r/parse-next "(defn fib [n] (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2)))))") 0)) + fib-scm (emit/emit fib-ir) + timed (string "(import (chezscheme))\n(load \"host/chez/rt.ss\")\n" + fib-scm "\n" + "(define fib (var-deref \"user\" \"fib\"))\n" + "(define (now-ns) (let ((t (current-time 'time-monotonic))) (+ (* (time-second t) 1000000000) (time-nanosecond t))))\n" + "(fib 24)(fib 24)\n" + "(let* ((t0 (now-ns)) (r (fib 30)) (ms (/ (- (now-ns) t0) 1000000.0)))\n" + " (printf \"~a ~a\\n\" (jolt-pr-str r) (exact->inexact ms)))")] + (spit "/tmp/chez-jolt-fib-timed.ss" timed) + (def proc (os/spawn ["chez" "--script" "/tmp/chez-jolt-fib-timed.ss"] :p {:out :pipe :err :pipe})) + (def out (string/trim (string (ev/read (proc :out) 0x100000)))) + (def err (string/trim (string (or (ev/read (proc :err) 0x100000) "")))) + (def code (os/proc-wait proc)) (def parts (string/split " " out)) (def result (get parts 0)) (def ms (scan-number (or (get parts 1) "999"))) - (ok "emitted fib(30) correct + fast" (and (= code 0) (= result "832040") (< ms 40))) - (printf " emitted fib(30): %s in %.2f ms (hand-Scheme spike ~5ms)" result ms) - (when (not= code 0) (printf " err: %s" err))) + (ok "timed fib(30) correct" (and (= code 0) (= result "832040")) (string out " | " err)) + (printf " emitted fib(30): %s in %.2f ms (hand-Scheme spike ~5ms)" result ms)) (printf "\nemit-test: %d/%d passed" (- total fails) total) (os/exit (if (> fails 0) 1 0)) diff --git a/test/chez/run-corpus-chez.janet b/test/chez/run-corpus-chez.janet new file mode 100644 index 0000000..28d4317 --- /dev/null +++ b/test/chez/run-corpus-chez.janet @@ -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)