jolt/jolt-core/jolt/passes/fold.clj
Yogthos 8789777323 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).
2026-06-15 04:57:42 -04:00

73 lines
3.2 KiB
Clojure

(ns jolt.passes.fold
"Constant folding (always-on IR pass) plus the shared const-shape predicate.
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."
(: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:
;; numbers only, the op table only names pure numeric fns, and any throw
;; during folding (e.g. (mod x 0)) leaves the node alone for runtime.
(def ^:private foldable
;; SEED fns only: this ns loads with the compiler, BEFORE the later core
;; tiers — a name from 20-coll (min/max/abs) wouldn't resolve yet.
{"+" + "-" - "*" * "/" /
"<" < ">" > "<=" <= ">=" >= "=" =
"inc" inc "dec" dec
"mod" mod "rem" rem "quot" quot
;; the __bit-* seams: the PUBLIC bit fns are 20-coll variadic shells now,
;; which don't exist yet when this ns loads. Folding stays 2-arg (a 3+-arg
;; constant call throws arity inside the fold and is left for runtime).
"bit-and" __bit-and "bit-or" __bit-or "bit-xor" __bit-xor})
(defn- const? [n] (= :const (get n :op)))
(defn- const-num? [n] (and (const? n) (number? (get n :val))))
(defn- fold-fn [fnode]
(let [op (get fnode :op)]
(when (or (and (= op :var) (= "clojure.core" (get fnode :ns)))
(= op :host))
(get foldable (get fnode :name)))))
(defn const-fold
"Bottom-up constant folding: a call of a foldable numeric fn whose args are
all constant numbers becomes a constant; an if with a constant test becomes
the taken branch."
[node]
(let [op (get node :op)]
(cond
(= op :invoke)
;; 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 n))
(= op :if)
(let [t (const-fold (get node :test))]
(if (const? t)
;; jolt truthiness = Clojure's: nil/false take else
(if (or (nil? (get t :val)) (= false (get t :val)))
(const-fold (get node :else))
(const-fold (get node :then)))
(assoc node
:test t
:then (const-fold (get node :then))
:else (const-fold (get node :else)))))
;; 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
;; (jolt.passes.types), which both reason about const-keyed maps.
(defn scalar-const? [n]
(and (= :const (get n :op))
(let [v (get n :val)] (or (keyword? v) (string? v) (number? v) (boolean? v)))))