Implement guarded LICM transformation

This commit is contained in:
2026-10-06 20:22:35 -05:00
parent ac99bc047a
commit 13008121d5

View File

@@ -1,12 +1,13 @@
import Spa.Analysis.Reaching
import Spa.Language.Equivalence
/-!
# Finding loop-invariant assignments (LICM groundwork)
# Loop-invariant code motion
This wires the **reaching-definitions** analysis (`Spa/Analysis/Reaching.lean`)
to the AST to *find* — not yet move — assignments inside a `while` loop whose
right-hand side depends only on definitions made *outside* the loop. These are
the candidates a later LICM pass could hoist.
to the AST to find assignments inside a `while` loop whose right-hand side
depends only on definitions made outside the loop. `licmCandidates` reports
these assignments; `hoistProgram` moves eligible leading assignments.
The traversal recurses over the plain `Stmt`, threading a `GGraph.Embed` of the
current subtree's CFG into the program's (`Program.rootEmbed`, then one
@@ -24,9 +25,12 @@ current subtree's CFG into the program's (`Program.rootEmbed`, then one
comparisons.
If every reaching definition of every RHS variable lies outside the loop, the
assignment is reported as loop-invariant. This is the first-order check ("all
reaching definitions outside the loop"); transitive/iterated invariance and the
actual hoisting are out of scope here.
assignment is reported as loop-invariant. Hoisting additionally requires the
assignment to lead the body, its destination to be absent from the guard, and
no reassignment of that destination in the remaining body. The hoist is guarded
by the original condition, preserving zero-iteration behavior.
Transitive invariance and motion of non-leading assignments are not implemented.
-/
namespace Spa
@@ -102,6 +106,50 @@ def licmCandidates (prog : Program) : List (prog.State × prog.State) :=
(collectCandidates prog none prog.rootStmt prog.rootEmbed).filterMap (fun c =>
if isInvariant prog c then some (c.encl.loopState, c.assignState) else none)
/-- Candidate for the leading assignment of a loop body. -/
def headCandidate (prog : Program) (cond : Expr) (x : String) (rhs : Expr) (tail : Stmt)
(e : Embed (Stmt.whileLoop cond (.andThen (.basic (.assign x rhs)) tail)).cfg prog.cfg) :
Candidate prog :=
let body := Stmt.andThen (.basic (.assign x rhs)) tail
let be := (Embed.loop body.cfg).trans e
let ae := (Embed.sequenceLeft (Stmt.basic (.assign x rhs)).cfg tail.cfg).trans be
{ encl := { loopState := e.f body.cfg.loopIn, bodyOff := be.off, bodySize := body.cfg.size },
assignState := ae.singletonIndex, rhsVars := rhs.vars.sort (· ≤ ·) }
/-- Guard the hoist so that a zero-iteration loop never evaluates the RHS. -/
def hoistHead (cond : Expr) (x : String) (rhs : Expr) (tail : Stmt) : Stmt :=
.ifElse cond (.andThen (.basic (.assign x rhs)) (.whileLoop cond tail)) (.basic .noop)
/-- Hoist a leading invariant assignment when its destination is neither
reassigned in the remaining body nor read by the guard. -/
def hoistLoop (prog : Program) (cond : Expr) (body : Stmt)
(e : Embed (Stmt.whileLoop cond body).cfg prog.cfg) : Option Stmt :=
match body with
| .andThen (.basic (.assign x rhs)) tail =>
if isInvariant prog (headCandidate prog cond x rhs tail e) &&
decide (x ∉ tail.writes ∧ x ∉ cond.vars) then
some (hoistHead cond x rhs tail)
else none
| _ => none
/-- Apply guarded leading-assignment LICM throughout the source tree. When a
loop is hoisted, keep its remaining body intact; further passes can reanalyze it. -/
def hoistStmt (prog : Program) : (s : Stmt) → Embed s.cfg prog.cfg → Stmt
| .basic bs, _ => .basic bs
| .andThen a b, e =>
.andThen (hoistStmt prog a ((Embed.sequenceLeft a.cfg b.cfg).trans e))
(hoistStmt prog b ((Embed.sequenceRight a.cfg b.cfg).trans e))
| .ifElse cond a b, e =>
.ifElse cond (hoistStmt prog a ((Embed.overlayLeft a.cfg b.cfg).trans e))
(hoistStmt prog b ((Embed.overlayRight a.cfg b.cfg).trans e))
| .whileLoop cond body, e =>
match hoistLoop prog cond body e with
| some moved => moved
| none => .whileLoop cond (hoistStmt prog body ((Embed.loop body.cfg).trans e))
/-- Run reaching definitions on the source program and perform guarded LICM. -/
def hoistProgram (prog : Program) : Stmt := hoistStmt prog prog.rootStmt prog.rootEmbed
/-- A human-readable report of the loop-invariant assignments. -/
def output (prog : Program) : String :=
match licmCandidates prog with