self-host: analyzer compiles to fast bytecode (fib(30) 0.52s)

The self-hosted compiler now genuinely compiles to native Janet bytecode rather
than falling back to the interpreter:
- bootstrap fixes that let it compile the analyzer: (def x) with no init, and an
  empty (do) (declare expands to one) no longer error;
- analyzer reordered so only the mutually-recursive  is forward declared
  (a deeply-nested forward ref to analyze-fn/analyze-try miscompiled to nil);
- back end emits native Janet ops for hot primitives (+,-,*,<,>,<=,>=,inc,dec)
  on clojure.core refs, like the bootstrap's core-renames.

Result: self-hosted fib(30) runs in 0.52s (was ~50s interpreted). Full
clojure-test-suite via JOLT_SELFHOST runs compiled at 3869 pass (some compiled-
vs-interpreted divergences remain to chase toward full parity — jolt-4xc).
Default suite green; conformance 218/218; jank-conformance up to 135.
This commit is contained in:
Yogthos 2026-06-06 10:25:05 -04:00
parent 06d5f51d4a
commit 3afd1c6142
3 changed files with 104 additions and 69 deletions

View file

@ -107,12 +107,37 @@
(defn- direct-call? [fnode]
(case (fnode :op) :var true :local true :fn true :host true false))
# Hot primitives emitted as native Janet ops (host-specific optimization): a
# call to clojure.core/+ etc. becomes (+ …) rather than a var deref + variadic
# core fn. Matches numeric semantics; relaxes the non-number checks (a documented
# perf-mode divergence, same as the bootstrap's core-renames).
(def- native-ops
{"+" '+ "-" '- "*" '* "<" '< ">" '> "<=" '<= ">=" '>= "inc" '++ "dec" '--})
(defn- native-op
"If fnode is a clojure.core ref (or host ref) to a native-op primitive, return
the Janet op symbol, else nil. inc/dec are unary so only at arity 1."
[fnode nargs]
(def nm (case (fnode :op)
:var (when (= "clojure.core" (fnode :ns)) (fnode :name))
:host (fnode :name)
nil))
(def op (and nm (get native-ops nm)))
(cond
(nil? op) nil
(and (or (= op '++) (= op '--)) (not= nargs 1)) nil
op))
(defn- emit-invoke [ctx node]
(def f (emit ctx (node :fn)))
(def args (map |(emit ctx $) (vview (node :args))))
(if (direct-call? (node :fn))
(tuple f ;args)
(tuple jolt-call f ;args)))
(def nop (native-op (node :fn) (length args)))
(cond
nop (case nop
'++ ['+ (in args 0) 1]
'-- ['- (in args 0) 1]
(tuple nop ;args))
(direct-call? (node :fn)) (tuple (emit ctx (node :fn)) ;args)
(tuple jolt-call (emit ctx (node :fn)) ;args)))
(defn- emit-vector [ctx node]
(def items (map |(emit ctx $) (vview (node :items))))
@ -170,8 +195,11 @@
(set s (in parsed 1))
(def f (in parsed 0))
(when (not (nil? f))
(def c (protect (comp/compile-ast f ctx)))
(if (c 0) (comp/eval-compiled (c 1) ctx) (eval-form ctx @{} f))))
# Guard BOTH compile and the Janet-compile-of-emitted step: a form whose
# emitted Janet is invalid (e.g. a bad splice) falls back to interpreted
# definition rather than killing the whole load.
(def r (protect (comp/eval-compiled (comp/compile-ast f ctx) ctx)))
(unless (r 0) (eval-form ctx @{} f))))
(ctx-set-current-ns ctx saved)))
(defn- ensure-analyzer [ctx]

View file

@ -421,9 +421,11 @@
"do" (let [all-statements (array/slice form 1)
n (length all-statements)
analyzed (map |(analyze-form $ bindings ctx) all-statements)]
{:op :do
:statements (array/slice analyzed 0 (- n 1))
:ret (in analyzed (- n 1))})
(if (= n 0)
{:op :const :val nil} # (do) -> nil
{:op :do
:statements (array/slice analyzed 0 (- n 1))
:ret (in analyzed (- n 1))}))
"if" {:op :if
:test (analyze-form (in form 1) bindings ctx)
:then (analyze-form (in form 2) bindings ctx)
@ -434,9 +436,11 @@
nm (if (struct? name-sym) (name-sym :name) (string name-sym))
# Create/find the var cell first so a recursive init body
# self-references the same cell.
cell (when ctx (ns-intern (ctx-find-ns ctx (ctx-current-ns ctx)) nm))]
cell (when ctx (ns-intern (ctx-find-ns ctx (ctx-current-ns ctx)) nm))
# (def x) with no init (declare) -> nil.
init-form (if (> (length form) 2) (in form 2) nil)]
{:op :def :name name-sym :var cell
:init (analyze-form (in form 2) bindings ctx)})
:init (analyze-form init-form bindings ctx)})
"fn*" (analyze-fn form bindings ctx)
"let*" (let [bind-vec (in form 1)
body-exprs (tuple/slice form 2)