Finish the projection migration for reaching definitions by replacing the accumulator-style runOfTrace*From definitions and their hand-rolled re-association lemmas with a single analysis-agnostic projection: Trace.steps / Traceₗ.steps, the chronological List of executed (index, statement) pairs. Its four simp lemmas are one-line inductions, with all re-association falling out of mathlib's List.append_assoc and List.reverse_append. Run is now an abbrev for List (State × BasicStmt) (latest-first, so LastAssign keeps its first-match structure) and runOfTrace is just steps.reverse. Also hoist the generic reaches_final_post into Forward.lean, letting analyze_correct' be stated directly about S.Post (prog.trace hrun). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
267 lines
13 KiB
Lean4
267 lines
13 KiB
Lean4
import Spa.Language.Semantics
|
||
import Spa.Language.Graphs
|
||
import Spa.Language.Program
|
||
|
||
/-!
|
||
|
||
# Program Traces
|
||
|
||
This module defines program traces tied to Control Flow Graphs, or CFGs
|
||
(see `Spa.GGraph` and `Spa.Graph`). These traces boil town to sequences of
|
||
basic-block executions (really, `Spa.BasicStmt` executions), each of which must
|
||
have an actual basic block in the graph _and_ be connected to the previous
|
||
basic block by an edge. In this way, traces encode executions admitted
|
||
by the CFG.
|
||
|
||
While the regular `Trace` is just _any_ path through the graph, an
|
||
`EndToEndTrace` is a path from the entry node to the exit node, denoting
|
||
full program execution.
|
||
|
||
Properties about graphs and language semantics (especially,
|
||
the fact that the graph contains the proper basic block and edges
|
||
to represent any program execution according to the
|
||
language's big-step semantics `EvalStmt`) is found
|
||
in `Spa/Language/Properties.lean`.
|
||
|
||
-/
|
||
|
||
namespace Spa
|
||
|
||
/-- A partial trace through a graph `g`, starting right before
|
||
the execution of the basic block at the first index, and
|
||
ending right after the execution of the basic block at the last index. -/
|
||
inductive Trace (g : Graph) : g.Index → g.Index → Env → Env → Type
|
||
| single {ρ₁ ρ₂ : Env} {idx : g.Index} :
|
||
EvalBasicStmtOpt ρ₁ (g.nodes idx) ρ₂ → Trace g idx idx ρ₁ ρ₂
|
||
| edge {ρ₁ ρ₂ ρ₃ : Env} {idx₁ idx₂ idx₃ : g.Index} :
|
||
EvalBasicStmtOpt ρ₁ (g.nodes idx₁) ρ₂ → (idx₁, idx₂) ∈ g.edges →
|
||
Trace g idx₂ idx₃ ρ₂ ρ₃ → Trace g idx₁ idx₃ ρ₁ ρ₃
|
||
|
||
/-!
|
||
|
||
## Open Traces
|
||
|
||
A normal `Trace` starts right before one state, and ends right after another.
|
||
This is convenient for inductively proving correctness / sufficience, but
|
||
awkward because 1) no empty traces exist and 2) concatenation requires an extra
|
||
edge.
|
||
|
||
However, when attempting an "empty" trace, two types are equally possible:
|
||
traces that end _right before_ executing a state (`Traceₗ`) and
|
||
traces that begin _right after_ executing a state (`Traceᵣ`). They
|
||
are symmetric and can be concatenated with full traces on the left
|
||
and right, respectively. -/
|
||
|
||
/-- Left-open trace, representing execution that ends right before `idx₂`. -/
|
||
inductive Traceₗ (g : Graph) : g.Index → g.Index → Env → Env → Type where
|
||
| nil {idx : g.Index} {ρ : Env} : Traceₗ g idx idx ρ ρ
|
||
| cons {idx₁ idx₂ idx₃ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
EvalBasicStmtOpt ρ₁ (g.nodes idx₁) ρ₂ →
|
||
(idx₁, idx₂) ∈ g.edges →
|
||
Traceₗ g idx₂ idx₃ ρ₂ ρ₃ → Traceₗ g idx₁ idx₃ ρ₁ ρ₃
|
||
|
||
def Traceₗ.single (g : Graph) (idx : g.Index) (ρ : Env) : Traceₗ g idx idx ρ ρ := .nil
|
||
|
||
/-- Right-open trace, representing execution that starts right after `idx₁`. -/
|
||
inductive Traceᵣ (g : Graph) : g.Index → g.Index → Env → Env → Type where
|
||
| nil {idx : g.Index} {ρ : Env} : Traceᵣ g idx idx ρ ρ
|
||
| cons {idx₁ idx₂ idx₃ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
Traceᵣ g idx₁ idx₂ ρ₁ ρ₂ →
|
||
(idx₂, idx₃) ∈ g.edges →
|
||
EvalBasicStmtOpt ρ₂ (g.nodes idx₃) ρ₃ → Traceᵣ g idx₁ idx₃ ρ₁ ρ₃
|
||
|
||
def Traceᵣ.single (g : Graph) (idx : g.Index) (ρ : Env) : Traceᵣ g idx idx ρ ρ := .nil
|
||
|
||
/-- Sequence two traces together. Since the endpoint of the first trace
|
||
is _after_ its last basic block's execution, and the beginning of
|
||
the next trace is _before_ its first basic block's execution,
|
||
there must be an edge to connect the two. -/
|
||
def Trace.concat {g : Graph} {idx₁ idx₂ idx₃ idx₄ : g.Index}
|
||
{ρ₁ ρ₂ ρ₃ : Env} (tr₁ : Trace g idx₁ idx₂ ρ₁ ρ₂)
|
||
(he : (idx₂, idx₃) ∈ g.edges) (tr₂ : Trace g idx₃ idx₄ ρ₂ ρ₃) :
|
||
Trace g idx₁ idx₄ ρ₁ ρ₃ :=
|
||
match tr₁ with
|
||
| single hbs => edge hbs he tr₂
|
||
| edge hbs he' tr₁' => edge hbs he' (tr₁'.concat he tr₂)
|
||
|
||
scoped notation:65 tr₁:66 " ++< " he " >++ " tr₂:65 => Trace.concat tr₁ he tr₂
|
||
|
||
def Trace.addEdge {g : Graph} {idx₁ idx₂ idx₃ : g.Index} {ρ₁ ρ₂ : Env} :
|
||
Trace g idx₁ idx₂ ρ₁ ρ₂ →
|
||
(idx₂, idx₃) ∈ g.edges →
|
||
Traceₗ g idx₁ idx₃ ρ₁ ρ₂
|
||
| .single hnode, hedge => .cons hnode hedge .nil
|
||
| .edge hnode hedge' rest, hedge => .cons hnode hedge' (rest.addEdge hedge)
|
||
|
||
@[aesop simp]
|
||
def Traceₗ.append {g : Graph} {idx₁ idx₂ idx₃ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
Traceₗ g idx₁ idx₂ ρ₁ ρ₂ → Traceₗ g idx₂ idx₃ ρ₂ ρ₃ →
|
||
Traceₗ g idx₁ idx₃ ρ₁ ρ₃
|
||
| .nil, rhs => rhs
|
||
| .cons hnode hedge rest, rhs => .cons hnode hedge (rest.append rhs)
|
||
|
||
@[simp] def traceₗ_append_nil {g : Graph} {idx₁ idx₂ : g.Index} {ρ₁ ρ₂ : Env}
|
||
{trₗ : Traceₗ g idx₁ idx₂ ρ₁ ρ₂} : trₗ.append Traceₗ.nil = trₗ := by
|
||
induction trₗ <;> aesop
|
||
|
||
def Traceₗ.appendTrace {g : Graph} {idx₁ idx₂ idx₃ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
Traceₗ g idx₁ idx₂ ρ₁ ρ₂ → Trace g idx₂ idx₃ ρ₂ ρ₃ →
|
||
Trace g idx₁ idx₃ ρ₁ ρ₃
|
||
| .nil, rhs => rhs
|
||
| .cons hnode hedge rest, rhs => .edge hnode hedge (rest.appendTrace rhs)
|
||
|
||
def Traceₗ.appendStep {g : Graph} {idx₁ idx₂ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
Traceₗ g idx₁ idx₂ ρ₁ ρ₂ → EvalBasicStmtOpt ρ₂ (g.nodes idx₂) ρ₃ →
|
||
Trace g idx₁ idx₂ ρ₁ ρ₃ := fun trₗ hbs => trₗ.appendTrace (Trace.single hbs)
|
||
|
||
def Trace.appendRight {g : Graph} {idx₁ idx₂ idx₃ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
Trace g idx₁ idx₂ ρ₁ ρ₂ → Traceᵣ g idx₂ idx₃ ρ₂ ρ₃ →
|
||
Trace g idx₁ idx₃ ρ₁ ρ₃
|
||
| lhs, .nil => lhs
|
||
| lhs, .cons rest hedge hnode => Trace.concat (lhs.appendRight rest) hedge (.single hnode)
|
||
|
||
instance instHAppendTraceLTraceL {g : Graph} {idx₁ idx₂ idx₃ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
HAppend (Traceₗ g idx₁ idx₂ ρ₁ ρ₂) (Traceₗ g idx₂ idx₃ ρ₂ ρ₃) (Traceₗ g idx₁ idx₃ ρ₁ ρ₃) where
|
||
hAppend := Traceₗ.append
|
||
|
||
instance instHAppendTraceLTrace {g : Graph} {idx₁ idx₂ idx₃ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
HAppend (Traceₗ g idx₁ idx₂ ρ₁ ρ₂) (Trace g idx₂ idx₃ ρ₂ ρ₃) (Trace g idx₁ idx₃ ρ₁ ρ₃) where
|
||
hAppend := Traceₗ.appendTrace
|
||
|
||
instance instHAppendTraceLStep {g : Graph} {idx₁ idx₂ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
HAppend (Traceₗ g idx₁ idx₂ ρ₁ ρ₂) (EvalBasicStmtOpt ρ₂ (g.nodes idx₂) ρ₃) (Trace g idx₁ idx₂ ρ₁ ρ₃) where
|
||
hAppend := Traceₗ.appendStep
|
||
|
||
instance instHAppendTraceTraceR {g : Graph} {idx₁ idx₂ idx₃ : g.Index} {ρ₁ ρ₂ ρ₃ : Env} :
|
||
HAppend (Trace g idx₁ idx₂ ρ₁ ρ₂) (Traceᵣ g idx₂ idx₃ ρ₂ ρ₃) (Trace g idx₁ idx₃ ρ₁ ρ₃) where
|
||
hAppend := Trace.appendRight
|
||
|
||
/-!
|
||
|
||
## Trace Steps
|
||
|
||
Analyses that care about *which statements executed* (e.g. reaching
|
||
definitions) need to project a trace down to its list of executed statements.
|
||
Defining that projection here, once, as a chronological mathlib `List` means
|
||
all the re-association facts about concatenating traces come for free from
|
||
`List.append_assoc` and friends, instead of being re-proven per analysis. -/
|
||
|
||
/-- The (index, statement) pairs executed by a single optional-statement step:
|
||
none if the node is empty, and the node's statement otherwise. -/
|
||
def EvalBasicStmtOpt.steps {α : Type*} (idx : α) {ρ₁ ρ₂ : Env} {obs : Option BasicStmt} :
|
||
EvalBasicStmtOpt ρ₁ obs ρ₂ → List (α × BasicStmt)
|
||
| .none => []
|
||
| .some (bs := bs) _ => [(idx, bs)]
|
||
|
||
/-- The statements executed by a left-open trace, in chronological order. -/
|
||
def Traceₗ.steps {g : Graph} {idx₁ idx₂ : g.Index} {ρ₁ ρ₂ : Env} :
|
||
Traceₗ g idx₁ idx₂ ρ₁ ρ₂ → List (g.Index × BasicStmt)
|
||
| .nil => []
|
||
| .cons (idx₁ := idx) hnode _ rest => hnode.steps idx ++ rest.steps
|
||
|
||
/-- The statements executed by a trace, in chronological order. -/
|
||
def Trace.steps {g : Graph} {idx₁ idx₂ : g.Index} {ρ₁ ρ₂ : Env} :
|
||
Trace g idx₁ idx₂ ρ₁ ρ₂ → List (g.Index × BasicStmt)
|
||
| .single (idx := idx) hnode => hnode.steps idx
|
||
| .edge (idx₁ := idx) hnode _ rest => hnode.steps idx ++ rest.steps
|
||
|
||
@[simp] lemma Traceₗ.steps_append {g : Graph} {idx₁ idx₂ idx₃ : g.Index}
|
||
{ρ₁ ρ₂ ρ₃ : Env} (tr₁ : Traceₗ g idx₁ idx₂ ρ₁ ρ₂)
|
||
(tr₂ : Traceₗ g idx₂ idx₃ ρ₂ ρ₃) :
|
||
(tr₁ ++ tr₂).steps = tr₁.steps ++ tr₂.steps := by
|
||
show (tr₁.append tr₂).steps = _
|
||
induction tr₁ <;> simp [Traceₗ.append, Traceₗ.steps, *]
|
||
|
||
@[simp] lemma Traceₗ.steps_appendTrace {g : Graph} {idx₁ idx₂ idx₃ : g.Index}
|
||
{ρ₁ ρ₂ ρ₃ : Env} (tr₁ : Traceₗ g idx₁ idx₂ ρ₁ ρ₂)
|
||
(tr₂ : Trace g idx₂ idx₃ ρ₂ ρ₃) :
|
||
(tr₁ ++ tr₂).steps = tr₁.steps ++ tr₂.steps := by
|
||
show (tr₁.appendTrace tr₂).steps = _
|
||
induction tr₁ <;> simp [Traceₗ.appendTrace, Traceₗ.steps, Trace.steps, *]
|
||
|
||
@[simp] lemma Traceₗ.steps_appendStep {g : Graph} {idx₁ idx₂ : g.Index}
|
||
{ρ₁ ρ₂ ρ₃ : Env} (tr : Traceₗ g idx₁ idx₂ ρ₁ ρ₂)
|
||
(hbs : EvalBasicStmtOpt ρ₂ (g.nodes idx₂) ρ₃) :
|
||
(tr ++ hbs).steps = tr.steps ++ hbs.steps idx₂ :=
|
||
Traceₗ.steps_appendTrace tr (Trace.single hbs)
|
||
|
||
@[simp] lemma Trace.steps_addEdge {g : Graph} {idx₁ idx₂ idx₃ : g.Index}
|
||
{ρ₁ ρ₂ : Env} (tr : Trace g idx₁ idx₂ ρ₁ ρ₂)
|
||
(hedge : (idx₂, idx₃) ∈ g.edges) :
|
||
(tr.addEdge hedge).steps = tr.steps := by
|
||
induction tr <;> simp [Trace.addEdge, Trace.steps, Traceₗ.steps, *]
|
||
|
||
@[simp] lemma Traceₗ.append_addEdge {g : Graph}
|
||
{idx₁ idx₂ idx₃ idx₄ : g.Index} {ρ₁ ρ₂ ρ₃ ρ₄ : Env}
|
||
(trₗ : Traceₗ g idx₁ idx₂ ρ₁ ρ₂)
|
||
(hnode : EvalBasicStmtOpt ρ₂ (g.nodes idx₂) ρ₃)
|
||
(hedge : (idx₂, idx₃) ∈ g.edges)
|
||
(rest : Traceₗ g idx₃ idx₄ ρ₃ ρ₄) :
|
||
trₗ.append (Traceₗ.cons hnode hedge rest) =
|
||
(Trace.addEdge (trₗ.appendStep hnode) hedge).append rest := by
|
||
induction trₗ <;> simp [Traceₗ.append, Traceₗ.appendStep, Traceₗ.appendTrace, Trace.addEdge, *]
|
||
|
||
@[simp] lemma Traceₗ.appendTrace_addEdge {g : Graph}
|
||
{idx₁ idx₂ idx₃ idx₄ : g.Index} {ρ₁ ρ₂ ρ₃ ρ₄ : Env}
|
||
(trₗ : Traceₗ g idx₁ idx₂ ρ₁ ρ₂)
|
||
(hnode : EvalBasicStmtOpt ρ₂ (g.nodes idx₂) ρ₃)
|
||
(hedge : (idx₂, idx₃) ∈ g.edges)
|
||
(rest : Trace g idx₃ idx₄ ρ₃ ρ₄) :
|
||
trₗ.appendTrace (Trace.edge hnode hedge rest) =
|
||
(Trace.addEdge (trₗ.appendStep hnode) hedge).appendTrace rest := by
|
||
induction trₗ <;> simp [Traceₗ.appendTrace, Traceₗ.appendStep, Trace.addEdge, *]
|
||
|
||
/-- A beginning-to-end trace corresponding to the CFG `g`. -/
|
||
inductive EndToEndTrace (g : Graph) (ρ₁ ρ₂ : Env) : Type
|
||
| intro (idx₁ : g.Index) (idx₁_mem : idx₁ ∈ g.inputs)
|
||
(idx₂ : g.Index) (idx₂_mem : idx₂ ∈ g.outputs)
|
||
(trace : Trace g idx₁ idx₂ ρ₁ ρ₂) : EndToEndTrace g ρ₁ ρ₂
|
||
|
||
inductive Reaches {prog : Program} : {s₁ s₂ : prog.State} → {ρ₁ ρ₂ : Env} →
|
||
Trace prog.cfg s₁ s₂ ρ₁ ρ₂ →
|
||
(s : prog.State) → (ρin ρout : Env) → Type
|
||
| single_here {s₁ : prog.State} {ρ₁ ρ₂ : Env}
|
||
(hnode : EvalBasicStmtOpt ρ₁ (prog.code s₁) ρ₂) :
|
||
Reaches (.single hnode) s₁ ρ₁ ρ₂
|
||
| edge_here {s₁ s₂ s₃ : prog.State} {ρ₁ ρ₂ ρ₃ : Env}
|
||
(hnode : EvalBasicStmtOpt ρ₁ (prog.code s₁) ρ₂)
|
||
(hedge : (s₁, s₂) ∈ prog.cfg.edges) (rest : Trace prog.cfg s₂ s₃ ρ₂ ρ₃) :
|
||
Reaches (.edge hnode hedge rest) s₁ ρ₁ ρ₂
|
||
| edge_there {s₁ s₂ s₃ : prog.State} {ρ₁ ρ₂ ρ₃ : Env}
|
||
(hnode : EvalBasicStmtOpt ρ₁ (prog.code s₁) ρ₂)
|
||
(hedge : (s₁, s₂) ∈ prog.cfg.edges) (rest : Trace prog.cfg s₂ s₃ ρ₂ ρ₃)
|
||
{s : prog.State} {ρin ρout : Env} :
|
||
Reaches rest s ρin ρout →
|
||
Reaches (.edge hnode hedge rest) s ρin ρout
|
||
|
||
def Reaches.pre {prog : Program} {s₁ s₂ s: prog.State}
|
||
{ρ₁ ρ₂ ρin ρout : Env} {tr : Trace prog.cfg s₁ s₂ ρ₁ ρ₂} :
|
||
(r : Reaches tr s ρin ρout) → Traceₗ prog.cfg s₁ s ρ₁ ρin
|
||
| .single_here _ => .nil
|
||
| .edge_here _ _ _ => .nil
|
||
| .edge_there hnode hedge _ r => .cons hnode hedge r.pre
|
||
|
||
def Reaches.post {prog : Program} {s₁ s₂ s: prog.State}
|
||
{ρ₁ ρ₂ ρin ρout : Env} {tr : Trace prog.cfg s₁ s₂ ρ₁ ρ₂} :
|
||
(r : Reaches tr s ρin ρout) → Trace prog.cfg s₁ s ρ₁ ρout
|
||
| .single_here hnode => .single hnode
|
||
| .edge_here hnode _ _ => .single hnode
|
||
| .edge_there hnode hedge _ r => .edge hnode hedge r.post
|
||
|
||
def Reaches.first {prog : Program} {s₁ s₂ s: prog.State}
|
||
{ρ₁ ρ₂ ρin ρout : Env} {tr : Trace prog.cfg s₁ s₂ ρ₁ ρ₂} :
|
||
(r : Reaches tr s ρin ρout) → Σ ρ₁', Reaches tr s₁ ρ₁ ρ₁'
|
||
| .single_here hnode => ⟨_, .single_here hnode⟩
|
||
| .edge_here hnode hedge hrest => ⟨_, .edge_here hnode hedge hrest⟩
|
||
| .edge_there hnode hedge hrest tmp' => ⟨_, .edge_here hnode hedge hrest⟩
|
||
|
||
def Reaches.step {prog : Program} {s₁ s₂ s: prog.State}
|
||
{ρ₁ ρ₂ ρin ρout : Env} {tr : Trace prog.cfg s₁ s₂ ρ₁ ρ₂} :
|
||
(r : Reaches tr s ρin ρout) → EvalBasicStmtOpt ρin (prog.code s) ρout
|
||
| .single_here hnode => hnode
|
||
| .edge_here hnode hedge hrest => hnode
|
||
| .edge_there hnode hedge hrest tmp' => tmp'.step
|
||
|
||
|
||
end Spa
|