Introduce `tr₁ ++< he >++ tr₂` scoped notation for `Trace.concat` (precedence 65, right-associative, mirroring `++`) and use it throughout Properties.lean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
295 lines
14 KiB
Lean4
295 lines
14 KiB
Lean4
import Spa.Language.Traces
|
||
|
||
/-!
|
||
|
||
# Properties of the Object Language, CFGs, and Traces
|
||
|
||
This module encodes some properties of the language, mostly those having to do
|
||
with connecting the computational view (the `Spa.Graph`s, on which static
|
||
analyses are executed) to the semantic view (such as `EvalStmt`, which
|
||
encodes the expected formal behavior of the language). In particular,
|
||
to prove that our computationally-implemented static analyses are correct,
|
||
we need to show that our computational model of their execution (the CFG)
|
||
matches the formal description. Thus, the key result `cfg_sufficient`.
|
||
|
||
Many lemmas and definitions here aim are used to prove that result,
|
||
by allowing inductive proofs on the construction of the CFG:
|
||
the bits where we _build up_ the trace corresponding to each
|
||
proof tree are exactly those when we have two graphs (through
|
||
which traces exist) and we want to combine these graphs, while
|
||
showing also that a combined trace exists as well. -/
|
||
|
||
namespace Spa
|
||
|
||
open Graph
|
||
|
||
lemma Fin.castAdd_ne_natAdd {n m : ℕ} (i : Fin n) (j : Fin m) :
|
||
Fin.castAdd m i ≠ Fin.natAdd n j := by
|
||
intro h
|
||
have := congrArg Fin.val h
|
||
simp only [Fin.coe_castAdd, Fin.coe_natAdd] at this
|
||
omega
|
||
|
||
section Embeddings
|
||
|
||
variable {g₁ g₂ : Graph} {ρ₁ ρ₂ : Env}
|
||
|
||
/-- When two graphs are overlaid, for each trace in the left graph,
|
||
a corresponding trace exists in the combined graph. -/
|
||
noncomputable def Trace.overlay_left {idx₁ idx₂ : g₁.Index}
|
||
(tr : Trace g₁ idx₁ idx₂ ρ₁ ρ₂) :
|
||
Trace (g₁ ∙ g₂) (idx₁.castAdd g₂.size) (idx₂.castAdd g₂.size) ρ₁ ρ₂ := by
|
||
induction tr with
|
||
| single hbs =>
|
||
exact Trace.single (by rwa [show (g₁ ∙ g₂).nodes = Fin.append g₁.nodes g₂.nodes from rfl,
|
||
Fin.append_left])
|
||
| edge hbs he _ ih =>
|
||
refine Trace.edge ?_ ?_ ih
|
||
· rwa [show (g₁ ∙ g₂).nodes = Fin.append g₁.nodes g₂.nodes from rfl, Fin.append_left]
|
||
· exact List.mem_append_left _ (List.mem_map_of_mem _ he)
|
||
|
||
/-- When two graphs are overlaid, for each trace in the right graph,
|
||
a corresponding trace exists in the combined graph. -/
|
||
noncomputable def Trace.overlay_right {idx₁ idx₂ : g₂.Index}
|
||
(tr : Trace g₂ idx₁ idx₂ ρ₁ ρ₂) :
|
||
Trace (g₁ ∙ g₂) (idx₁.natAdd g₁.size) (idx₂.natAdd g₁.size) ρ₁ ρ₂ := by
|
||
induction tr with
|
||
| single hbs =>
|
||
exact Trace.single (by rwa [show (g₁ ∙ g₂).nodes = Fin.append g₁.nodes g₂.nodes from rfl,
|
||
Fin.append_right])
|
||
| edge hbs he _ ih =>
|
||
refine Trace.edge ?_ ?_ ih
|
||
· rwa [show (g₁ ∙ g₂).nodes = Fin.append g₁.nodes g₂.nodes from rfl, Fin.append_right]
|
||
· exact List.mem_append_right _ (List.mem_map_of_mem _ he)
|
||
|
||
/-- When two graphs are sequenced, for each trace in the first graph,
|
||
a corresponding trace exists in the combined graph. -/
|
||
noncomputable def Trace.sequence_left {idx₁ idx₂ : g₁.Index}
|
||
(tr : Trace g₁ idx₁ idx₂ ρ₁ ρ₂) :
|
||
Trace (g₁ ⤳ g₂) (idx₁.castAdd g₂.size) (idx₂.castAdd g₂.size) ρ₁ ρ₂ := by
|
||
induction tr with
|
||
| single hbs =>
|
||
exact Trace.single (by rwa [show (g₁ ⤳ g₂).nodes = Fin.append g₁.nodes g₂.nodes from rfl,
|
||
Fin.append_left])
|
||
| edge hbs he _ ih =>
|
||
refine Trace.edge ?_ ?_ ih
|
||
· rwa [show (g₁ ⤳ g₂).nodes = Fin.append g₁.nodes g₂.nodes from rfl, Fin.append_left]
|
||
· exact List.mem_append_left _ (List.mem_append_left _ (List.mem_map_of_mem _ he))
|
||
|
||
/-- When two graphs are sequenced, for each trace in the second graph,
|
||
a corresponding trace exists in the combined graph. -/
|
||
noncomputable def Trace.sequence_right {idx₁ idx₂ : g₂.Index}
|
||
(tr : Trace g₂ idx₁ idx₂ ρ₁ ρ₂) :
|
||
Trace (g₁ ⤳ g₂) (idx₁.natAdd g₁.size) (idx₂.natAdd g₁.size) ρ₁ ρ₂ := by
|
||
induction tr with
|
||
| single hbs =>
|
||
exact Trace.single (by rwa [show (g₁ ⤳ g₂).nodes = Fin.append g₁.nodes g₂.nodes from rfl,
|
||
Fin.append_right])
|
||
| edge hbs he _ ih =>
|
||
refine Trace.edge ?_ ?_ ih
|
||
· rwa [show (g₁ ⤳ g₂).nodes = Fin.append g₁.nodes g₂.nodes from rfl, Fin.append_right]
|
||
· exact List.mem_append_left _
|
||
(List.mem_append_right _ (List.mem_map_of_mem _ he))
|
||
|
||
/-- Equivalent of `Trace.overlay_left` for end-to-end traces. -/
|
||
noncomputable def EndToEndTrace.overlay_left (etr : EndToEndTrace g₁ ρ₁ ρ₂) :
|
||
EndToEndTrace (g₁ ∙ g₂) ρ₁ ρ₂ := by
|
||
obtain ⟨i₁, h₁, i₂, h₂, tr⟩ := etr
|
||
exact ⟨i₁.castAdd g₂.size, List.mem_append_left _ (List.mem_map_of_mem _ h₁),
|
||
i₂.castAdd g₂.size, List.mem_append_left _ (List.mem_map_of_mem _ h₂),
|
||
tr.overlay_left⟩
|
||
|
||
/-- Equivalent of `Trace.overlay_right` for end-to-end traces. -/
|
||
noncomputable def EndToEndTrace.overlay_right (etr : EndToEndTrace g₂ ρ₁ ρ₂) :
|
||
EndToEndTrace (g₁ ∙ g₂) ρ₁ ρ₂ := by
|
||
obtain ⟨i₁, h₁, i₂, h₂, tr⟩ := etr
|
||
exact ⟨i₁.natAdd g₁.size, List.mem_append_right _ (List.mem_map_of_mem _ h₁),
|
||
i₂.natAdd g₁.size, List.mem_append_right _ (List.mem_map_of_mem _ h₂),
|
||
tr.overlay_right⟩
|
||
|
||
/-- When two graphs are sequenced, two end-to-end traces through the respective
|
||
graphs can be sequenced to create an end-to-end trace in the combined
|
||
graph. This is only possible for end-to-end traces and not for general
|
||
`Trace`s, because sequencing only introduces edges from the output nodes
|
||
of one graph to the input nodes of another graph. A non-end-to-end trace
|
||
need to conclude at the output node, so it cannot necessarily be sequenced
|
||
with a trace in another graph. -/
|
||
noncomputable def EndToEndTrace.concat {ρ₃ : Env} (etr₁ : EndToEndTrace g₁ ρ₁ ρ₂)
|
||
(etr₂ : EndToEndTrace g₂ ρ₂ ρ₃) : EndToEndTrace (g₁ ⤳ g₂) ρ₁ ρ₃ := by
|
||
obtain ⟨i₁, h₁, i₂, h₂, tr₁⟩ := etr₁
|
||
obtain ⟨j₁, k₁, j₂, k₂, tr₂⟩ := etr₂
|
||
refine ⟨i₁.castAdd g₂.size, List.mem_map_of_mem _ h₁,
|
||
j₂.natAdd g₁.size, List.mem_map_of_mem _ k₂,
|
||
tr₁.sequence_left ++< ?_ >++ tr₂.sequence_right⟩
|
||
exact List.mem_append_right _
|
||
(List.mem_product.mpr ⟨List.mem_map_of_mem _ h₂, List.mem_map_of_mem _ k₁⟩)
|
||
|
||
end Embeddings
|
||
|
||
section Loop
|
||
|
||
variable {g : Graph} {ρ₁ ρ₂ ρ₃ : Env}
|
||
|
||
/-- A trace through a body CFG still exists (up to reindexing) in a zero-or-more loop CFG. -/
|
||
noncomputable def Trace.loop {idx₁ idx₂ : g.Index} (tr : Trace g idx₁ idx₂ ρ₁ ρ₂) :
|
||
Trace (Graph.loop g) (idx₁.natAdd 2) (idx₂.natAdd 2) ρ₁ ρ₂ := by
|
||
induction tr with
|
||
| single hbs =>
|
||
exact Trace.single (by
|
||
rwa [show (Graph.loop g).nodes = Fin.append (fun _ : Fin 2 => none) g.nodes from rfl,
|
||
Fin.append_right])
|
||
| edge hbs he _ ih =>
|
||
refine Trace.edge ?_ ?_ ih
|
||
· rwa [show (Graph.loop g).nodes = Fin.append (fun _ : Fin 2 => none) g.nodes from rfl,
|
||
Fin.append_right]
|
||
· exact List.mem_append_left _ (List.mem_append_left _
|
||
(List.mem_append_left _ (List.mem_map_of_mem _ he)))
|
||
|
||
/-- The beginning node of a loop graph is empty. -/
|
||
private lemma loop_nodes_at_in :
|
||
(Graph.loop g).nodes g.loopIn = none :=
|
||
Fin.append_left (fun _ : Fin 2 => none) g.nodes 0
|
||
|
||
/-- The ending node of a loop graph is empty. -/
|
||
private lemma loop_nodes_at_out :
|
||
(Graph.loop g).nodes g.loopOut = none :=
|
||
Fin.append_left (fun _ : Fin 2 => none) g.nodes 1
|
||
|
||
/-- Equivlaent of `Trace.loop` for end-to-end traces. -/
|
||
noncomputable def EndToEndTrace.loop (etr : EndToEndTrace g ρ₁ ρ₂) :
|
||
EndToEndTrace (Graph.loop g) ρ₁ ρ₂ := by
|
||
obtain ⟨i₁, h₁, i₂, h₂, tr⟩ := etr
|
||
-- the edge in → (2 ↑ʳ i₁), reached through the second edge group
|
||
have hin : (g.loopIn, i₁.natAdd 2) ∈ (Graph.loop g).edges := by
|
||
refine List.mem_append_left _ (List.mem_append_left _ (List.mem_append_right _ ?_))
|
||
exact List.mem_map_of_mem _ (List.mem_map_of_mem _ h₁)
|
||
-- the edge (2 ↑ʳ i₂) → out, reached through the third edge group
|
||
have hout : (i₂.natAdd 2, g.loopOut) ∈ (Graph.loop g).edges := by
|
||
refine List.mem_append_left _ (List.mem_append_right _ ?_)
|
||
exact List.mem_map_of_mem _ (List.mem_map_of_mem _ h₂)
|
||
refine ⟨g.loopIn, List.mem_singleton_self _, g.loopOut, List.mem_singleton_self _, ?_⟩
|
||
exact Trace.single (loop_nodes_at_in ▸ EvalBasicStmtOpt.none) ++< hin >++
|
||
tr.loop ++< hout >++ Trace.single (loop_nodes_at_out ▸ EvalBasicStmtOpt.none)
|
||
|
||
/-- The zero-or-more times loop has an edge to return back to the top, to continue after an iteration. -/
|
||
private lemma loop_edge_out_in :
|
||
((g.loopOut, g.loopIn) : (Graph.loop g).Edge) ∈ (Graph.loop g).edges := by
|
||
refine List.mem_append_right _ ?_
|
||
exact List.mem_cons_self _ _
|
||
|
||
/-- Two traces through a loop can be combined, since a loop can be executed any number of times. -/
|
||
noncomputable def EndToEndTrace.loop_concat (etr₁ : EndToEndTrace (Graph.loop g) ρ₁ ρ₂)
|
||
(etr₂ : EndToEndTrace (Graph.loop g) ρ₂ ρ₃) :
|
||
EndToEndTrace (Graph.loop g) ρ₁ ρ₃ := by
|
||
obtain ⟨i₁, h₁, i₂, h₂, tr₁⟩ := etr₁
|
||
obtain ⟨j₁, k₁, j₂, k₂, tr₂⟩ := etr₂
|
||
simp only [Graph.loop_inputs, Graph.loop_outputs, List.mem_singleton] at h₁ h₂ k₁ k₂
|
||
subst h₁; subst h₂; subst k₁; subst k₂
|
||
exact ⟨g.loopIn, List.mem_singleton_self _, g.loopOut, List.mem_singleton_self _,
|
||
tr₁ ++< loop_edge_out_in >++ tr₂⟩
|
||
|
||
/-- A loop can be executed zero times. -/
|
||
noncomputable def EndToEndTrace.loop_empty {ρ : Env} : EndToEndTrace (Graph.loop g) ρ ρ := by
|
||
have hedge : ((g.loopIn, g.loopOut) : (Graph.loop g).Edge) ∈ (Graph.loop g).edges :=
|
||
List.mem_append_right _ (List.mem_cons_of_mem _ (List.mem_cons_self _ _))
|
||
exact ⟨g.loopIn, List.mem_singleton_self _, g.loopOut, List.mem_singleton_self _,
|
||
Trace.single (loop_nodes_at_in ▸ EvalBasicStmtOpt.none) ++< hedge >++
|
||
Trace.single (loop_nodes_at_out ▸ EvalBasicStmtOpt.none)⟩
|
||
|
||
end Loop
|
||
|
||
/-- A CFG consisting of only a single node has a trace through it corresponding to that node. -/
|
||
noncomputable def EndToEndTrace.singleton {o : Option BasicStmt} {ρ₁ ρ₂ : Env}
|
||
(h : EvalBasicStmtOpt ρ₁ o ρ₂) : EndToEndTrace (Graph.singleton o) ρ₁ ρ₂ :=
|
||
⟨(0 : Fin 1), List.mem_singleton_self _, (0 : Fin 1), List.mem_singleton_self _,
|
||
Trace.single h⟩
|
||
|
||
/-- If a CFG's only node is empty, the no-op trace exists through it. -/
|
||
noncomputable def EndToEndTrace.singleton_nil (ρ : Env) :
|
||
EndToEndTrace (Graph.singleton none) ρ ρ :=
|
||
EndToEndTrace.singleton EvalBasicStmtOpt.none
|
||
|
||
/-- Invoking 'Graph.wrap` (which ensures a single entry and exit node for a CFG)
|
||
does not invalidate traces in the original graph. -/
|
||
noncomputable def EndToEndTrace.wrap {g : Graph} {ρ₁ ρ₂ : Env}
|
||
(etr : EndToEndTrace g ρ₁ ρ₂) : EndToEndTrace (Graph.wrap g) ρ₁ ρ₂ :=
|
||
(EndToEndTrace.singleton_nil ρ₁).concat (etr.concat (EndToEndTrace.singleton_nil ρ₂))
|
||
|
||
/-- Key result: the control flow graph admits every execution that's made
|
||
possible by a language's semantics. Thus, the CFG encodes _at least_ all
|
||
semantically-possible executions. Informally, we can conclude from this
|
||
that if we compute a result that using the graph's edges to determine
|
||
what's possible, this result will not disagree with the semantics.
|
||
|
||
Note that a CFG like $K_4$ (where the nodes are basic blocks) is
|
||
technically also a sufficient graph, but is very likely meaningless in that
|
||
it grossly overestimates the possible execution paths in the language, and
|
||
thus is bound to produce less-than-specific results. There is as yet no
|
||
result in this framework that the CFG we produce is _minimal_: loosely,
|
||
posessing only edges for things that are admitted by the semantics.
|
||
This is difficult to state (in its strongest form, this would
|
||
require the CFG to be able to detect something like `while (alwaysFalse)`,
|
||
and so remains a TODO. -/
|
||
noncomputable def Stmt.cfg_sufficient {s : Stmt} {ρ₁ ρ₂ : Env}
|
||
(h : EvalStmt ρ₁ s ρ₂) : EndToEndTrace s.cfg ρ₁ ρ₂ := by
|
||
induction h with
|
||
| basic ρ₁ ρ₂ bs hbs =>
|
||
exact EndToEndTrace.singleton (EvalBasicStmtOpt.some hbs)
|
||
| andThen ρ₁ ρ₂ ρ₃ s₁ s₂ _ _ ih₁ ih₂ =>
|
||
exact ih₁.concat ih₂
|
||
| ifTrue ρ₁ ρ₂ e z s₁ s₂ _ _ _ ih =>
|
||
exact ih.overlay_left
|
||
| ifFalse ρ₁ ρ₂ e s₁ s₂ _ _ ih =>
|
||
exact ih.overlay_right
|
||
| whileTrue ρ₁ ρ₂ ρ₃ e z s _ _ _ _ ih₁ ih₂ =>
|
||
exact (ih₁.loop).loop_concat ih₂
|
||
| whileFalse ρ e s _ =>
|
||
exact EndToEndTrace.loop_empty
|
||
|
||
/-- The input / entry node generated by `Graph.wrap`. -/
|
||
def Graph.wrapInput (g : Graph) : (Graph.wrap g).Index :=
|
||
(0 : Fin 1).castAdd ((g ⤳ Graph.singleton none).size)
|
||
|
||
/-- The output / exit node generated by `Graph.wrap`. -/
|
||
def Graph.wrapOutput (g : Graph) : (Graph.wrap g).Index :=
|
||
Fin.natAdd 1 ((Fin.natAdd g.size (0 : Fin 1)))
|
||
|
||
/-- The `Graph.wrapInput` is, indeed, the graph's only input after `Graph.wrap`. -/
|
||
lemma Graph.wrap_inputs (g : Graph) :
|
||
(Graph.wrap g).inputs = [g.wrapInput] := rfl
|
||
|
||
/-- The `Graph.wrapInput` is, indeed, the graph's only output after `Graph.wrap`. -/
|
||
lemma Graph.wrap_outputs (g : Graph) :
|
||
(Graph.wrap g).outputs = [g.wrapOutput] := rfl
|
||
|
||
/-- When sequencing (proven here with `Graph.singleton` on the left), no edges
|
||
exist from the right-hand graph back to the left. -/
|
||
private lemma not_mem_edges_castAdd_sequence {g₂ : Graph} (i : Fin 1)
|
||
(idx : (Graph.singleton none ⤳ g₂).Index) :
|
||
((idx, i.castAdd g₂.size) : (Graph.singleton none ⤳ g₂).Edge)
|
||
∉ (Graph.singleton none ⤳ g₂).edges := by
|
||
intro h
|
||
rcases List.mem_append.mp h with h' | h'
|
||
· rcases List.mem_append.mp h' with h'' | h''
|
||
· -- lifted edges of `singleton []`: there are none
|
||
simp [Graph.singleton, List.finCastAddProd] at h''
|
||
· -- lifted edges of g₂: targets are natAdd
|
||
obtain ⟨e, _, heq⟩ := List.mem_map.mp h''
|
||
exact Fin.castAdd_ne_natAdd i e.2 (congrArg Prod.snd heq).symm
|
||
· -- product edges: targets are natAdd'd inputs of g₂
|
||
obtain ⟨-, hb⟩ := List.mem_product.mp h'
|
||
obtain ⟨j, -, heq⟩ := List.mem_map.mp hb
|
||
exact Fin.castAdd_ne_natAdd i j heq.symm
|
||
|
||
/-- The input node of a graph after `Graph.wrap` has no predecessors. -/
|
||
lemma Graph.wrap_predecessors_eq_nil (g : Graph) (idx : (Graph.wrap g).Index)
|
||
(h : idx ∈ (Graph.wrap g).inputs) :
|
||
(Graph.wrap g).predecessors idx = [] := by
|
||
rw [Graph.wrap_inputs, List.mem_singleton] at h
|
||
subst h
|
||
rw [GGraph.predecessors, List.filter_eq_nil_iff]
|
||
intro idx' _
|
||
simpa using not_mem_edges_castAdd_sequence (g₂ := g ⤳ Graph.singleton none) 0 idx'
|
||
|
||
end Spa
|