diff --git a/src/jolt/backend.janet b/src/jolt/backend.janet index 498b645..071c4a4 100644 --- a/src/jolt/backend.janet +++ b/src/jolt/backend.janet @@ -352,20 +352,36 @@ # while it runs — so h/current-ns must read this instead of ctx-current-ns. (put (ctx :env) :compile-ns (ctx-current-ns ctx)) (def av (ns-find (ctx-find-ns ctx "jolt.analyzer") "analyze")) + # Pre-kernel bootstrap: ensure-analyzer is gated until the kernel tier loads + # (see api/load-core-overlay!), so a compile request from an earlier tier (e.g. + # 00-syntax's destructure defn) finds no analyzer. That fallback is DESIGNED — + # route it through the sanctioned punt channel rather than crashing on a nil var. + (unless av (error "jolt/uncompilable: analyzer not built (pre-kernel bootstrap)")) (def r ((var-get av) ctx form)) (put (ctx :env) :compile-ns nil) r) +# The analyzer's deliberate punt signal — (uncompilable why) throws the string +# "jolt/uncompilable: ". Anything else escaping the compile step is an +# unexpected compiler error, not a punt. +(defn- uncompilable-error? [err] + (and (or (string? err) (buffer? err)) + (string/has-prefix? "jolt/uncompilable" (string err)))) + (defn compile-and-eval "Self-hosted compile path: analyze (portable Clojure) -> IR -> Janet -> eval. - Hybrid: only the compile step (analyze+emit) is guarded — a form the analyzer - can't handle throws and falls back to the interpreter; runtime errors in - compiled code propagate (no double-eval, no hidden errors)." + The interpreter fallback is DELIBERATE-ONLY (Stage 2): only an analyzer punt + (jolt/uncompilable — the curated stateful/letrec set) falls back; any other + compile-step error is a compiler bug and propagates rather than being silently + hidden by interpretation. Runtime errors in compiled code propagate as before + (no double-eval, no hidden errors)." [ctx form] (def compiled (protect (emit-ir ctx (analyze-form ctx form)))) (if (compiled 0) (eval (compiled 1) (comp/ctx-janet-env ctx)) - (eval-form ctx @{} form))) + (if (uncompilable-error? (compiled 1)) + (eval-form ctx @{} form) + (error (compiled 1))))) (defn analyzer-built? [ctx] (> (length ((ctx-find-ns ctx "jolt.analyzer") :mappings)) 0)) diff --git a/test/unit/compile-fallback-test.janet b/test/unit/compile-fallback-test.janet new file mode 100644 index 0000000..1248128 --- /dev/null +++ b/test/unit/compile-fallback-test.janet @@ -0,0 +1,41 @@ +# Stage 2 Task 3: the compile path's interpreter fallback is DELIBERATE-ONLY. +# +# compile-and-eval used to wrap the compile step in a blanket protect — ANY +# failure (including a genuine compiler bug) silently fell back to the +# interpreter, hiding the bug behind a correct-looking result. Now only the +# analyzer's deliberate punt signal ("jolt/uncompilable: …", raised for the +# curated stateful/letrec set) may fall back; any other compile-step error +# propagates. Verified here by stubbing jolt.analyzer/analyze. + +(use ../../src/jolt/types) +(use ../../src/jolt/api) +(use ../../src/jolt/reader) +(import ../../src/jolt/backend :as backend) + +(def ctx (init)) + +# 1. A deliberate punt (letfn needs letrec IR) falls back and evaluates correctly. +(assert (= 3 (backend/compile-and-eval ctx (parse-string "(letfn [(f [n] (+ n 1))] (f 2))"))) + "deliberate uncompilable punt falls back to the interpreter") + +(assert (backend/analyzer-built? ctx) "analyzer built") +(def analyze-var (ns-find (ctx-find-ns ctx "jolt.analyzer") "analyze")) +(def real-analyze (var-get analyze-var)) + +# 2. A NON-punt compile error must propagate — even though the interpreter could +# evaluate the form fine, it must NOT be silently used (that hides compiler bugs). +(var-set analyze-var (fn [ctx form] (error "boom: simulated compiler bug"))) +(def r (protect (backend/compile-and-eval ctx (parse-string "(+ 1 2)")))) +(assert (not (r 0)) "non-uncompilable compile error must propagate, not silently interpret") +(assert (string/find "boom" (string (r 1))) "the original error is surfaced") + +# 3. The punt marker is the one sanctioned fallback channel. +(var-set analyze-var (fn [ctx form] (error "jolt/uncompilable: stubbed"))) +(assert (= 3 (backend/compile-and-eval ctx (parse-string "(+ 1 2)"))) + "uncompilable punt falls back to the interpreter") + +# Restore the real analyzer and confirm the pipeline still works. +(var-set analyze-var real-analyze) +(assert (= 7 (backend/compile-and-eval ctx (parse-string "(+ 3 4)"))) "restored analyzer compiles") + +(print "All compile-fallback tests passed!")