compiler: IR pass pipeline + constant folding (jolt-2om, nanopass-lite)

jolt.passes is the new portable pipeline stage between the analyzer and the
back end: pure IR -> IR rewrites, total over node :ops (unknown ops pass
through with folded children), loaded with the compiler namespaces and
resolved lazily by analyze-form (JOLT_NO_IR_PASSES=1 disables — the same
escape-hatch pattern as the macro oracle). The shape is flatiron's opt.clj
applied to the jolt IR, which is what jolt-2om asked for.

The first pass is constant folding: a call of a foldable numeric SEED fn
(the later tiers don't exist when the compiler loads) whose args are all
constant numbers becomes a constant, and an if with a constant test becomes
the taken branch (dead-branch elimination — the untaken side never even
resolves). Folding computes with the ACTUAL jolt fns, so results match
runtime semantics by construction; a fold that would throw (mod 5 0) is
left for runtime.

Two walk lessons paid for in debugging: let/loop bindings are
[name init-ir] PAIRS, not maps (assoc'ing :init into a pair corrupts it);
and a throw inside the interpreted pass unwinds past the interpreter's ns
restores, so analyze-form restores the compile ns after the (protected)
pass call — without that, one pass error left current-ns in jolt.passes and
the rest of the tier compile resolved against the wrong namespace (sort-by
landed on the 2-arg JANET builtin).

ir-passes-test pins folds, conservatism (free vars, throwing folds), and
end-to-end eval. Gate exit 0.
This commit is contained in:
Yogthos 2026-06-10 19:29:32 -04:00
parent 40da75cee4
commit 35e8821a92
3 changed files with 151 additions and 2 deletions

View file

@ -385,7 +385,8 @@
# kernel tier must already be loaded (see api/load-core-overlay!).
(defn- build-compiler! [ctx]
(compile-load ctx "jolt.ir")
(compile-load ctx "jolt.analyzer"))
(compile-load ctx "jolt.analyzer")
(compile-load ctx "jolt.passes"))
(defn- ensure-analyzer [ctx]
# Don't build until the kernel tier is loaded (see api/load-core-overlay! and
@ -435,7 +436,20 @@
(def r (protect ((var-get av) ctx form)))
(put (ctx :env) :compile-ns nil)
(ctx-set-current-ns ctx saved-ns)
(if (r 0) (r 1) (error (r 1))))
(unless (r 0) (error (r 1)))
# IR passes (jolt.passes/run-passes — nanopass-lite, jolt-2om): pure IR->IR
# rewrites (constant folding, ...) between the analyzer and the back end.
# Resolved lazily; absent during the pre-passes bootstrap window.
(def pv (unless (= "1" (os/getenv "JOLT_NO_IR_PASSES"))
(ns-find (ctx-find-ns ctx "jolt.passes") "run-passes")))
(if pv
(let [pr (protect ((var-get pv) (r 1)))]
# the pass runs interpreted; a throw inside it unwinds past the
# interpreter's ns restores — put the compile ns back either way, or
# the REST of this compilation resolves in jolt.passes
(ctx-set-current-ns ctx saved-ns)
(if (pr 0) (pr 1) (r 1)))
(r 1)))
# The analyzer's deliberate punt signal — (uncompilable why) throws the string
# "jolt/uncompilable: <why>". Anything else escaping the compile step is an