Files
agda-spa/lean/Spa/Transformation/Licm.lean

117 lines
5.1 KiB
Lean4
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Spa.Analysis.Reaching
/-!
# Finding loop-invariant assignments (LICM groundwork)
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.
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
`Embed.trans` per descent). That embedding is what supplies program states:
1. at an assignment, its CFG state is `Embed.singletonIndex` — the subtree's CFG
is a `singleton`, so its sole node is the state, and `nodes_eq` proves it
holds that very statement;
2. read the reaching definitions at the assignment's *entry* (`joinForKey s
result` — the join over predecessors, i.e. before the assignment runs);
3. union the definition sets of the RHS variables;
4. check no definition site lies in the loop body's CFG range. Every embedding is
a constant index shift, so the body occupies the interval
`[off, off + size)` (`GGraph.Embed.mem_range_iff`) and the test is two
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.
-/
namespace Spa
namespace LicmTransformation
open Forward GGraph
/-- The CFG footprint of an enclosing loop: its entry node (for reporting) and
the index interval its body occupies. -/
structure Enclosing (prog : Program) where
/-- The loop's entry node, i.e. `GGraph.loopIn` embedded into the program. -/
loopState : prog.State
/-- Start of the body's index range. -/
bodyOff :
/-- Length of the body's index range. -/
bodySize :
/-- Is this definition site inside the loop body's CFG range? -/
def Enclosing.covers {prog : Program} (l : Enclosing prog) (d : prog.State) : Bool :=
decide (l.bodyOff d.val d.val < l.bodyOff + l.bodySize)
/-- An assignment found inside a loop, paired with the data needed to test its
invariance against that (immediately enclosing) loop. -/
structure Candidate (prog : Program) where
/-- The enclosing loop. -/
encl : Enclosing prog
/-- The assignment's CFG state. -/
assignState : prog.State
/-- The variables read by the assignment's RHS. -/
rhsVars : List String
/-- Collect every assignment together with its *immediately enclosing* loop.
`enc` is `none` outside any loop, in which case assignments are skipped — only
in-loop assignments are candidates. -/
def collectCandidates (prog : Program) (enc : Option (Enclosing prog)) :
(s : Stmt) Embed s.cfg prog.cfg List (Candidate prog)
| .basic bs, e =>
match bs, enc with
| .assign _ ex, some l =>
[{ encl := l, assignState := e.singletonIndex,
rhsVars := ex.vars.sort (· ·) }]
| _, _ => []
| .andThen s₁ s₂, e =>
collectCandidates prog enc s₁ ((Embed.sequenceLeft s₁.cfg s₂.cfg).trans e) ++
collectCandidates prog enc s₂ ((Embed.sequenceRight s₁.cfg s₂.cfg).trans e)
| .ifElse _ s₁ s₂, e =>
collectCandidates prog enc s₁ ((Embed.overlayLeft s₁.cfg s₂.cfg).trans e) ++
collectCandidates prog enc s₂ ((Embed.overlayRight s₁.cfg s₂.cfg).trans e)
| .whileLoop _ body, e =>
let be := (Embed.loop body.cfg).trans e
collectCandidates prog
(some { loopState := e.f body.cfg.loopIn, bodyOff := be.off,
bodySize := body.cfg.size }) body be
/-- Read the definition set assigned to variable `k`, or `⊥` if absent. -/
def lookupDef (prog : Program) (vs : VariableValues (DefSet prog) prog)
(k : String) : DefSet prog :=
if h : FiniteMap.MemKey k vs then (FiniteMap.locate h).1 else
/-- Is the candidate assignment loop-invariant: do all reaching definitions of
its RHS variables lie outside the loop body? -/
def isInvariant (prog : Program) (c : Candidate prog) : Bool :=
let entry := joinForKey c.assignState (result (DefSet prog) prog)
let combined : DefSet prog :=
c.rhsVars.foldl (fun acc k => acc lookupDef prog entry k)
-- `Finset.toList` is noncomputable; the decidable bounded-∀ folds over the
-- underlying multiset and keeps `lake exe` working.
decide ( d combined, c.encl.covers d = false)
/-- The loop-invariant assignments of `prog`, as `(loop, assignment)` state pairs. -/
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)
/-- A human-readable report of the loop-invariant assignments. -/
def output (prog : Program) : String :=
match licmCandidates prog with
| [] => "no loop-invariant assignments found"
| cands =>
"loop-invariant assignments (loop ↦ assignment):\n" ++
String.intercalate "\n"
(cands.map (fun p => s!" loop #{p.1.val}: assignment #{p.2.val}"))
end LicmTransformation
end Spa