From e02ccab4c0384b576894ab32a5f2f7f74236883a Mon Sep 17 00:00:00 2001 From: Yogthos Date: Sat, 6 Jun 2026 18:27:14 -0400 Subject: [PATCH] test: bootstrap fixpoint (stage1 == stage2 == stage3 behavioral parity) Soundness gate for self-hosting (jolt-d0r). stage1 = analyzer built by the Janet bootstrap; stage2 = analyzer rebuilt by compiling jolt.ir + jolt.analyzer through stage1 (self-host); stage3 = the same self-rebuild again. A corpus of programs must produce identical results across all three. Compared BEHAVIORALLY rather than by emitted code: emitted Janet forms embed live setter/getter closures (identity varies per compile) and the IR carries representation-level gensyms and pvec internals, so structural equality fights the representation. Behavioral parity is the property that actually matters and is representation-independent. Also corrects the battery baseline 3920 -> 3919: 3920 was a lucky run; 3919 is the stable value (the 9 timeout-prone tests can shift the count by one). --- .../integration/bootstrap-fixpoint-test.janet | 81 +++++++++++++++++++ .../integration/clojure-test-suite-test.janet | 8 +- 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 test/integration/bootstrap-fixpoint-test.janet diff --git a/test/integration/bootstrap-fixpoint-test.janet b/test/integration/bootstrap-fixpoint-test.janet new file mode 100644 index 0000000..cf5a510 --- /dev/null +++ b/test/integration/bootstrap-fixpoint-test.janet @@ -0,0 +1,81 @@ +# Bootstrap fixpoint (jolt-d0r). +# +# Soundness gate for self-hosting: the self-hosted compiler, rebuilt by compiling +# its OWN source through itself (stage2), must behave identically to the compiler +# built by the Janet bootstrap (stage1). We test this BEHAVIORALLY — run a corpus +# of programs through each stage and compare results — rather than by comparing +# emitted code, because emitted forms embed live setter/getter closures and the IR +# carries representation-level gensyms; behavioral parity is the property that +# actually matters and is representation-independent. +# +# stage1 = analyzer as built by the bootstrap. +# stage2 = analyzer rebuilt by compiling jolt.ir + jolt.analyzer through stage1 +# (self-host, the fractal turn) and installing the result over itself. +# stage3 = the same self-rebuild applied again, on top of stage2. +# All three must produce identical results on the corpus. + +(use ../../src/jolt/types) +(use ../../src/jolt/api) +(use ../../src/jolt/reader) +(import ../../src/jolt/backend :as be) +(import ../../src/jolt/stdlib_embed :as se) + +(defn- forms [src] + (var s src) (def fs @[]) + (while (> (length (string/trim s)) 0) + (def p (parse-next s)) (set s (p 1)) + (when (p 0) (array/push fs (p 0)))) + fs) + +# Programs exercising the compiled constructs (fn/multi-arity/recur/loop/if/let/ +# map+vector literals/closures/higher-order/protocol dispatch). Each is a single +# expression evaluated through the compile pipeline; we compare printed results. +(def corpus + ["(let [f (fn [x] (* x x))] (map f [1 2 3 4]))" + "(loop [i 0 acc 0] (if (< i 10) (recur (inc i) (+ acc i)) acc))" + "((fn fact [n] (if (zero? n) 1 (* n (fact (dec n))))) 6)" + "(reduce + 0 (filter even? (range 20)))" + "(let [[a b & r] [1 2 3 4 5]] [a b r])" + "(mapv (juxt identity inc dec) [10 20])" + "(frequencies (concat [:a :a] [:b]))" + "(group-by odd? (range 8))" + "(get-in {:a {:b {:c 42}}} [:a :b :c])" + "(first {:x 1})" + "(into {} (map (fn [k] [k (* k k)]) (range 5)))" + "((comp inc inc) 10)" + "(apply max [3 1 4 1 5 9 2 6])" + "(str (reverse \"hello\") (count [1 2 3]))" + "(let [m {:a 1}] (assoc m :b (+ (:a m) 1)))"]) + +(defn- run-corpus [ctx] + (map (fn [p] (def r (protect (eval-string ctx p))) + (if (r 0) (string/format "%j" (normalize-pvecs (r 1))) (string "ERR:" (r 1)))) + corpus)) + +# Rebuild the analyzer through the self-hosted pipeline, in place. +(defn- self-rebuild! [ctx] + (def saved (ctx-current-ns ctx)) + (each nsn ["jolt.ir" "jolt.analyzer"] + (ctx-set-current-ns ctx nsn) + (each f (forms (get se/sources nsn)) (protect (be/compile-and-eval ctx f)))) + (ctx-set-current-ns ctx saved)) + +(def ctx (init {:compile? true})) +(def r1 (run-corpus ctx)) # stage1 (bootstrap-built) +(self-rebuild! ctx) +(def r2 (run-corpus ctx)) # stage2 (self-built) +(self-rebuild! ctx) +(def r3 (run-corpus ctx)) # stage3 (self-built from stage2) + +(var failures 0) +(for i 0 (length corpus) + (unless (and (= (r1 i) (r2 i)) (= (r2 i) (r3 i))) + (++ failures) + (printf "FAIL [%s]\n stage1=%s\n stage2=%s\n stage3=%s" (corpus i) (r1 i) (r2 i) (r3 i))) + # also guard against everything silently erroring + (when (string/has-prefix? "ERR:" (r1 i)) + (++ failures) (printf "FAIL [%s] stage1 errored: %s" (corpus i) (r1 i)))) + +(if (pos? failures) + (do (printf "bootstrap-fixpoint: %d failure(s)" failures) (os/exit 1)) + (printf "bootstrap-fixpoint: stage1 == stage2 == stage3 on %d programs\n" (length corpus))) diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet index bd1e05e..0c73c28 100644 --- a/test/integration/clojure-test-suite-test.janet +++ b/test/integration/clojure-test-suite-test.janet @@ -26,9 +26,11 @@ # (sleep 1)))` re-raises the unresolved-`Thread/sleep` error — a documented # platform gap, not a regression in any previously-working behavior. # Raised 3913 -> 3916 with the staged-bootstrap kernel tier (ns-restore-on-throw -# + faithful subvec coercion), then 3916 -> 3920 moving juxt/every-pred/some-fn to -# Clojure (the canonical defs are more correct than the prior Janet ones). -(def baseline-pass 3920) +# + faithful subvec coercion), then 3916 -> 3919 moving juxt/every-pred/some-fn to +# Clojure (the canonical defs are more correct than the prior Janet ones). 3919 is +# the stable value; runs can read 3920 when a timeout-prone test (of the 9 that +# can time out) happens to finish, so the floor is set at the consistent 3919. +(def baseline-pass 3919) # A file is "clean" when it ran with zero failures AND zero errors. (def baseline-clean-files 45) # Per-file wall-clock budget (seconds). Normal files finish in well under 1s;