Merge pull request #20 from jolt-lang/stage2-task3-deliberate-fallback

Stage 2 Task 3: interpreter fallback is deliberate-only
This commit is contained in:
Dmitri Sotnikov 2026-06-10 21:17:38 +08:00 committed by GitHub
commit 771cd10564
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 61 additions and 4 deletions

View file

@ -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: <why>". 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))

View file

@ -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!")