Refactor phase 3a: one map-ir-children combinator for the IR rewrite walks (jolt-26dm)

Six bottom-up IR rewrites (const-fold, inline-node, subst, flatten-lets,
subst-lookup, scalar-replace) each hand-listed every op's child positions —
~250 lines of identical "recurse children, rebuild" arms that had to be kept in
sync whenever an op was added. Extract one map-ir-children into ir.clj that
knows each op's child layout; each walk keeps only its genuine specials
(const-fold's invoke/if, inline-node's invoke, subst's local/let alpha-rename,
scalar-replace's invoke/let folds) and delegates the rest.

The combinator is total over the op set, so the walks are now total too: a
couple soundly gain coverage they previously skipped (const-fold now folds
inside :try; subst-lookup now recurses :def inits, which fixes a latent dangling
ref where a dropped const-key-map binding was referenced inside a def). These
are sound — all six are result-preserving optimizations — and 3-mode conformance
+ fixpoint confirm identical program behavior.

map-ir-children is shape-preserving for :try (recurses :catch-body/:finally only
when present, never assoc's nil) so it can't turn a struct node into a phm.
Written with cond/get only, matching the passes' tier, so no new load-order dep.

Predicates (body-closed?/pure?/local-escapes?), the type-threading infer, and the
Janet backend emit stay as-is: their conservative :else defaults / [type node]
threading / host language don't fit a node-rebuilding combinator.

Adds ir-passes-test coverage for folding reaching fn/loop/try bodies. Full gate
green (conformance x3, suite >=4695/88, fixpoint stage1==2==3, inline-sra + devirt).
This commit is contained in:
Yogthos 2026-06-15 04:57:42 -04:00
parent b54bd6c856
commit 8789777323
4 changed files with 101 additions and 196 deletions

View file

@ -3,7 +3,8 @@
Bottom-up numeric folding + dead-branch removal, total over node :ops (unknown
ops pass through with folded children). Portable Clojure: kernel-tier fns +
seed primitives only it loads with the compiler namespaces, before the later
core tiers.")
core tiers."
(:require [jolt.ir :refer [map-ir-children]]))
;; Folding computes with THE ACTUAL jolt fns, so a folded result matches what
;; the unfolded code would produce at runtime by construction. Conservative:
@ -38,14 +39,15 @@
(let [op (get node :op)]
(cond
(= op :invoke)
(let [f (const-fold (get node :fn))
args (mapv const-fold (get node :args))
ff (fold-fn f)
;; fold children first, then this call if the fn is foldable over consts
(let [n (map-ir-children const-fold node)
ff (fold-fn (get n :fn))
args (get n :args)
folded (when (and ff (pos? (count args)) (every? const-num? args))
(try
{:op :const :val (apply ff (mapv (fn [a] (get a :val)) args))}
(catch Exception e nil)))]
(or folded (assoc node :fn f :args args)))
(or folded n))
(= op :if)
(let [t (const-fold (get node :test))]
@ -59,41 +61,9 @@
:then (const-fold (get node :then))
:else (const-fold (get node :else)))))
(= op :do)
(assoc node
:statements (mapv const-fold (get node :statements))
:ret (const-fold (get node :ret)))
;; let/loop bindings are [name-string init-ir] PAIRS (see
;; analyzer/analyze-bindings), not maps.
(= op :let)
(assoc node
:bindings (mapv (fn [b] [(nth b 0) (const-fold (nth b 1))])
(get node :bindings))
:body (const-fold (get node :body)))
(= op :loop)
(assoc node
:bindings (mapv (fn [b] [(nth b 0) (const-fold (nth b 1))])
(get node :bindings))
:body (const-fold (get node :body)))
(= op :recur)
(assoc node :args (mapv const-fold (get node :args)))
(= op :fn)
(assoc node
:arities (mapv (fn [a] (assoc a :body (const-fold (get a :body))))
(get node :arities)))
(= op :def) (assoc node :init (const-fold (get node :init)))
(= op :throw) (assoc node :expr (const-fold (get node :expr)))
(= op :vector) (assoc node :items (mapv const-fold (get node :items)))
(= op :set) (assoc node :items (mapv const-fold (get node :items)))
(= op :map) (assoc node :pairs (mapv (fn [pr] (mapv const-fold pr)) (get node :pairs)))
;; leaves and anything this pass doesn't know: unchanged
:else node)))
;; every other op: fold each child (let/loop bindings are [name init]
;; pairs, handled by the combinator)
:else (map-ir-children const-fold node))))
;; A const node whose value is a scalar literal (kw/str/num/bool). Shared by the
;; scalar-replace pass (jolt.passes.inline) and the collection-type inference