self-host: analyzer reaches full conformance parity via hybrid fallback

The Clojure analyzer + Janet back end now pass all 218 conformance cases (vs the
interpreter) through the self-hosted pipeline:
- host contract gains special? (the full special-form/uncompilable set), so the
  analyzer falls back for forms it doesn't compile instead of miscompiling them
  as calls;
- qualified symbol refs that don't resolve to a var (janet/…, Math/…, host
  interop) and tagged literals (#"regex", #inst) and set literals throw
  uncompilable -> interpreter;
- the back end's compile-and-eval is now hybrid: only the compile step is
  guarded, runtime errors propagate.

Still interpreter-routed (coverage to grow next): loop/recur, recur in fn, try,
sets, destructuring.
This commit is contained in:
Yogthos 2026-06-06 06:19:45 -04:00
parent 849449aada
commit 5624e99eb6
3 changed files with 55 additions and 14 deletions

View file

@ -100,13 +100,20 @@
(defn- analyze-symbol [ctx form locals]
(let [nm (h/sym-name form) ns (h/sym-ns form)]
(if (and (nil? ns) (contains? locals nm))
(ir/local nm)
(let [r (h/resolve-global ctx form)]
(cond
;; local (only unqualified)
(and (nil? ns) (contains? locals nm)) (ir/local nm)
;; qualified: must resolve to a var, else interpret (handles janet/…,
;; Math/…, and any host interop the back end doesn't model).
ns (let [r (h/resolve-global ctx form)]
(if (= :var (:kind r))
(ir/var-ref (:ns r) (:name r))
(uncompilable (str "qualified ref " ns "/" nm))))
:else (let [r (h/resolve-global ctx form)]
(case (:kind r)
:var (ir/var-ref (:ns r) (:name r))
:host (ir/host-ref (:name r))
;; unresolved: a forward reference in the current ns; resolved at call time
;; unresolved: forward reference in the current ns (resolved at call time)
(ir/var-ref (h/current-ns ctx) nm))))))
(defn- analyze-list [ctx form locals]
@ -119,6 +126,9 @@
(cond
(and hname (not shadowed) (contains? handled hname))
(analyze-special ctx hname items locals)
;; A special form the analyzer doesn't compile -> interpreter.
(and hname (not shadowed) (h/special? hname))
(uncompilable (str "special form " hname))
(and (h/sym? head) (not shadowed) (h/macro? ctx head))
(analyze ctx (h/expand-1 ctx form) locals)
:else
@ -136,6 +146,8 @@
(h/map? form) (ir/map-node (mapv (fn [p] [(analyze ctx (first p) locals)
(analyze ctx (second p) locals)])
(h/map-pairs form)))
(h/set? form) (ir/set-node (mapv #(analyze ctx % locals) (h/set-items form)))
(h/set? form) (uncompilable "set literal")
(h/list? form) (analyze-list ctx form locals)
:else (ir/const form))))
;; Anything else (tagged literals like #"regex"/#inst, unknown shapes) is
;; host-specific or not a value the back end can embed — interpret it.
:else (uncompilable "unsupported form"))))

View file

@ -136,6 +136,12 @@
((var-get av) ctx form))
(defn compile-and-eval
"Self-hosted compile path: analyze (portable Clojure) -> IR -> Janet -> 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)."
[ctx form]
(eval (emit-ir ctx (analyze-form ctx form)) (comp/ctx-janet-env ctx)))
(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)))

View file

@ -51,6 +51,29 @@
# Compile-time environment
# ---------------------------------------------------------------------------
# Names the analyzer must NOT treat as a function call: interpreter special forms
# plus definitional/host macros the compiler doesn't lower. The analyzer handles
# a subset (quote/if/do/def/fn*/let*/loop*/recur/throw/try) and falls back to the
# interpreter for the rest. Kept in sync with evaluator/special-symbol? and
# compiler/uncompilable-heads.
(def- special-names
(let [t @{}]
(each n ["quote" "syntax-quote" "unquote" "unquote-splicing" "do" "if" "def"
"defmacro" "fn*" "let*" "loop*" "recur" "throw" "try" "set!" "var"
"locking" "eval" "instance?" "defmulti" "defmethod" "deftype" "new"
"." "var-get" "var-set" "var?" "alter-var-root" "find-var" "intern"
"alter-meta!" "reset-meta!" "disj" "set?" "satisfies?"
"protocol-dispatch" "register-method" "make-reified" "prefer-method"
"remove-method" "remove-all-methods" "get-method" "methods"
"read-string" "macroexpand-1" "defonce" "ns" "in-ns" "require"
"import" "use" "refer" "defrecord" "defprotocol" "definterface"
"reify" "proxy" "extend-type" "extend-protocol" "extend" "gen-class"
"monitor-enter" "monitor-exit" "binding" "letfn"]
(put t n true))
t))
(defn h-special? [name] (if (get special-names name) true false))
(defn h-current-ns [ctx] (ctx-current-ns ctx))
(defn h-macro? [ctx sym]
@ -92,8 +115,8 @@
"list?" h-list? "vector?" h-vector? "map?" h-map? "set?" h-set? "char?" h-char?
"literal?" h-literal? "elements" h-elements "vector-items" h-vector-items
"map-pairs" h-map-pairs "set-items" h-set-items
"current-ns" h-current-ns "macro?" h-macro? "expand-1" h-expand-1
"resolve-global" h-resolve-global "intern!" h-intern!})
"special?" h-special? "current-ns" h-current-ns "macro?" h-macro?
"expand-1" h-expand-1 "resolve-global" h-resolve-global "intern!" h-intern!})
(defn install! [ctx]
(def ns (ctx-find-ns ctx "jolt.host"))