From d313d3a9ea68d72bc7578e5a734a0cadb2cb1654 Mon Sep 17 00:00:00 2001 From: math-fehr Date: Fri, 12 Jun 2026 16:22:11 +0100 Subject: [PATCH 01/31] feat: Add lemmas about `setArgumentValues?` and the equation lemmas These lemmas are similar to the ones about `setResultValues` in that they show preservation of `EquationLemmaAt` and `DefinesDominating`. These required additional simple lemmas, and additional axioms about dominance. --- Veir/Dominance.lean | 29 +++++++ Veir/IR/Basic.lean | 8 ++ Veir/Interpreter/EquationLemma.lean | 45 ++++++++++ Veir/Interpreter/Lemmas.lean | 126 ++++++++++++++++++++++++++++ Veir/Rewriter/InsertPoint.lean | 10 +++ 5 files changed, 218 insertions(+) diff --git a/Veir/Dominance.lean b/Veir/Dominance.lean index 9ec6c198b..10ea02b9b 100644 --- a/Veir/Dominance.lean +++ b/Veir/Dominance.lean @@ -159,3 +159,32 @@ it dominates the program point before the operation, or it is a result of the op axiom WfIRContext.Dom.value_dominatesIp_after_iff (ctxDom : ctx.Dom) : value.dominatesIp (InsertPoint.after op ctx.raw block blockIsParent opInBounds) ctx ↔ value.dominatesIp (InsertPoint.before op) ctx ∨ value ∈ op.getResults! ctx.raw + +/-- A value dominating the entry of a successor block either already dominates the predecessor's +end, or it is one of the successor's own block arguments. -/ +axiom WfIRContext.Dom.value_dominatesIp_successor_entry (ctxDom : ctx.Dom) + {block : BlockPtr} (blockInBounds : block.InBounds ctx.raw) + (hsucc : succ ∈ block.getSuccessors! ctx.raw) : + value.dominatesIp (InsertPoint.atStart! succ ctx.raw) ctx → + value.dominatesIp (InsertPoint.atEnd block) ctx ∨ + value ∈ succ.getArguments! ctx.raw + +/-- An operation dominating the entry of a successor already dominates the predecessor's end. -/ +axiom WfIRContext.Dom.op_dominatesIp_successor_entry (ctxDom : ctx.Dom) + {block : BlockPtr} (blockInBounds : block.InBounds ctx.raw) + (hsucc : succ ∈ block.getSuccessors! ctx.raw) : + op.dominatesIp (InsertPoint.atStart! succ ctx.raw) ctx → + op.dominatesIp (InsertPoint.atEnd block) ctx + +/-- An argument of a block dominates the block's start. -/ +axiom WfIRContext.Dom.blockArgument_dominatesIp_entry (ctxDom : ctx.Dom) + {block : BlockPtr} (blockInBounds : block.InBounds ctx.raw) + (hMem : value ∈ block.getArguments! ctx.raw) : + value.dominatesIp (InsertPoint.atStart! block ctx.raw) ctx + +/-- An argument of a block cannot dominate a program point that dominates hde block start. -/ +axiom WfIRContext.Dom.blockArgument_not_dominatesIp_before_of_dominatesIp_firstOp + (ctxDom : ctx.Dom) {op : OperationPtr} (opInBounds : op.InBounds ctx.raw) + (opDom : op.dominatesIp (InsertPoint.atStart! block ctx.raw) ctx) + (hMem : value ∈ block.getArguments! ctx.raw) : + ¬ value.dominatesIp (InsertPoint.before op) ctx diff --git a/Veir/IR/Basic.lean b/Veir/IR/Basic.lean index b515d4248..61b1ddfbd 100644 --- a/Veir/IR/Basic.lean +++ b/Veir/IR/Basic.lean @@ -2557,6 +2557,14 @@ theorem cons_iff {ctx : IRContext OpInfo} {parent : BlockPtr} {head : OperationP end BlockPtr.OpChainSlice +/-- Get the successors of a block. This is defined as the successors of the terminator operation. +In case the block has no terminator, this function returns an empty array. -/ +def BlockPtr.getSuccessors! (block : BlockPtr) (ctx : IRContext OpInfo) : Array BlockPtr := + let term := (block.get! ctx).lastOp + match term with + | none => #[] + | some term => term.getSuccessors! ctx + def IRContext.empty (OpInfo : Type) [HasOpInfo OpInfo] : IRContext OpInfo := { nextID := 0, operations := Std.HashMap.emptyWithCapacity, diff --git a/Veir/Interpreter/EquationLemma.lean b/Veir/Interpreter/EquationLemma.lean index 6a5625343..b75a0d2f7 100644 --- a/Veir/Interpreter/EquationLemma.lean +++ b/Veir/Interpreter/EquationLemma.lean @@ -204,6 +204,51 @@ theorem interpretOp_DefinesDominating {ctx : WfIRContext OpCode} {opInBounds} case inr => grind [OperationPtr.getResults!.mem_iff_exists_index] +/-- Setting a successor's block arguments preserves `DefinesDominating` in a state satisfying it at +the predecessor's exit. -/ +theorem InterpreterState.DefinesDominating.setArgumentValues?_succ_entry + (ctxDom : ctx.Dom) {exitState : InterpreterState ctx} + {block : BlockPtr} (blockInBounds : block.InBounds ctx.raw) + (hsucc : succ ∈ block.getSuccessors! ctx.raw) + (hExit : exitState.DefinesDominating (InsertPoint.atEnd block)) + (hArgs : exitState.variables.setArgumentValues? succ res succInBounds = some newVars) : + InterpreterState.DefinesDominating ⟨newVars, exitState.memory⟩ (InsertPoint.atStart! succ ctx.raw) := by + intro value valueInBounds valueDom + cases WfIRContext.Dom.value_dominatesIp_successor_entry ctxDom blockInBounds hsucc valueDom + · grind [InterpreterState.DefinesDominating] + · grind [BlockPtr.getArguments!.mem_iff_exists_index] + +/-- `EquationHolds` for an `op` dominating `succ`'s entry is preserved when setting `succ`'s block +arguments. -/ +theorem InterpreterState.EquationHolds.setArgumentValues?_of_dominatesIp (ctxDom : ctx.Dom) + (opDom : op.dominatesIp (InsertPoint.atStart! succ ctx.raw) ctx) + {exitState : InterpreterState ctx} (hEq : exitState.EquationHolds op opIn) + (hArgs : exitState.variables.setArgumentValues? succ res succInBounds = some newVars) : + InterpreterState.EquationHolds ⟨newVars, exitState.memory⟩ op := by + simp only [InterpreterState.EquationHolds] at hEq ⊢ + obtain ⟨cf, hinterp⟩ := hEq + simp only [interpretOp_some_iff] at hinterp ⊢ + grind [VariableState.getOperandValues_eq_of_getVar?_eq, + VariableState.getVar?_setArgumentValues?_of_notMem_getArguments!, + WfIRContext.Dom.blockArgument_not_dominatesIp_before_of_dominatesIp_firstOp, + → VariableState.setResultValues?_setArgumentValues?_comm] + +/-- Setting a successor's block arguments preserves `EquationLemmaAt` in a state satisfying it at +the predecessor's exit. -/ +theorem InterpreterState.EquationLemmaAt.setArgumentValues?_succ_entry (ctxDom : ctx.Dom) + {block : BlockPtr} (blockInBounds : block.InBounds ctx.raw) + (hsucc : succ ∈ block.getSuccessors! ctx.raw) + {exitState : InterpreterState ctx} + (hExit : exitState.EquationLemmaAt (InsertPoint.atEnd block)) + (hArgs : exitState.variables.setArgumentValues? succ res succInBounds = some newVars) : + InterpreterState.EquationLemmaAt ⟨newVars, exitState.memory⟩ + (InsertPoint.atStart! succ ctx.raw) := by + intro op opIn hPure hDom + have opDomAtEnd : op.dominatesIp (InsertPoint.atEnd block) ctx := by + grind [WfIRContext.Dom.op_dominatesIp_successor_entry] + have := hExit op opIn hPure opDomAtEnd + grind [InterpreterState.EquationHolds.setArgumentValues?_of_dominatesIp] + /-- Interpreting a verified operation never fails on a state satisfying `DefinesDominating` at the operation's location. -/ theorem InterpreterState.DefinesDominating.interpretOp_ne_none diff --git a/Veir/Interpreter/Lemmas.lean b/Veir/Interpreter/Lemmas.lean index f053fee79..4cc915e52 100644 --- a/Veir/Interpreter/Lemmas.lean +++ b/Veir/Interpreter/Lemmas.lean @@ -180,6 +180,90 @@ theorem VariableState.getVar?_setResultValues?_of_value_inBounds simp only [getVar?_setResultValues? h] cases value <;> grind +theorem VariableState.getVar?_setArgumentValues?_loop {block : BlockPtr} + {values : Array RuntimeValue} {i : Nat} {iInBounds blockInBounds} : + VariableState.setArgumentValues?.loop block values blockInBounds varState i iInBounds = some varState' → + varState'.getVar? value = + match value with + | .blockArgument ⟨block', index⟩ => + if block' = block ∧ index < i then + some values[index]! + else + varState.getVar? value + | _ => + varState.getVar? value := by + fun_induction VariableState.setArgumentValues?.loop + next => grind + next => + simp only [Option.bind_eq_bind, Nat.succ_eq_add_one, Option.bind] + grind [cases BlockArgumentPtr, OperationPtr.getResult_def, cases ValuePtr] + +@[grind =>] +theorem VariableState.getVar?_setArgumentValues? {block : BlockPtr} {values : Array RuntimeValue} + {blockInBounds} : + varState.setArgumentValues? block values blockInBounds = some varState' → + varState'.getVar? value = + match value with + | .blockArgument ⟨block', index⟩ => + if block' = block ∧ index < block.getNumArguments! ctx.raw then + some values[index]! + else + varState.getVar? value + | _ => + varState.getVar? value := by + grind [VariableState.setArgumentValues?, VariableState.getVar?_setArgumentValues?_loop] + +theorem VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! + {block : BlockPtr} {value values blockInBounds} : + value ∉ block.getArguments! ctx.raw → + varState.setArgumentValues? block values blockInBounds = some varState' → + varState'.getVar? value = varState.getVar? value := by + intro hNotMem hSet + simp only [VariableState.getVar?_setArgumentValues? hSet] + rcases value with _ | ⟨block', index⟩ + · grind + · simp only [BlockPtr.getArguments!.mem_iff_exists_index, not_exists, not_and] at hNotMem + grind [BlockPtr.getArgument_def] + +/-- `block` arguments are exactly the values set by `setArgumentValues? block` in the new state. -/ +theorem VariableState.getVar?_getArgument_of_setArgumentValues? + {block : BlockPtr} {values blockInBounds} : + i < block.getNumArguments! ctx.raw → + varState.setArgumentValues? block values blockInBounds = some varState' → + varState'.getVar? (block.getArgument i) = some values[i]! := by + intro hi hSet + simp only [VariableState.getVar?_setArgumentValues? hSet] + grind [BlockPtr.getArgument_def] + +/-- `setArgumentValues?.loop` succeeds iff every argument value it binds conforms to its argument +type. -/ +theorem VariableState.setArgumentValues?_loop_isSome_iff {block : BlockPtr} + {values : Array RuntimeValue} {i : Nat} {iInBounds blockInBounds} : + (∀ j, j < i → (values[j]!).Conforms ((block.getArgument j : ValuePtr).getType! ctx.raw)) ↔ + (∃ v, VariableState.setArgumentValues?.loop block values blockInBounds varState i iInBounds = some v) := by + fun_induction VariableState.setArgumentValues?.loop + next => grind + next varState hin k hk arg value ih => + simp only [Option.bind_eq_bind, Option.bind] + constructor + · intro hconform + rw [VariableState.setVar?_eq_some_setVar (hconform k (by grind))] + simp only [← ih] + grind + · rintro ⟨v, hv⟩ j hj + rcases hsv : hin.setVar? (ValuePtr.blockArgument arg) value with _ | varState' + · grind + · grind [ih varState'] + +/-- `setArgumentValues?` succeeds iff every argument value conforms to its argument type. -/ +theorem VariableState.setArgumentValues?_isSome_iff_conforms (varState : VariableState ctx) + {block : BlockPtr} {values : Array RuntimeValue} {blockInBounds} : + (∀ j, j < block.getNumArguments! ctx.raw → + (values[j]!).Conforms ((block.getArgument j : ValuePtr).getType! ctx.raw)) ↔ + (∃ v, varState.setArgumentValues? block values blockInBounds = some v) := by + simp only [VariableState.setArgumentValues?] + exact VariableState.setArgumentValues?_loop_isSome_iff + /-- Assert equality between two `interpretOp'` calls that have the same operation type and properties. This lemma is useful to avoid introducing extra casts on the `interpretOp'` arguments. @@ -282,6 +366,48 @@ theorem VariableState.setResultValues?_comm ext val value cases val <;> grind [getVar?_setResultValues?] +/-- Success of `setArgumentValues?` only depends on the values conforming to the argument types, +not on the contents of the variable state, so it can be transferred to any other state. -/ +theorem VariableState.setArgumentValues?_eq_some_of_varState + (varState₂ : VariableState ctx) : + varState.setArgumentValues? block values blockInBounds = some varState' → + ∃ varState₂', varState₂.setArgumentValues? block values blockInBounds = some varState₂' := by + intro h + simp only [← VariableState.setArgumentValues?_isSome_iff_conforms] + grind [(VariableState.setArgumentValues?_isSome_iff_conforms varState).mpr ⟨_, h⟩] + +/-- Setting block arguments and operation results commute: block arguments and operation results +are distinct `ValuePtr`s, so they never alias. -/ +theorem VariableState.setArgumentValues?_setResultValues?_comm : + varState.setArgumentValues? block argValues blockInBounds = some varState' → + varState'.setResultValues? op resValues opInBounds = some varState'' → + ∃ varState₂, varState.setResultValues? op resValues opInBounds = some varState₂ ∧ + varState₂.setArgumentValues? block argValues blockInBounds = some varState'' := by + intros h₁ h₂ + have ⟨varState₂, hvs₂⟩ := setResultValues?_eq_some_of_varState varState h₂ + have ⟨varState₂', hvs₂'⟩ := setArgumentValues?_eq_some_of_varState varState₂ h₁ + exists varState₂ + constructor; grind + simp only [hvs₂', Option.some.injEq] + ext val value + grind [getVar?_setResultValues?, getVar?_setArgumentValues?] + +/-- Setting operation results and block arguments commute: block arguments and operation results +are distinct `ValuePtr`s, so they never alias. -/ +theorem VariableState.setResultValues?_setArgumentValues?_comm : + varState.setResultValues? op resValues opInBounds = some varState' → + varState'.setArgumentValues? block argValues blockInBounds = some varState'' → + ∃ varState₂, varState.setArgumentValues? block argValues blockInBounds = some varState₂ ∧ + varState₂.setResultValues? op resValues opInBounds = some varState'' := by + intros h₁ h₂ + have ⟨varState₂, hvs₂⟩ := setArgumentValues?_eq_some_of_varState varState h₂ + have ⟨varState₂', hvs₂'⟩ := setResultValues?_eq_some_of_varState varState₂ h₁ + exists varState₂ + constructor; grind + simp only [hvs₂', Option.some.injEq] + ext val value + grind [getVar?_setResultValues?, getVar?_setArgumentValues?] + theorem VariableState.getVar?_setResultValues?_operand_of_dominates (ctxDom : ctx.Dom) (hdom : op'.dominates op ctx) : value ∈ op'.getOperands! ctx.raw → diff --git a/Veir/Rewriter/InsertPoint.lean b/Veir/Rewriter/InsertPoint.lean index 5891d5250..a97616783 100644 --- a/Veir/Rewriter/InsertPoint.lean +++ b/Veir/Rewriter/InsertPoint.lean @@ -56,6 +56,16 @@ theorem InsertPoint.atStart!_eq_atStart (block : BlockPtr) (ctx : IRContext OpIn InsertPoint.atStart! block ctx = InsertPoint.atStart block ctx hIn := by cases (block.get ctx (by grind)).firstOp <;> grind [InsertPoint.atStart!, InsertPoint.atStart] +@[simp, grind =] +theorem InsertPoint.inBounds_atStart (ctxWf : ctx.WellFormed) : + (InsertPoint.atStart block ctx hIn).InBounds ctx ↔ block.InBounds ctx := by + grind [InsertPoint.atStart] + +@[simp, grind =] +theorem InsertPoint.inBounds_atStart! (ctxWf : ctx.WellFormed) (blockInBounds : block.InBounds ctx) : + (InsertPoint.atStart! block ctx).InBounds ctx ↔ block.InBounds ctx := by + grind [InsertPoint.atStart!] + def InsertPoint.after (op : OperationPtr) (ctx : IRContext OpInfo) (block : BlockPtr) (_opHasParent : (op.get! ctx).parent = some block := by grind) (opInBounds : op.InBounds ctx := by grind) : InsertPoint := From 7a8a64b9fe285551214d348a3e06c78b297cd8dd Mon Sep 17 00:00:00 2001 From: math-fehr Date: Fri, 19 Jun 2026 02:08:47 +0100 Subject: [PATCH 02/31] --- Veir/Interpreter/Lemmas.lean | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Veir/Interpreter/Lemmas.lean b/Veir/Interpreter/Lemmas.lean index 4cc915e52..5b35d3e15 100644 --- a/Veir/Interpreter/Lemmas.lean +++ b/Veir/Interpreter/Lemmas.lean @@ -644,3 +644,23 @@ theorem interpretOp'_results_conform {ctx : WfIRContext OpCode} (h : op.interpret ctx.raw operands mem = some (.ok (vals, mem', act))) : RuntimeValue.ArrayConforms vals (op.getResultTypes! ctx.raw) := by sorry + +set_option warn.sorry false in +/-- A branch control-flow action returned by interpreting an operation always targets one of the +provided successor blocks. -/ +theorem interpretOp'_branch_dest_mem + (h : interpretOp' opType properties resultTypes operands blockOperands mem + = some (.ok (vals, mem', some (.branch res dest)))) : + dest ∈ blockOperands := by + sorry + +/-- A branch control-flow action returned by interpreting an operation always targets one of the +operation successors. -/ +theorem interpretOp_branch_dest_mem_getSuccessors! + {ctx : WfIRContext OpCode} {op : OperationPtr} {state state' : InterpreterState ctx} + {inBounds : op.InBounds ctx.raw} {res : Array RuntimeValue} {dest : BlockPtr} + (h : interpretOp op state inBounds = some (.ok (state', some (.branch res dest)))) : + dest ∈ op.getSuccessors! ctx.raw := by + obtain ⟨operandValues, resValues, mem', varState', hOperand, hInterp', hSetRes, hStateEq⟩ := + interpretOp_some_iff.mp h + exact interpretOp'_branch_dest_mem hInterp' From 4048468903e91d47fa1c21ad4e702876d4c19937 Mon Sep 17 00:00:00 2001 From: math-fehr Date: Sat, 20 Jun 2026 02:16:08 +0100 Subject: [PATCH 03/31] --- Veir/Interpreter/Refinement/Lemmas.lean | 14 ++++ Veir/Interpreter/Refinement/Monotonicity.lean | 74 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/Veir/Interpreter/Refinement/Lemmas.lean b/Veir/Interpreter/Refinement/Lemmas.lean index 053098586..72f51c252 100644 --- a/Veir/Interpreter/Refinement/Lemmas.lean +++ b/Veir/Interpreter/Refinement/Lemmas.lean @@ -129,6 +129,20 @@ theorem ValueMapping.applyToArray_getResults!_ext Array.getElem_attach] at hResults grind +/-- Extensibility theorem for value mappings fixing a block's argument pointers across contexts. -/ +theorem ValueMapping.applyToArray_getArguments!_ext + {ctx ctx' : WfIRContext OpInfo} {block : BlockPtr} + {mapping : ValueMapping ctx ctx'} + (blockIn : block.InBounds ctx.raw) + (hArgs : mapping.applyToArray (block.getArguments! ctx.raw) = block.getArguments! ctx'.raw) : + ∀ (i : Nat) (hi : i < block.getNumArguments! ctx.raw), + (mapping ⟨block.getArgument i, (by grind)⟩).val = block.getArgument i := by + intro i hi + simp only [applyToArray, Array.ext_iff, Array.size_map, Array.size_attach, + BlockPtr.getArguments!.size_eq_getNumArguments!, Array.getElem_map, + Array.getElem_attach] at hArgs + grind + /-- If a value mapping reflects results from `op` to `op'`, then values that are not in `op` results are not mapped to values in `op'` results. -/ @[grind .] diff --git a/Veir/Interpreter/Refinement/Monotonicity.lean b/Veir/Interpreter/Refinement/Monotonicity.lean index ab49da0e3..c4d331ac9 100644 --- a/Veir/Interpreter/Refinement/Monotonicity.lean +++ b/Veir/Interpreter/Refinement/Monotonicity.lean @@ -72,6 +72,80 @@ theorem VariableState.setResultValues?_isRefinedBy have hfix := ValueMapping.applyToArray_getResults!_ext opIn hResults.symm grind [RuntimeValue.arrayIsRefinedBy] +/-- `setArgumentValues?` preserves the state refinement. If the source/target variable states are +related by `mapping`, the block argument values refine pointwise (`values ⊒ values'`), the +renaming `mapping` doesn't change the block arguments (`hArgs` and `hReflectArgs`), and the target +values conform to the target argument types (`tgtConforms`), then the target `setArgumentValues?` +also succeeds and the states after binding the block arguments are again related by `mapping`. -/ +theorem VariableState.setArgumentValues?_isRefinedBy {ctx ctx' : WfIRContext OpCode} + {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} {mapping : ValueMapping ctx ctx'} + {block : BlockPtr} {values values' : Array RuntimeValue} {newSrcVars : VariableState ctx} + (hRef : srcVars.isRefinedBy tgtVars mapping) + (hVals : values ⊒ values') + (blockIn : block.InBounds ctx.raw) (blockIn' : block.InBounds ctx'.raw) + (hArgs : block.getArguments! ctx'.raw = mapping.applyToArray (block.getArguments! ctx.raw)) + (hReflectArgs : ∀ (val : ValuePtr) (valIn : val.InBounds ctx.raw) (arg : ValuePtr), + arg ∈ block.getArguments! ctx'.raw → + (mapping ⟨val, valIn⟩).val = arg → val = arg) + (tgtConforms : ∀ j, j < block.getNumArguments! ctx'.raw → + (values'[j]!).Conforms ((block.getArguments! ctx'.raw)[j]!.getType! ctx'.raw)) + (hSrc : srcVars.setArgumentValues? block values blockIn = some newSrcVars) : + ∃ newTgtVars, tgtVars.setArgumentValues? block values' blockIn' = some newTgtVars ∧ + newSrcVars.isRefinedBy newTgtVars mapping := by + -- `applyToArray` preserves size, so the two blocks have the same number of arguments; the renaming + -- fixes every block argument (`hArgs` is the pointwise "fixes" equation in array form). + have hNumArgs : block.getNumArguments! ctx'.raw = block.getNumArguments! ctx.raw := by + have := congrArg Array.size hArgs + simpa using this + have hFix : ∀ (val : ValuePtr) (valIn : val.InBounds ctx.raw), + val ∈ block.getArguments! ctx.raw → (mapping ⟨val, valIn⟩).val = val := by + intro val valIn hMem + obtain ⟨i, hi, rfl⟩ := BlockPtr.getArguments!.mem_iff_exists_index.mp hMem + exact ValueMapping.applyToArray_getArguments!_ext blockIn hArgs.symm i hi + -- Target success follows from conformance of the (refined) target values. + have tgtConforms' : ∀ j, j < block.getNumArguments! ctx'.raw → + (values'[j]!).Conforms ((block.getArgument j : ValuePtr).getType! ctx'.raw) := by + intro j hj + rw [← BlockPtr.getArguments!.getElem!_eq_getArgument hj] + exact tgtConforms j hj + have ⟨newTgtVars, hTgt⟩ := + (VariableState.setArgumentValues?_isSome_iff_conforms tgtVars + (block := block) (blockInBounds := blockIn')).mp tgtConforms' + refine ⟨newTgtVars, hTgt, ?_⟩ + -- Pointwise refinement of the value arrays at every argument index. Out-of-range indices read the + -- same `default` on both sides (the arrays have equal size by `hVals`), so refinement holds there too. + have hPt : ∀ i, i < block.getNumArguments! ctx.raw → values[i]! ⊒ values'[i]! := by + intro i _hi + obtain ⟨hsize, hpt⟩ := hVals + by_cases h : i < values.size + · exact hpt i h + · rw [getElem!_neg values i h, getElem!_neg values' i (hsize ▸ h)] + exact RuntimeValue.isRefinedBy_refl _ + intro val valIn sv hsv + by_cases hMem : val ∈ block.getArguments! ctx.raw + · -- `val` is a block argument `block.getArgument i`, set to `values[i]!`/`values'[i]!`. + obtain ⟨i, hi, rfl⟩ := BlockPtr.getArguments!.mem_iff_exists_index.mp hMem + have hsrcval := VariableState.getVar?_getArgument_of_setArgumentValues? hi hSrc + rw [hsv] at hsrcval + obtain rfl : sv = values[i]! := (Option.some.injEq ..).mp hsrcval + refine ⟨values'[i]!, ?_, hPt i hi⟩ + rw [hFix (block.getArgument i) valIn hMem] + exact VariableState.getVar?_getArgument_of_setArgumentValues? (hNumArgs ▸ hi) hTgt + · -- `val` is not a block argument: its value is unchanged on both sides; use `hRef`. + rw [VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! hMem hSrc] at hsv + obtain ⟨tv, htv, href⟩ := hRef val valIn sv hsv + refine ⟨tv, ?_, href⟩ + -- `mapping val` is not a block argument either (else `hReflectArgs` forces `val` to be one). + have hσnotMem : (mapping ⟨val, valIn⟩).val ∉ block.getArguments! ctx'.raw := by + intro hm + obtain ⟨i, hi, heq⟩ := BlockPtr.getArguments!.mem_iff_exists_index.mp hm + have harg : (block.getArgument i : ValuePtr) ∈ block.getArguments! ctx'.raw := + BlockPtr.getArguments!.mem_iff_exists_index.mpr ⟨i, hi, rfl⟩ + exact hMem (BlockPtr.getArguments!.mem_iff_exists_index.mpr + ⟨i, hNumArgs ▸ hi, (hReflectArgs val valIn _ harg heq.symm).symm⟩) + rw [VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! hσnotMem hTgt] + exact htv + /-- `interpretOp` is monotone under a *cross-context* interpreter-state refinement. From f27237c4c824af0e7b01a21e97a74c73d6db1d18 Mon Sep 17 00:00:00 2001 From: math-fehr Date: Sat, 20 Jun 2026 03:02:19 +0100 Subject: [PATCH 04/31] --- Veir.lean | 1 + Veir/Interpreter/Basic.lean | 6 + Veir/Interpreter/Refinement/Lemmas.lean | 11 + Veir/PatternRewriter/Soundness.lean | 1669 +++++++++++++++++++++++ 4 files changed, 1687 insertions(+) create mode 100644 Veir/PatternRewriter/Soundness.lean diff --git a/Veir.lean b/Veir.lean index 54f6d0ab3..03a34a222 100644 --- a/Veir.lean +++ b/Veir.lean @@ -10,6 +10,7 @@ import Veir.Rewriter.WfRewriter import Veir.Printer import Veir.PatternRewriter.Basic import Veir.Passes.RISCVCombines.Proofs +import Veir.PatternRewriter.Soundness import Veir.Benchmarks import Veir.Parser.Lexer import Veir.Interpreter diff --git a/Veir/Interpreter/Basic.lean b/Veir/Interpreter/Basic.lean index 9ce99680b..6a1287ede 100644 --- a/Veir/Interpreter/Basic.lean +++ b/Veir/Interpreter/Basic.lean @@ -328,6 +328,12 @@ instance : MonadLift Option Interp where instance : Inhabited (Interp α) := ⟨(none : Option (UBOr α))⟩ +/-- The interpreter monad is a chain-complete partial order with `none` as bottom (the flat order on +`Option`). This steers `partial_fixpoint` (used for `interpretBlockCFG`) to take its least fixpoint in +the `none`-bottom order, so that `interpretBlockCFG.fixpoint_induct`'s admissibility base case is the +trivially-true `none` outcome — exactly what refinement/correctness proofs need. -/ +instance instCCPOInterp {α : Type} : Lean.Order.CCPO (Interp α) := Lean.Order.instCCPOOption + /-- Signal undefined behaviour from inside the interpreter monad. -/ @[inline] def Interp.ub : Interp α := some .ub diff --git a/Veir/Interpreter/Refinement/Lemmas.lean b/Veir/Interpreter/Refinement/Lemmas.lean index 72f51c252..608aa313c 100644 --- a/Veir/Interpreter/Refinement/Lemmas.lean +++ b/Veir/Interpreter/Refinement/Lemmas.lean @@ -156,3 +156,14 @@ theorem ValueMapping.ReflectsResults.not_mem_getResults simp only [OperationPtr.getResults!.mem_iff_exists_index] at hmem have ⟨index, hindex, heq⟩ := hmem grind [OperationPtr.getResults!.mem_iff_exists_index, hReflect val valIn index heq.symm] + +/-! ## Conformance under refinement -/ + +/-- Refinement preserves conformance to a type: if `sv ⊒ tv` and `sv` conforms to `ty`, then `tv` +conforms to `ty`. -/ +@[grind →] +theorem RuntimeValue.Conforms_of_isRefinedBy {sv tv : RuntimeValue} {ty : TypeAttr} + (href : sv ⊒ tv) (hconf : sv.Conforms ty) : tv.Conforms ty := by + obtain ⟨attr, hattr⟩ := ty + cases sv <;> cases attr <;> simp_all [RuntimeValue.isRefinedBy, RuntimeValue.Conforms] + all_goals cases tv <;> grind diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean new file mode 100644 index 000000000..b3ef0b39a --- /dev/null +++ b/Veir/PatternRewriter/Soundness.lean @@ -0,0 +1,1669 @@ +import Veir.Interpreter.Refinement.Monotonicity +import Veir.PatternRewriter.Semantics + +/-! +# `RewrittenAt`: the net effect of a single local rewrite + +This file defines `RewrittenAt`, an *abstract* relation capturing the net edit performed by a single +local rewrite: a matched operation `op` (in `ctx`) is replaced by a list of new operations `newOps` +producing `newValues`, yielding a new context `newCtx`. The value renaming `σ` identifying surviving +values across the two contexts is *not* a parameter: it is fixed to `rewriteMapping` (the identity +except on `op`'s results, which are redirected to `newValues`). This is definitionally the same +renaming as `LocalRewritePattern.mapping`, but kept independent of the concrete driver so that +`RewrittenAt` stays decoupled from it. + +`RewrittenAt` is stated abstractly — decoupled from the concrete `fromLocalRewrite` driver — so the +soundness lift (block-`B` simulation, the headline refinement theorems) can be developed against it +directly; connecting it to `fromLocalRewrite` is a deferred milestone (PR 9). + +The clauses describe the edit as the composition *insert `newOps` before `op`* → *redirect each use of +`op`'s `i`-th result to `newValues[i]`* → *erase `op`*: + +1. **created values/ops** — `newValues` has one entry per result of `op`, all in bounds of `newCtx`; + `newOps` are exactly the operations fresh to `newCtx`. +2. **`op` erased, others survive** — `op` is no longer in bounds of `newCtx`; every other operation + of `ctx` still is. +3. **block list shape** — in the block `B` containing `op`, the operation list `pre ++ [op] ++ post` + becomes `pre ++ newOps ++ post` (the operations of `post` are the same pointers, only their + operands are redirected — that redirection is recorded by `σ` in clause 4); every other block has + an identical operation list. +4. **intrinsic-data frame** — every surviving operation satisfies `CrossContextFrame` under `σ` + (op-type/properties/result-types/successors agree; operands/results map through `σ`; `op` verified + in `newCtx`) — exactly what `interpretOp_monotone`/`interpretOpList_monotone` consume. +5. **structure frame** — blocks stay in bounds (successor transport), and parent operations of + surviving operations are preserved (so `IsTopLevelFuncWithName` transports). +6. **result well-formedness** — `newCtx` is dominance-well-formed, and every member of `newOps` is + verified in `newCtx`; this discharges target progress when the source hits UB at the matched op. +-/ + +namespace Veir + +variable {OpInfo : Type} [HasOpInfo OpInfo] + +/-- +The value renaming performed by a single local rewrite: the identity on every value except the +results of `op`, which are redirected to the produced `newValues` index-by-index. + +This is definitionally the same renaming as `LocalRewritePattern.mapping`, but it is kept independent +of the concrete `LocalRewritePattern` driver so that `RewrittenAt` (and the soundness lift built on +it) stays decoupled from the driver. The in-bounds witnesses `mapResultsInBounds`/`mapNonResultsInBounds` +play the role of the driver's `Return*` obligations. +-/ +def rewriteMapping {ctx newCtx : WfIRContext OpCode} + (op : OperationPtr) (newValues : Array ValuePtr) + (mapResultsInBounds : ∀ (index : Nat), index < (op.getResults! ctx.raw).size → + newValues[index]!.InBounds newCtx.raw) + (mapNonResultsInBounds : ∀ (v : ValuePtr), v.InBounds ctx.raw → + v ∉ op.getResults! ctx.raw → v.InBounds newCtx.raw) : + ValueMapping ctx newCtx := + fun ⟨v, vInBounds⟩ => + if h : v ∈ op.getResults! ctx.raw then + ⟨newValues[(op.getResults! ctx.raw).idxOf v]!, mapResultsInBounds _ (by grind)⟩ + else + ⟨v, mapNonResultsInBounds v vInBounds h⟩ + +/-- `rewriteMapping` fixes `op`'s results onto `newValues` index-by-index: applying it to the result +array yields `newValues` exactly. This needs only that the sizes match (`newValuesSize`); the +distinctness of `op`'s result pointers (each is `⟨op, i⟩`) makes `idxOf (getResult i) = i`. -/ +theorem rewriteMapping_applyToArray_getResults {ctx newCtx : WfIRContext OpCode} + (op : OperationPtr) (newValues : Array ValuePtr) + (mapResultsInBounds : ∀ (index : Nat), index < (op.getResults! ctx.raw).size → + newValues[index]!.InBounds newCtx.raw) + (mapNonResultsInBounds : ∀ (v : ValuePtr), v.InBounds ctx.raw → + v ∉ op.getResults! ctx.raw → v.InBounds newCtx.raw) + (newValuesSize : newValues.size = op.getNumResults! ctx.raw) : + (rewriteMapping op newValues mapResultsInBounds mapNonResultsInBounds).applyToArray + (op.getResults! ctx.raw) = newValues := by + apply Array.ext + · simp [newValuesSize] + · intro i h1 h2 + have hsize : i < (op.getResults! ctx.raw).size := by grind + simp only [ValueMapping.applyToArray, Array.getElem_map, Array.getElem_attach, + rewriteMapping] + rw [dif_pos (Array.getElem_mem hsize)] + have hmem := OperationPtr.getResults!.mem_getResult (op := op) (ctx := ctx.raw) + (index := i) (by grind) + have hidx : (op.getResults! ctx.raw).idxOf (op.getResults! ctx.raw)[i] = i := by + rw [OperationPtr.getResults!.getElem_eq_getResult (by grind)] + have hge := OperationPtr.getResult_eq_of_idxOf_getResults! hmem rfl + grind + simp [hidx, getElem!_pos, h2] + +/-- +`RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn'` asserts that +`newCtx` is obtained from `ctx` by the single local rewrite that replaces `op` with `newOps` +(producing `newValues`). The renaming identifying surviving values across the two contexts is *not* a +parameter: it is the function `RewrittenAt.σ` of the instance, defined as `rewriteMapping op newValues +h.mapResultsInBounds h.mapNonResultsInBounds` (the in-bounds witnesses `mapResultsInBounds`/ +`mapNonResultsInBounds` are fields below). `block` is the block containing `op`, `pre`/`post` the +operation lists before/after `op` in `block`, and `blockIn`/`blockIn'` the in-bounds witnesses for +`block` in the source/target contexts. See the module docstring for the clauses. +-/ +structure RewrittenAt + (ctx : WfIRContext OpCode) (op : OperationPtr) + (newOps : Array OperationPtr) (newValues : Array ValuePtr) + (newCtx : WfIRContext OpCode) (opIn : op.InBounds ctx.raw) + (block : BlockPtr) (pre post : Array OperationPtr) + (blockIn : block.InBounds ctx.raw) (blockIn' : block.InBounds newCtx.raw) : Prop where + /-- The operations contained in each block. -/ + srcList : block.operationList ctx.raw ctx.wellFormed blockIn = pre ++ #[op] ++ post + tgtList : block.operationList newCtx.raw newCtx.wellFormed blockIn' = pre ++ newOps ++ post + otherBlocks : ∀ (b : BlockPtr) (bIn : b.InBounds ctx.raw) (bIn' : b.InBounds newCtx.raw), + b ≠ block → + b.operationList ctx.raw ctx.wellFormed bIn = b.operationList newCtx.raw newCtx.wellFormed bIn' + -- Clause 1: created values/ops. + /-- `newValues` carries one value per result of `op`. -/ + newValuesSize : newValues.size = op.getNumResults! ctx.raw + /-- All produced values are in bounds of the target context. -/ + newValuesInBounds : ∀ v ∈ newValues, v.InBounds newCtx.raw + /-- `newOps` are exactly the operations fresh to the target context. -/ + newOpsFresh : ∀ newOp, newOp ∈ newOps ↔ (newOp.InBounds newCtx.raw ∧ ¬ newOp.InBounds ctx.raw) + /-- In-bounds witness for the redirect branch of `σ` (`rewriteMapping`): each produced value is in + bounds of the target context. (Derivable from `newValuesSize`/`newValuesInBounds`, but kept as a + field so `σ := RewrittenAt.σ` is total without re-deriving it.) -/ + mapResultsInBounds : ∀ (index : Nat), index < (op.getResults! ctx.raw).size → + newValues[index]!.InBounds newCtx.raw + /-- In-bounds witness for the identity branch of `σ` (`rewriteMapping`): every value that is not a + result of `op` survives into the target context. -/ + mapNonResultsInBounds : ∀ (v : ValuePtr), v.InBounds ctx.raw → + v ∉ op.getResults! ctx.raw → v.InBounds newCtx.raw + -- Clause 2: `op` erased, others survive. + /-- The matched operation is erased. -/ + opErased : ¬ op.InBounds newCtx.raw + /-- Every operation other than `op` survives into the target context. -/ + survives : ∀ (o : OperationPtr), o.InBounds ctx.raw → o ≠ op → o.InBounds newCtx.raw + -- Clause 4: intrinsic-data frame for survivors. + /-- Every surviving operation carries identical intrinsic data, modulo the renaming `σ`. -/ + frame : ∀ (o : OperationPtr) (oIn : o.InBounds ctx.raw) (oIn' : o.InBounds newCtx.raw), + o ≠ op → + (rewriteMapping op newValues mapResultsInBounds mapNonResultsInBounds).PreservesOperation + o o oIn oIn' + -- Clause 5: structure frame. + /-- Blocks stay in bounds — successor-`InBounds` transport. -/ + blocksInBounds : ∀ (b : BlockPtr), b.InBounds ctx.raw → b.InBounds newCtx.raw + /-- Parent operations of surviving operations are preserved. -/ + parentOps : ∀ (o : OperationPtr), o.InBounds ctx.raw → o ≠ op → + o.getParentOp! newCtx.raw = o.getParentOp! ctx.raw + -- Clause 6: result well-formedness. + /-- The target context is dominance-well-formed. -/ + newCtxDom : newCtx.Dom + newCtxVerif : newCtx.Verified + -- Clause 7: value-renaming frame for block arguments (PR 9 / `LocalRewritePattern.mapping`). + /-- `σ` fixes every value that is not a result of `op`. `LocalRewritePattern.mapping` is the + identity except on `op`'s results (which it redirects to `newValues`), so every other value — in + particular every block argument, which is never an `op` result — is left untouched. This is what + discharges the `hFix`/`hReflectArgs` frame hypotheses of `setArgumentValues?_isRefinedBy`. -/ + mappingFixesNonResults : ∀ (v : ValuePtr) (vIn : v.InBounds ctx.raw), + v ∉ op.getResults! ctx.raw → + ((rewriteMapping op newValues mapResultsInBounds mapNonResultsInBounds) ⟨v, vIn⟩).val = v + /-- Every produced value is an operation result, never a block argument (in the concrete driver they + are results of `newOps`). Together with `mappingFixesNonResults`, this lets `σ` reflect a block + argument only from that same block argument. -/ + newValuesAreResults : ∀ v ∈ newValues, ∃ opr : OpResultPtr, v = ValuePtr.opResult opr + /-- The block-argument list of every block is preserved by the rewrite (it only edits operations). + Gives equal argument counts and argument types across the two contexts. -/ + blockArgsPreserved : ∀ (bl : BlockPtr), bl.InBounds ctx.raw → + (bl.get! newCtx.raw).arguments = (bl.get! ctx.raw).arguments + -- Clause 8: region/block structure frame (the rewrite edits only operation lists). + /-- Regions stay in bounds — region-`InBounds` transport (mirrors `blocksInBounds`). -/ + regionsInBounds : ∀ (r : RegionPtr), r.InBounds ctx.raw → r.InBounds newCtx.raw + /-- The region list of every surviving operation is preserved: the rewrite only edits the operation + list of `block`, never region structure. Gives equal region counts and equal region pointers across + the two contexts (used to relate `interpretFunction`/`interpretRegion`). -/ + opRegionsPreserved : ∀ (o : OperationPtr), o.InBounds ctx.raw → o ≠ op → + (o.get! newCtx.raw).regions = (o.get! ctx.raw).regions + /-- The first block of every region is preserved by the rewrite, so `interpretRegion` starts the + CFG walk from the same entry block in both contexts. -/ + regionFirstBlockPreserved : ∀ (r : RegionPtr), r.InBounds ctx.raw → + (r.get! newCtx.raw).firstBlock = (r.get! ctx.raw).firstBlock + -- Clause 9: the matched operation is not a function. + /-- The matched operation `op` is not a function: it does not have exactly one region. Functions + (the operations `interpretFunction` runs) have exactly one region, so this guarantees every function + operation is distinct from `op` — the rewrite matches an operation *inside* a function body, never a + function itself. This lets `interpretFunction_refinement` conclude `funcOp ≠ op` for any function it + interprets, hence that the function survives the rewrite. -/ + opNotFunction : op.getNumRegions! ctx.raw ≠ 1 + +/-! ## Structural frame lemmas -/ + +variable {ctx newCtx : WfIRContext OpCode} {op : OperationPtr} + {newOps : Array OperationPtr} {newValues : Array ValuePtr} + {opIn : op.InBounds ctx.raw} + {block : BlockPtr} {pre post : Array OperationPtr} + {blockIn : block.InBounds ctx.raw} {blockIn' : block.InBounds newCtx.raw} + +namespace RewrittenAt + +/-- The fixed renaming performed by the rewrite: `rewriteMapping` applied to the in-bounds witnesses +carried by the `RewrittenAt` instance. This is *not* a parameter of `RewrittenAt`; it is a function of +the instance (the lemmas below refer to it as `h.σ`). -/ +abbrev σ (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ValueMapping ctx newCtx := + rewriteMapping op newValues h.mapResultsInBounds h.mapNonResultsInBounds + +/-- `op` lives in `block`: derived from `srcList`, since `op` occurs in `block`'s operation list. -/ +theorem opParent (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + (op.get! ctx.raw).parent = some block := + (BlockPtr.operationList.mem opIn).mpr (by rw [h.srcList]; simp [Array.mem_append]) + +/-- Every new operation is in bounds of the target context. -/ +theorem newOpsInBounds (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ newOp ∈ newOps, newOp.InBounds newCtx.raw := + fun newOp hmem => ((h.newOpsFresh newOp).mp hmem).1 + +/-- New operations are fresh: none of them is in bounds of the source context. -/ +theorem newOps_not_inBounds_source (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ newOp ∈ newOps, ¬ newOp.InBounds ctx.raw := + fun newOp hmem => ((h.newOpsFresh newOp).mp hmem).2 + +/-- The matched operation is not among the new operations (it is erased, they are fresh). -/ +theorem op_not_mem_newOps (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + op ∉ newOps := by + intro hmem + exact (h.newOps_not_inBounds_source op hmem) opIn + +/-- A surviving operation carries the `CrossContextFrame` data — repackaged so the source/target +in-bounds proofs do not need to be supplied at the call site. -/ +theorem frame_of_ne (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {o : OperationPtr} (oIn : o.InBounds ctx.raw) (hne : o ≠ op) : + (h.σ).PreservesOperation o o oIn (h.survives o oIn hne) := + h.frame o oIn (h.survives o oIn hne) hne + +/-- Every `pre` operation is in bounds of the source context (it lies in the source block list). -/ +theorem preInBounds (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ pre.toList, o.InBounds ctx.raw := by + intro o ho + have hchain := BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed + rw [h.srcList] at hchain + exact hchain.arrayInBounds (by simp only [Array.mem_append]; grind) + +/-- Every `post` operation is in bounds of the source context (it lies in the source block list). -/ +theorem postInBounds (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ post.toList, o.InBounds ctx.raw := by + intro o ho + have hchain := BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed + rw [h.srcList] at hchain + exact hchain.arrayInBounds (by simp only [Array.mem_append]; grind) + +/-- `op` does not appear in `pre`: the source block list `pre ++ #[op] ++ post` is `Nodup`. -/ +theorem opNotMemPre (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + op ∉ pre.toList := by + have hchain := BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed + have hnodup := BlockPtr.OpChain_array_toList_Nodup hchain + rw [h.srcList] at hnodup + simp only [Array.append_assoc, Array.toList_append, List.nodup_append] at hnodup + grind + +/-- `op` does not appear in `post`: the source block list `pre ++ #[op] ++ post` is `Nodup`. -/ +theorem opNotMemPost (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + op ∉ post.toList := by + have hchain := BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed + have hnodup := BlockPtr.OpChain_array_toList_Nodup hchain + rw [h.srcList] at hnodup + simp only [Array.append_assoc, Array.toList_append, List.nodup_append] at hnodup + grind + +/-- Every `pre` operation is in bounds of the target context (it lies in the target block list). -/ +theorem preInBounds' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ pre.toList, o.InBounds newCtx.raw := by + intro o ho + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + exact hchain.arrayInBounds (by simp only [Array.mem_append]; grind) + +/-- Every new operation is in bounds of the target context, as a `List` membership statement. -/ +theorem newOpsInBounds' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ newOps.toList, o.InBounds newCtx.raw := + fun o ho => h.newOpsInBounds o (by simpa using ho) + +/-- Every `post` operation is in bounds of the target context (it lies in the target block list). -/ +theorem postInBounds' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ post.toList, o.InBounds newCtx.raw := by + intro o ho + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + exact hchain.arrayInBounds (by simp only [Array.mem_append]; grind) + +/-- Every `pre` operation has `block` as its parent in the source context (it lies in the source +block list). -/ +theorem preParent (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ pre.toList, ∃ block, (o.get! ctx.raw).parent = some block := by + intro o ho + have hchain := BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed + rw [h.srcList] at hchain + exact ⟨block, hchain.opParent (by simp only [Array.mem_append]; grind)⟩ + +/-- Every `pre` operation has `block` as its parent in the target context (it lies in the target +block list). -/ +theorem preParent' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ pre.toList, ∃ block, (o.get! newCtx.raw).parent = some block := by + intro o ho + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + exact ⟨block, hchain.opParent (by simp only [Array.mem_append]; grind)⟩ + +/-- Every new operation has `block` as its parent in the target context (it lies in the target +block list). -/ +theorem newOpsParent' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ newOps.toList, ∃ block, (o.get! newCtx.raw).parent = some block := by + intro o ho + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + exact ⟨block, hchain.opParent (by simp only [Array.mem_append]; grind)⟩ + +/-- Every `post` operation has `block` as its parent in the target context (it lies in the target +block list). -/ +theorem postParent' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ post.toList, ∃ block, (o.get! newCtx.raw).parent = some block := by + intro o ho + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + exact ⟨block, hchain.opParent (by simp only [Array.mem_append]; grind)⟩ + +/-- Every `pre` operation has `block` as its parent in the source context. -/ +theorem preParentEq (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ pre.toList, (o.get! ctx.raw).parent = some block := by + intro o ho + have hchain := BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed + rw [h.srcList] at hchain + exact hchain.opParent (by simp only [Array.mem_append]; grind) + +/-- Every `pre` operation has `block` as its parent in the target context. -/ +theorem preParentEq' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ pre.toList, (o.get! newCtx.raw).parent = some block := by + intro o ho + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + exact hchain.opParent (by simp only [Array.mem_append]; grind) + +/-- Every new operation has `block` as its parent in the target context. -/ +theorem newOpsParentEq' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ newOps.toList, (o.get! newCtx.raw).parent = some block := by + intro o ho + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + exact hchain.opParent (by simp only [Array.mem_append]; grind) + +/-- Every `post` operation has `block` as its parent in the source context. -/ +theorem postParentEq (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ post.toList, (o.get! ctx.raw).parent = some block := by + intro o ho + have hchain := BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed + rw [h.srcList] at hchain + exact hchain.opParent (by simp only [Array.mem_append]; grind) + +/-- Every `post` operation has `block` as its parent in the target context. -/ +theorem postParentEq' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ o ∈ post.toList, (o.get! newCtx.raw).parent = some block := by + intro o ho + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + exact hchain.opParent (by simp only [Array.mem_append]; grind) + +/-! ### Block-argument frame consequences (clause 7) -/ + +/-- The number of arguments of any in-bounds block is preserved by the rewrite. -/ +theorem numArgsEq (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) : + bl.getNumArguments! newCtx.raw = bl.getNumArguments! ctx.raw := by + simp only [BlockPtr.getNumArguments!, h.blockArgsPreserved bl blIn] + +/-- The type of any block argument is preserved by the rewrite. -/ +theorem argType_eq (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) (i : Nat) : + (bl.getArgument i : ValuePtr).getType! newCtx.raw = (bl.getArgument i : ValuePtr).getType! ctx.raw := by + simp only [ValuePtr.getType!_blockArgument, BlockArgumentPtr.get!, BlockPtr.getArgument_block, + BlockPtr.getArgument_index, h.blockArgsPreserved bl blIn] + +/-- A block argument is never a result of `op` (distinct `ValuePtr` constructors). -/ +theorem blockArg_notMem_getResults + (_h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {bl : BlockPtr} {i : Nat} : + (bl.getArgument i : ValuePtr) ∉ op.getResults! ctx.raw := by + simp only [OperationPtr.getResults!.mem_iff_exists_index] + rintro ⟨index, _, heq⟩ + simp [BlockPtr.getArgument, OperationPtr.getResult_def] at heq + +/-- `σ` fixes block-argument pointers: it maps `bl.getArgument i` to itself. Discharges the `hFix` +hypothesis of `setArgumentValues?_isRefinedBy`. -/ +theorem mappingFixesArg + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {bl : BlockPtr} {i : Nat} (vIn : (bl.getArgument i : ValuePtr).InBounds ctx.raw) : + (h.σ ⟨(bl.getArgument i : ValuePtr), vIn⟩).val = (bl.getArgument i : ValuePtr) := + h.mappingFixesNonResults _ vIn h.blockArg_notMem_getResults + +/-- `σ` fixes a block's argument array: applying it to `bl`'s source arguments yields the same +arguments in the target context. Discharges the `hArgs` hypothesis of +`setArgumentValues?_isRefinedBy`. -/ +theorem argsApplyToArray + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) : + bl.getArguments! newCtx.raw = h.σ.applyToArray (bl.getArguments! ctx.raw) := by + apply Array.ext + · simp [h.numArgsEq blIn] + · intro i h1 _h2 + simp only [BlockPtr.getArguments!.size_eq_getNumArguments!] at h1 + have hi : i < bl.getNumArguments! ctx.raw := h.numArgsEq blIn ▸ h1 + simp only [ValueMapping.applyToArray, Array.getElem_map, Array.getElem_attach, + BlockPtr.getArguments!.getElem_eq_getArgument h1, + BlockPtr.getArguments!.getElem_eq_getArgument hi] + exact (h.mappingFixesArg (i := i) (bl := bl) (by grind)).symm + +/-- A result of `op` is mapped by `σ` into `newValues`. -/ +theorem mapping_getResult_mem_newValues + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {val : ValuePtr} (valIn : val.InBounds ctx.raw) (hMem : val ∈ op.getResults! ctx.raw) : + (h.σ ⟨val, valIn⟩).val ∈ newValues := by + have hx : (h.σ ⟨val, valIn⟩).val ∈ (h.σ).applyToArray (op.getResults! ctx.raw) := by + simp only [ValueMapping.applyToArray, Array.mem_map, Array.mem_attach, true_and] + exact ⟨⟨val, hMem⟩, rfl⟩ + rw [rewriteMapping_applyToArray_getResults op newValues h.mapResultsInBounds + h.mapNonResultsInBounds h.newValuesSize] at hx + exact hx + +/-- `σ` reflects block-argument pointers: the only value it maps onto `bl.getArgument i` is +`bl.getArgument i` itself (results map into `newValues`, which are never block arguments). Discharges +the `hReflectArgs` hypothesis of `setArgumentValues?_isRefinedBy`. -/ +theorem mappingReflectsArg + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {bl : BlockPtr} (val : ValuePtr) (valIn : val.InBounds ctx.raw) (i : Nat) + (heq : (h.σ ⟨val, valIn⟩).val = (bl.getArgument i : ValuePtr)) : + val = (bl.getArgument i : ValuePtr) := by + by_cases hMem : val ∈ op.getResults! ctx.raw + · exfalso + have hmem := h.mapping_getResult_mem_newValues valIn hMem + rw [heq] at hmem + obtain ⟨opr, hopr⟩ := h.newValuesAreResults _ hmem + simp [BlockPtr.getArgument] at hopr + · rw [h.mappingFixesNonResults val valIn hMem] at heq + exact heq + +/-- Every new operation is verified in the target context. Derived from `newCtxVerif`: the target +context is verified, so every in-bounds operation (in particular every `newOp`) satisfies its local +invariants. -/ +theorem newOpsVerif + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (o : OperationPtr) (oIn : o.InBounds newCtx.raw) (_ : o ∈ newOps) : + o.Verified newCtx oIn := + OperationPtr.satisfyInvariants_of_IRContext_satisfyOpInvariants h.newCtxVerif oIn + +/-- The number of regions of a surviving operation is preserved by the rewrite. -/ +theorem getNumRegions_eq + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {o : OperationPtr} (oIn : o.InBounds ctx.raw) (hne : o ≠ op) : + o.getNumRegions newCtx.raw (h.survives o oIn hne) = o.getNumRegions ctx.raw oIn := by + simp only [OperationPtr.getNumRegions, ← OperationPtr.get!_eq_get, h.opRegionsPreserved o oIn hne] + +/-- The `i`-th region pointer of a surviving operation is preserved by the rewrite. -/ +theorem getRegion_eq + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {o : OperationPtr} (oIn : o.InBounds ctx.raw) (hne : o ≠ op) + (i : Nat) (hi : i < o.getNumRegions ctx.raw oIn) : + o.getRegion newCtx.raw i (h.survives o oIn hne) (by rw [h.getNumRegions_eq oIn hne]; exact hi) + = o.getRegion ctx.raw i oIn hi := by + simp only [OperationPtr.getRegion, ← OperationPtr.get!_eq_get, h.opRegionsPreserved o oIn hne] + +end RewrittenAt + +/-! ## Block-`B` simulation (PR 7) + +The simulation for the rewritten block `B`: interpreting `B`'s operation list `pre ++ [op] ++ post` +in the source context is refined by interpreting `pre ++ newOps ++ post` in the target context, under +the rewrite renaming `σ`. + +The proof (below) splits the list with `interpretTerminatedOpList_append` and dispatches on the source +outcome at the matched operation `op`: + +* **`pre` (identical list)** — cross-context monotonicity `interpretOpList_monotone` (PR 3), fed the + `CrossContextFrame`s of clause 4; this also threads `EquationLemmaAt`/`DefinesDominating` to `op`. +* **`[op]` vs `newOps`** — the local op-step simulation `hOpSim`, which PR 8 discharges from + `PreservesSemantics` (bridging its create-only context to the inserted/erased `newCtx` via clause 4). +* **`post` (same pointers, redirected operands)** — `interpretOpList_mono` again, now under `σ`, + seeded by the post-`op` state from the previous regime. +* **source `.ub` at `op`** — a source `ub` refines any target outcome, so no target-progress argument + is needed: cross-context monotonicity (`interpretOpList_mono`) discharges this regime directly. + +The `hOpSim` hypothesis is the local op→`newOps` simulation, stated so PR 8 can supply it: refined, +SSA-valid entry states map a source `interpretOp op` step onto a target `interpretOpList newOps` run, +preserving `σ`-refinement and refining the (optional) control-flow action. +-/ + +/-- Membership/in-bounds for the source block list `pre ++ [op] ++ post`. -/ +theorem sourceListIn {ctx : WfIRContext OpCode} {op : OperationPtr} {pre post : Array OperationPtr} + (opIn : op.InBounds ctx.raw) + (preIn : ∀ o ∈ pre.toList, o.InBounds ctx.raw) (postIn : ∀ o ∈ post.toList, o.InBounds ctx.raw) : + ∀ o ∈ pre.toList ++ [op] ++ post.toList, o.InBounds ctx.raw := by + intro o ho + simp only [List.mem_append, List.mem_singleton] at ho + rcases ho with (h | h) | h + · exact preIn o h + · exact h ▸ opIn + · exact postIn o h + +/-- Membership/in-bounds for the target block list `pre ++ newOps ++ post`. -/ +theorem targetListIn {newCtx : WfIRContext OpCode} {pre newOps post : Array OperationPtr} + (preIn' : ∀ o ∈ pre.toList, o.InBounds newCtx.raw) + (newOpsIn' : ∀ o ∈ newOps.toList, o.InBounds newCtx.raw) + (postIn' : ∀ o ∈ post.toList, o.InBounds newCtx.raw) : + ∀ o ∈ pre.toList ++ newOps.toList ++ post.toList, o.InBounds newCtx.raw := by + intro o ho + simp only [List.mem_append] at ho + rcases ho with (h | h) | h + · exact preIn' o h + · exact newOpsIn' o h + · exact postIn' o h + +/-- +The local op-step simulation consumed by the block-`B` proof: under `σ`-refined, SSA-valid entry +states, the source `interpretOp op` step is refined by the target `interpretOpList newOps` run, +preserving `σ`-refinement of the states and refining the optional control-flow action. + +PR 8 discharges this from `LocalRewritePattern.PreservesSemantics` (with `σ = LocalRewritePattern.mapping`), +bridging the create-only context where `PreservesSemantics` is stated to the inserted/erased `newCtx`. +-/ +def OpStepSimulation + {ctx newCtx : WfIRContext OpCode} (op : OperationPtr) (newOps : Array OperationPtr) + (μ : ValueMapping ctx newCtx) (opIn : op.InBounds ctx.raw) + (newOpsIn' : ∀ o ∈ newOps.toList, o.InBounds newCtx.raw) : Prop := + ∀ (s : InterpreterState ctx) (s' : InterpreterState newCtx), + s.isRefinedBy s' μ → + s.EquationLemmaAt (InsertPoint.before op) → + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) + (r₂ : InterpreterState newCtx × Option ControlFlowAction) => + r₁.1.isRefinedBy r₂.1 μ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + (interpretOp op s) + (interpretOpList newOps.toList s' newOpsIn') + +/-- A prefix of an operation chain slice is itself an operation chain slice (the boundary `.next`-link +to the dropped suffix is the only condition lost, and it is not required for the prefix alone). -/ +theorem BlockPtr.OpChainSlice.append_left {ctx : IRContext OpInfo} {block : BlockPtr} : + ∀ {a b : List OperationPtr}, block.OpChainSlice ctx (a ++ b) → + block.OpChainSlice ctx a := by + intro a + induction a with + | nil => intro b _; exact BlockPtr.OpChainSlice.nil + | cons x xs ih => + intro b h + simp only [List.cons_append, BlockPtr.OpChainSlice.cons_iff] at h ⊢ + obtain ⟨hin, hparent, hnext, htail⟩ := h + exact ⟨hin, hparent, fun c hc => hnext c (by cases xs <;> simp_all), ih htail⟩ + +/-- A suffix of an operation chain slice is itself an operation chain slice (dropping a prefix only +loses the boundary `.next`-link into the suffix, which the suffix alone does not require). -/ +theorem BlockPtr.OpChainSlice.append_right {ctx : IRContext OpInfo} {block : BlockPtr} : + ∀ {a b : List OperationPtr}, block.OpChainSlice ctx (a ++ b) → + block.OpChainSlice ctx b := by + intro a + induction a with + | nil => intro b h; simpa using h + | cons x xs ih => + intro b h + rw [List.cons_append, BlockPtr.OpChainSlice.cons_iff] at h + exact ih h.2.2.2 + +/-- The trailing `.next`-link of an operation chain slice `l ++ [x]`: the last operation of `l` points +to `x`. -/ +theorem BlockPtr.OpChainSlice.getLast?_next_eq {ctx : IRContext OpInfo} {block : BlockPtr} + {l : List OperationPtr} {x lastOp : OperationPtr} + (h : block.OpChainSlice ctx (l ++ [x])) (hl : l.getLast? = some lastOp) : + (lastOp.get! ctx).next = some x := by + induction l with + | nil => simp at hl + | cons a t ih => + rw [List.cons_append, BlockPtr.OpChainSlice.cons_iff] at h + obtain ⟨_, _, hnext, htail⟩ := h + cases t with + | nil => + obtain rfl : a = lastOp := by simpa using hl + exact hnext x (by simp) + | cons b t' => exact ih htail (by simpa using hl) + +/-- An operation chain slice characterized by its per-element data: every operation is in bounds, has +the expected parent, and links by `.next` to its successor in the list. -/ +theorem BlockPtr.OpChainSlice.of_getElem {ctx : IRContext OpInfo} {block : BlockPtr} + {l : List OperationPtr} + (hin : ∀ o ∈ l, o.InBounds ctx) + (hparent : ∀ o ∈ l, (o.get! ctx).parent = some block) + (hnext : ∀ (i : Nat) (hi : i + 1 < l.length), + ((l[i]'(Nat.lt_of_succ_lt hi)).get! ctx).next = some (l[i + 1]'hi)) : + block.OpChainSlice ctx l := by + induction l with + | nil => exact BlockPtr.OpChainSlice.nil + | cons a l ih => + rw [BlockPtr.OpChainSlice.cons_iff] + refine ⟨hin a (by simp), hparent a (by simp), fun b hb => ?_, + ih (fun o ho => hin o (List.mem_cons_of_mem a ho)) + (fun o ho => hparent o (List.mem_cons_of_mem a ho)) (fun i hi => ?_)⟩ + · cases l with + | nil => simp at hb + | cons a' l' => + simp only [List.head?_cons, Option.some.injEq] at hb + subst hb + simpa using hnext 0 (by simp only [List.length_cons]; omega) + · have hh := hnext (i + 1) (by simp only [List.length_cons]; omega) + simp only [List.getElem_cons_succ] at hh + exact hh + +/-- The operations of a block (its `OpChain` array) form an operation chain slice of that block. -/ +theorem BlockPtr.OpChain.opChainSlice {ctx : WfIRContext OpCode} {block : BlockPtr} + {array : Array OperationPtr} (h : BlockPtr.OpChain block ctx.raw array) : + block.OpChainSlice ctx.raw array.toList := by + apply BlockPtr.OpChainSlice.of_getElem + · intro o ho; exact h.arrayInBounds (by simpa using ho) + · intro o ho; exact h.opParent (by simpa using ho) + · intro i hi + have hsize : i + 1 < array.size := by simpa using hi + have hnext := h.next (i := i) (Nat.lt_of_succ_lt hsize) + rw [Array.getElem?_eq_getElem hsize] at hnext + simpa using hnext + +/-- The source block list `pre ++ [op] ++ post` is an operation chain slice of `block`. -/ +theorem RewrittenAt.srcChain + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + block.OpChainSlice ctx.raw (pre.toList ++ [op] ++ post.toList) := by + have hchain := BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed + rw [h.srcList] at hchain + simpa using hchain.opChainSlice + +/-- The target block list `pre ++ newOps ++ post` is an operation chain slice of `block`. -/ +theorem RewrittenAt.tgtChain + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + block.OpChainSlice newCtx.raw (pre.toList ++ newOps.toList ++ post.toList) := by + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + simpa using hchain.opChainSlice + +/-- The first operation of `block` in the target context is the head of the target block list +`pre ++ newOps ++ post` (the block's op chain via `tgtList`/`OpChain.first`). -/ +theorem RewrittenAt.tgtFirstOp + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + (block.get! newCtx.raw).firstOp = (pre.toList ++ newOps.toList ++ post.toList).head? := by + have hchain := BlockPtr.operationListWF newCtx.raw block blockIn' newCtx.wellFormed + rw [h.tgtList] at hchain + rw [hchain.first] + simp [List.head?_eq_getElem?, ← Array.toList_append] + +/-- The first operation of `block` in the source context is the head of the source block list +`pre ++ [op] ++ post` (the block's op chain via `srcList`/`OpChain.first`). -/ +theorem RewrittenAt.srcFirstOp + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + (block.get! ctx.raw).firstOp = (pre.toList ++ [op] ++ post.toList).head? := by + have hchain := BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed + rw [h.srcList] at hchain + rw [hchain.first] + simp [List.head?_eq_getElem?, ← Array.getElem?_toList] + +/-- The source prefix `pre ++ [op]` is an operation chain slice of `block`. -/ +theorem RewrittenAt.preChainOp + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + block.OpChainSlice ctx.raw (pre.toList ++ [op]) := + h.srcChain.append_left + +/-- The target `pre` segment is an operation chain slice of `block`. -/ +theorem RewrittenAt.preChain' + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + block.OpChainSlice newCtx.raw pre.toList := by + have hc := h.tgtChain + rw [List.append_assoc] at hc + exact hc.append_left + +/-- The target `post` segment is an operation chain slice of `block`. -/ +theorem RewrittenAt.postChain' + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + block.OpChainSlice newCtx.raw post.toList := + h.tgtChain.append_right + + +/-- Bridge `interpretOpList_equationLemmaAt` to the *before-`nextOp`* framing used by the block +simulation: given an operation chain slice `ops ++ [nextOp]` and a run of `ops` that completes without +a terminator, the resulting state satisfies the SSA invariant at the point *before* `nextOp` — the +operation that follows the run in the chain. (`interpretOpList_equationLemmaAt` concludes at the point +*after* the last run operation; the chain link `getLast?_next_eq` identifies that point with +`before nextOp`.) -/ +theorem interpretOpList_equationLemmaAt_before {ctx : WfIRContext OpCode} + {block : BlockPtr} {ops : List OperationPtr} {nextOp : OperationPtr} + {state state' : InterpreterState ctx} + (ctxDom : ctx.Dom) + (opsIn : ∀ op ∈ ops, op.InBounds ctx.raw) + (nextOpIn : nextOp.InBounds ctx.raw) + (hChain : block.OpChainSlice ctx.raw (ops ++ [nextOp])) + (stateWf : ∀ fst (h : fst ∈ ops ++ [nextOp]), (ops ++ [nextOp]).head? = some fst → + state.EquationLemmaAt (.before fst) + (by simp only [List.mem_append, List.mem_singleton] at h + rcases h with h | h + · exact opsIn fst h + · exact h ▸ nextOpIn)) + (hrun : interpretOpList ops state opsIn = some (.ok (state', none))) : + state'.EquationLemmaAt (.before nextOp) nextOpIn := by + cases ops with + | nil => + rw [interpretOpList_nil] at hrun + obtain rfl : state = state' := by grind + exact stateWf nextOp (by simp) (by simp) + | cons a l => + obtain ⟨lastOp, hlast⟩ : ∃ lastOp, (a :: l).getLast? = some lastOp := by + cases hg : (a :: l).getLast? with + | none => simp at hg + | some x => exact ⟨x, rfl⟩ + have hEq := interpretOpList_equationLemmaAt ctxDom hChain.append_left (by simp) + (stateWf a (by simp) (by simp)) hlast hrun + simp only [InsertPoint.after_eq_of_some_next (hChain.getLast?_next_eq hlast)] at hEq + exact hEq + +/-- Bridge `interpretOpList_DefinesDominating` to the *before-`nextOp`* framing: given an operation +chain slice `ops ++ [nextOp]` and a run of `ops` that completes without a terminator, the resulting +state defines every value dominating the point *before* `nextOp`. -/ +theorem interpretOpList_DefinesDominating_before {ctx : WfIRContext OpCode} + {block : BlockPtr} {ops : List OperationPtr} {nextOp : OperationPtr} + {state state' : InterpreterState ctx} + (ctxDom : ctx.Dom) + (opsIn : ∀ op ∈ ops, op.InBounds ctx.raw) + (nextOpIn : nextOp.InBounds ctx.raw) + (hChain : block.OpChainSlice ctx.raw (ops ++ [nextOp])) + (stateDom : ∀ fst (h : fst ∈ ops ++ [nextOp]), (ops ++ [nextOp]).head? = some fst → + state.DefinesDominating (.before fst) + (by simp only [List.mem_append, List.mem_singleton] at h + rcases h with h | h + · exact opsIn fst h + · exact h ▸ nextOpIn)) + (hrun : interpretOpList ops state opsIn = some (.ok (state', none))) : + state'.DefinesDominating (.before nextOp) nextOpIn := by + cases ops with + | nil => + rw [interpretOpList_nil] at hrun + obtain rfl : state = state' := by grind + exact stateDom nextOp (by simp) (by simp) + | cons a l => + obtain ⟨lastOp, hlast⟩ : ∃ lastOp, (a :: l).getLast? = some lastOp := by + cases hg : (a :: l).getLast? with + | none => simp at hg + | some x => exact ⟨x, rfl⟩ + have hDom := interpretOpList_DefinesDominating ctxDom hChain.append_left (by simp) + (stateDom a (by simp) (by simp)) hlast hrun + simp only [InsertPoint.after_eq_of_some_next (hChain.getLast?_next_eq hlast)] at hDom + exact hDom + +/-- Interpreting a singleton operation list is interpreting the operation. -/ +theorem interpretOpList_singleton {ctx : WfIRContext OpCode} {op : OperationPtr} + {state : InterpreterState ctx} (h : ∀ o ∈ [op], o.InBounds ctx.raw) : + interpretOpList [op] state h = interpretOp op state (h op (by simp)) := by + rw [interpretOpList_cons] + rcases interpretOp op state (h op (by simp)) with _ | (⟨s, a⟩ | _) + · rfl + · cases a <;> simp [interpretOpList_nil] + · rfl + +/-- +Sequential composition of cross-context refinement over `interpretOpList`. If interpreting the +prefix `l₁`/`m₁` refines (`hPrefix`), and — whenever the prefixes both run to completion without a +terminator into `σ`-refined states — interpreting the suffixes `l₂`/`m₂` refines (`hCont`), then +interpreting `l₁ ++ l₂` refines `m₁ ++ m₂`. The target must make progress under a UB source +(`hTgtProgress`), which the caller discharges from the verified, SSA-well-formed target chain. +-/ +theorem isRefinedBy_interpretOpList_seqCompose + {ctx ctx' : WfIRContext OpCode} {μ : ValueMapping ctx ctx'} + {l₁ l₂ m₁ m₂ : List OperationPtr} + {s : InterpreterState ctx} {s' : InterpreterState ctx'} + (hl : ∀ o ∈ l₁ ++ l₂, o.InBounds ctx.raw) + (hm : ∀ o ∈ m₁ ++ m₂, o.InBounds ctx'.raw) + (hPrefix : Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) + (r₂ : InterpreterState ctx' × Option ControlFlowAction) => + r₁.1.isRefinedBy r₂.1 μ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + (interpretOpList l₁ s (fun o ho => hl o (List.mem_append_left _ ho))) + (interpretOpList m₁ s' (fun o ho => hm o (List.mem_append_left _ ho)))) + (hCont : ∀ (s2 : InterpreterState ctx) (s2' : InterpreterState ctx'), + s2.isRefinedBy s2' μ → + interpretOpList l₁ s (fun o ho => hl o (List.mem_append_left _ ho)) = some (.ok (s2, none)) → + interpretOpList m₁ s' (fun o ho => hm o (List.mem_append_left _ ho)) = some (.ok (s2', none)) → + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) + (r₂ : InterpreterState ctx' × Option ControlFlowAction) => + r₁.1.isRefinedBy r₂.1 μ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + (interpretOpList l₂ s2 (fun o ho => hl o (List.mem_append_right _ ho))) + (interpretOpList m₂ s2' (fun o ho => hm o (List.mem_append_right _ ho)))) : + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) + (r₂ : InterpreterState ctx' × Option ControlFlowAction) => + r₁.1.isRefinedBy r₂.1 μ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + (interpretOpList (l₁ ++ l₂) s hl) + (interpretOpList (m₁ ++ m₂) s' hm) := by + rw [interpretOpList_append, interpretOpList_append] + rcases hsrc : interpretOpList l₁ s (fun o ho => hl o (List.mem_append_left _ ho)) with + _ | (⟨s2, a⟩ | _) + · simp [Interp.isRefinedBy] + · rw [hsrc] at hPrefix + simp only [Interp.isRefinedBy_ok_target_iff] at hPrefix + obtain ⟨⟨s2', a'⟩, htgt, hsRef, haRef⟩ := hPrefix + rw [htgt] + cases a with + | none => + have ha' : a' = none := by cases a' <;> simp_all [ControlFlowAction.optionIsRefinedBy] + subst ha' + exact hCont s2 s2' hsRef hsrc htgt + | some cf => + obtain ⟨cf', ha', hcf⟩ : ∃ cf', a' = some cf' ∧ cf.isRefinedBy cf' := by + cases a' <;> simp_all [ControlFlowAction.optionIsRefinedBy] + subst ha' + exact ⟨hsRef, hcf⟩ + · simp + +/-- +**Block-`B` simulation.** Interpreting the source block list `pre ++ [op] ++ post` is refined by +interpreting the target block list `pre ++ newOps ++ post` (the `post` operations are the same +pointers — their operands are redirected, recorded by `σ` in clause 4). + +Stated over the op-lists directly; PR 8 obtains the lists and the source/target chain-and-parent +structure from `RewrittenAt.srcList`/`tgtList` (clause 3), the source-entry `EquationLemmaAt`/target +`DefinesDominating` invariants from the per-block invariant `I`, and supplies `hOpSim` from +`PreservesSemantics`. + +The proof composes the three regimes via `isRefinedBy_interpretOpList_seqCompose`: +* **`pre` (identical list)** — cross-context monotonicity `hPre` (PR 3), fed the clause-4 frames. +* **`[op]` vs `newOps`** — the local op-step simulation `hOpSim`, after threading the SSA invariant + `EquationLemmaAt` from block entry through `pre` to `op` (`interpretOpList_equationLemmaAt`). +* **`post` (same pointers, redirected operands)** — cross-context monotonicity `hPost` (PR 3) under + `σ`, applicable from any pair of `σ`-refined post-`op` states. + +No target-progress argument is needed: a source `ub` refines any target outcome, so cross-context +monotonicity (`interpretOpList_mono`) and `isRefinedBy_interpretOpList_seqCompose` discharge the +source-UB case directly. The target-side `DefinesDominating` invariant is therefore unused by the +proof, but kept in the signature (`_hDefDom'`) for the Stage D / PR 8 contract. +-/ +theorem RewrittenAt.blockSimulation + (hRW : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (newCtxVerif : newCtx.Verified) + {state : InterpreterState ctx} {state' : InterpreterState newCtx} + (hState : state.isRefinedBy state' hRW.σ) + -- Source dominance well-formedness (PR 8 supplies it from the function-level `ctx.Dom` assumption). + (hCtxDom : ctx.Dom) + -- Source-side SSA invariant at block entry: threads `EquationLemmaAt` up to `op` (PR 8 supplies + -- it from invariant `I`; the source `pre`-chain is derived via `RewrittenAt.preChainOp`). + (hEqLem : ∀ fst (hfst : (block.get! ctx.raw).firstOp = some fst), + state.EquationLemmaAt (.before fst)) + (hOpSim : OpStepSimulation op newOps hRW.σ opIn hRW.newOpsInBounds') : + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × ControlFlowAction) + (r₂ : InterpreterState newCtx × ControlFlowAction) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2.isRefinedBy r₂.2) + (interpretTerminatedOpList (pre.toList ++ [op] ++ post.toList) state + (sourceListIn opIn hRW.preInBounds hRW.postInBounds)) + (interpretTerminatedOpList (pre.toList ++ newOps.toList ++ post.toList) state' + (targetListIn hRW.preInBounds' hRW.newOpsInBounds' hRW.postInBounds')) := by + -- In-bounds witnesses for `pre`/`post`/`newOps`, derived from the block lists `hRW.srcList` / + -- `hRW.tgtList` (membership in an `operationList` implies in-bounds). + have preIn := hRW.preInBounds + have postIn := hRW.postInBounds + have preIn' := hRW.preInBounds' + have newOpsIn' := hRW.newOpsInBounds' + have postIn' := hRW.postInBounds' + -- Regime 1 (`pre`, identical list): cross-context monotonicity (PR 3), fed the + -- `CrossContextFrame`s of clause 4 (`hRW.frame_of_ne`, since every `pre` op survives and `≠ op`). + -- Threads `σ`-refinement and (in the full proof) the `EquationLemmaAt`/`DefinesDominating` + -- invariants from block entry up to `op`. + have hPre : Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) + (r₂ : InterpreterState newCtx × Option ControlFlowAction) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + (interpretOpList pre.toList state preIn) + (interpretOpList pre.toList state' preIn') := by + exact interpretOpList_mono newCtxVerif preIn preIn' hState + (fun o h => hRW.frame o (preIn o h) (preIn' o h) (fun heq => hRW.opNotMemPre (heq ▸ h))) + -- Regime 3 (`post`, same pointers / redirected operands): cross-context monotonicity (PR 3) + -- under `σ`, applicable from any pair of `σ`-refined post-`op` states. Each `post` op survives + -- and `≠ op`, so clause 4 supplies its frame under `σ` (operands redirected through `σ`). + have hPost : ∀ (s : InterpreterState ctx) (s' : InterpreterState newCtx), + s.isRefinedBy s' hRW.σ → + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) + (r₂ : InterpreterState newCtx × Option ControlFlowAction) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + (interpretOpList post.toList s postIn) + (interpretOpList post.toList s' postIn') := by + intro s s' hs + exact interpretOpList_mono newCtxVerif postIn postIn' hs + (fun o h => hRW.frame o (postIn o h) (postIn' o h) (fun heq => hRW.opNotMemPost (heq ▸ h))) + -- Assembly. Prove the op-list refinement over `pre ++ [op] ++ post` vs `pre ++ newOps ++ post` by + -- two nested `isRefinedBy_interpretOpList_seqCompose` applications (inner: `pre`/`[op]` vs + -- `pre`/`newOps`; outer: append `post`), threading `EquationLemmaAt` (source, for `hOpSim`) and + -- `DefinesDominating` (target, for `hPost`); the UB-source obligation is the verified target run's + -- progress. Then convert to the terminated lists, dispatching the source-UB case via `hPostTerm`. + -- In-bounds helpers for the intermediate prefixes `pre ++ [op]` (source) and `pre ++ newOps` + -- (target). + have hpreOpIn : ∀ o ∈ pre.toList ++ [op], o.InBounds ctx.raw := by + intro o ho; simp only [List.mem_append, List.mem_singleton] at ho + rcases ho with h | h + · exact preIn o h + · exact h ▸ opIn + have hpreNewIn' : ∀ o ∈ pre.toList ++ newOps.toList, o.InBounds newCtx.raw := by + intro o ho; simp only [List.mem_append] at ho + rcases ho with h | h + · exact preIn' o h + · exact newOpsIn' o h + -- Inner composition: `pre ++ [op]` (source) refined by `pre ++ newOps` (target). + have hInner := isRefinedBy_interpretOpList_seqCompose + (l₁ := pre.toList) (l₂ := [op]) (m₁ := pre.toList) (m₂ := newOps.toList) + hpreOpIn hpreNewIn' hPre + (fun s1 s1' hs1Ref hrunSrc hrunTgt => by + -- Thread the SSA invariant `EquationLemmaAt` from block entry through `pre` to `op`. + have hEq1 : s1.EquationLemmaAt (.before op) opIn := + interpretOpList_equationLemmaAt_before hCtxDom preIn opIn hRW.preChainOp + (fun fst _ hhead => hEqLem fst + (by rw [hRW.srcFirstOp]; simp only [List.head?_append, hhead, Option.some_or])) + hrunSrc + rw [interpretOpList_singleton] + exact hOpSim s1 s1' hs1Ref hEq1) + -- Outer composition: append `post` on both sides. + have hFull := isRefinedBy_interpretOpList_seqCompose + (l₁ := pre.toList ++ [op]) (l₂ := post.toList) + (m₁ := pre.toList ++ newOps.toList) (m₂ := post.toList) + (sourceListIn opIn preIn postIn) (targetListIn preIn' newOpsIn' postIn') + hInner + (fun s2 s2' hs2Ref hrunSrc hrunTgt => hPost s2 s2' hs2Ref) + -- Convert the op-list refinement to the terminated-list refinement: the source-UB case is + -- refined by any target outcome (monotonicity now discharges target progress internally). + simp only [interpretTerminatedOpList, bind] + rcases hs : interpretOpList (pre.toList ++ [op] ++ post.toList) state + (sourceListIn opIn preIn postIn) with _ | (⟨sf, act⟩ | _) + · simp [Interp.isRefinedBy] + · rw [hs] at hFull + simp only [Interp.isRefinedBy_ok_target_iff] at hFull + obtain ⟨⟨sf', act'⟩, htgt, hsRef, hactRef⟩ := hFull + rw [htgt] + cases act with + | none => simp [Interp.isRefinedBy] + | some cf => + obtain ⟨cf', hact', hcfRef⟩ : ∃ cf', act' = some cf' ∧ cf.isRefinedBy cf' := by + cases act' <;> simp_all [ControlFlowAction.optionIsRefinedBy] + subst hact' + exact ⟨hsRef, hcfRef⟩ + · -- Source-UB case: a source `ub` is refined by any target outcome. + simp [Interp.isRefinedBy] + +/-- Bridge `interpretBlock` to a `setArgumentValues?` followed by `interpretTerminatedOpList` over the +block's operation list. When the block is empty (`firstOp = none`) the operation list is empty and both +sides are `none`; otherwise `interpretOpChain` from the first operation is the terminated run of the +list. -/ +theorem interpretBlock_eq_setArgumentValues?_interpretTerminatedOpList + {ctx : WfIRContext OpCode} {b : BlockPtr} (bIn : b.InBounds ctx.raw) + (values : Array RuntimeValue) (state : InterpreterState ctx) : + interpretBlock b values state bIn = + (state.variables.setArgumentValues? b values bIn).bind (fun newVars => + interpretTerminatedOpList (b.operationList ctx.raw ctx.wellFormed bIn).toList + ⟨newVars, state.memory⟩ (by grind [BlockPtr.operationListWF, BlockPtr.OpChain])) := by + simp only [interpretBlock, liftM, monadLift, MonadLift.monadLift, bind] + rcases hsa : state.variables.setArgumentValues? b values bIn with _ | newVars + · simp + · simp only [Option.bind_some] + have hchain := BlockPtr.operationListWF ctx.raw b bIn ctx.wellFormed + split + · -- Empty block: the operation list has size 0. + next h => + have hfirst := hchain.first + rw [BlockPtr.get!_eq_get bIn, h] at hfirst + have hsize : (b.operationList ctx.raw ctx.wellFormed bIn).size = 0 := by + rcases Nat.eq_zero_or_pos (b.operationList ctx.raw ctx.wellFormed bIn).size with h0 | h0 + · exact h0 + · rw [Array.getElem?_eq_getElem h0] at hfirst; simp at hfirst + have htl : (b.operationList ctx.raw ctx.wellFormed bIn).toList = [] := by + rw [← List.length_eq_zero_iff]; simpa using hsize + simp only [htl, interpretTerminatedOpList_nil] + · next firstOp h => + rw [interpretOpChain_eq_interpretTerminatedOpList_of_firstOp bIn + (by rw [BlockPtr.get!_eq_get bIn]; exact h)] + +/-! ## Stage C: `interpretBlock` refinement for every block + +Lifts the block-`B` simulation (and cross-context monotonicity for the unchanged blocks) to the full +`interpretBlock` of *any* block `b`, dispatching `b = block` vs. `b ≠ block`. The source-entry SSA +invariant on the post-`setArgumentValues?` state (`hSrcInv`) and the local op-step simulation +(`hOpSim`) are supplied by the caller (the CFG induction, Stage D). +-/ + +/-- +**Stage C — `interpretBlock` refinement (all blocks).** For any in-bounds block `b`, refined entry +states and arguments, plus the per-entry SSA/dominance invariants on the post-argument states, give a +refinement of `interpretBlock b` across the rewrite. Dispatches on whether `b` is the rewritten block. +-/ +theorem RewrittenAt.interpretBlock_refinement + (hRW : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (newCtxVerif : newCtx.Verified) + (hCtxDom : ctx.Dom) + {b : BlockPtr} (bIn : b.InBounds ctx.raw) + {values values' : Array RuntimeValue} + {state : InterpreterState ctx} {state' : InterpreterState newCtx} + (hState : state.isRefinedBy state' hRW.σ) + (hVals : values ⊒ values') + (hSrcInv : ∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → + ∀ fst (hfst : (b.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) + (hOpSim : OpStepSimulation op newOps hRW.σ opIn hRW.newOpsInBounds') : + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × ControlFlowAction) + (r₂ : InterpreterState newCtx × ControlFlowAction) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2.isRefinedBy r₂.2) + (interpretBlock b values state bIn) + (interpretBlock b values' state' (hRW.blocksInBounds b bIn)) := by + have bIn' := hRW.blocksInBounds b bIn + rw [interpretBlock_eq_setArgumentValues?_interpretTerminatedOpList bIn, + interpretBlock_eq_setArgumentValues?_interpretTerminatedOpList bIn'] + rcases hsa : state.variables.setArgumentValues? b values bIn with _ | newVars + · simp [Interp.isRefinedBy] + · -- Target also sets its block arguments, into a `σ`-refined state (Stage A). + -- The source succeeded, so its arguments conform; refinement (`hVals`) and argument-type + -- preservation (`argType_eq`) carry that conformance to the target arguments. + have tgtConforms : ∀ j, j < b.getNumArguments! newCtx.raw → + (values'[j]!).Conforms ((b.getArguments! newCtx.raw)[j]!.getType! newCtx.raw) := by + intro j hj + rw [BlockPtr.getArguments!.getElem!_eq_getArgument hj] + have hPt : values[j]! ⊒ values'[j]! := by + obtain ⟨hsize, hpt⟩ := hVals + by_cases h : j < values.size + · exact hpt j h + · rw [getElem!_neg values j h, getElem!_neg values' j (hsize ▸ h)] + exact RuntimeValue.isRefinedBy_refl _ + rw [hRW.argType_eq bIn] + exact RuntimeValue.Conforms_of_isRefinedBy hPt + ((VariableState.setArgumentValues?_isSome_iff_conforms state.variables).mpr ⟨newVars, hsa⟩ j + (hRW.numArgsEq bIn ▸ hj)) + obtain ⟨newVars', hsa', hpsRefVar⟩ := VariableState.setArgumentValues?_isRefinedBy + hState.2 hVals bIn bIn' (hRW.argsApplyToArray bIn) + (fun val valIn arg hMem heq => by + obtain ⟨i, _hi, rfl⟩ := BlockPtr.getArguments!.mem_iff_exists_index.mp hMem + exact hRW.mappingReflectsArg val valIn i heq) + tgtConforms hsa + have hpsRef : (InterpreterState.mk newVars state.memory).isRefinedBy + ⟨newVars', state'.memory⟩ hRW.σ := ⟨hState.1, hpsRefVar⟩ + simp only [hsa', Option.bind_some] + by_cases hbB : b = block + · -- Rewritten block `B`: rewrite the op-lists and apply the block-`B` simulation. + subst hbB + have hsrc : (b.operationList ctx.raw ctx.wellFormed bIn).toList + = pre.toList ++ [op] ++ post.toList := by rw [hRW.srcList]; simp + have htgt : (b.operationList newCtx.raw newCtx.wellFormed bIn').toList + = pre.toList ++ newOps.toList ++ post.toList := by rw [hRW.tgtList]; simp + simp only [hsrc, htgt] + grind [hRW.blockSimulation] + · -- Other block: op-lists identical, apply cross-context monotonicity. + have hother := hRW.otherBlocks b bIn bIn' hbB + have chainSrc := BlockPtr.operationListWF ctx.raw b bIn ctx.wellFormed + have chainTgt := BlockPtr.operationListWF newCtx.raw b bIn' newCtx.wellFormed + simp only [hother] + have opsIn : ∀ o ∈ (b.operationList newCtx.raw newCtx.wellFormed bIn').toList, + o.InBounds ctx.raw := by + intro o ho + rw [← hother] at ho + exact chainSrc.arrayInBounds (by simpa using ho) + have opsIn' : ∀ o ∈ (b.operationList newCtx.raw newCtx.wellFormed bIn').toList, + o.InBounds newCtx.raw := fun o ho => chainTgt.arrayInBounds (by simpa using ho) + have hne_op : ∀ o ∈ (b.operationList newCtx.raw newCtx.wellFormed bIn').toList, o ≠ op := by + intro o ho heq; subst heq; exact hRW.opErased (opsIn' o ho) + have hFrame : ∀ o, (h : o ∈ (b.operationList newCtx.raw newCtx.wellFormed bIn').toList) → + (hRW.σ).PreservesOperation o o := fun o h => hRW.frame_of_ne (opsIn o h) (hne_op o h) + exact interpretTerminatedOpList_mono newCtxVerif opsIn opsIn' hpsRef hFrame + +/-! ## Stage B bundling: cross-block invariant re-establishment + +When interpreting a block `b` yields a `.branch res succ`, the entry invariant on `b`'s post-argument +state is threaded through `b`'s operation chain to its terminator's exit, then crossed over the CFG +edge into `succ` (Stage B-core establishment lemmas), giving the entry invariant on `succ`'s +post-argument state. The block's operation list is supplied split as `front ++ [term]` with `term` the +terminator; `hFrontNoCf` (only the last operation branches) certifies the control-flow action comes +from `term`. These structural facts are supplied by the driver (Stage D). +-/ + +/-- Decompose a branching block run. If interpreting `b` (whose operation list is `front ++ [term]`, +with `front` never branching) yields `.branch res succ`, then `b`'s arguments are set, `front` runs to +completion without a terminator, and the terminator `term` produces the branch. -/ +theorem interpretBlock_branch_split + {ctx : WfIRContext OpCode} {b succ : BlockPtr} (bIn : b.InBounds ctx.raw) + {values res : Array RuntimeValue} {state exitState : InterpreterState ctx} + {front : List OperationPtr} {term : OperationPtr} + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (termIn : term.InBounds ctx.raw) + (harr : (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term]) + (hFrontNoCf : ∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf))) + (hRun : interpretBlock b values state bIn = some (.ok (exitState, .branch res succ))) : + ∃ newVars s', + state.variables.setArgumentValues? b values bIn = some newVars ∧ + interpretOpList front ⟨newVars, state.memory⟩ frontIn = some (.ok (s', none)) ∧ + interpretOp term s' termIn = some (.ok (exitState, some (.branch res succ))) := by + rw [interpretBlock_eq_setArgumentValues?_interpretTerminatedOpList bIn] at hRun + rcases hsa : state.variables.setArgumentValues? b values bIn with _ | newVars + · rw [hsa] at hRun; simp at hRun + · rw [hsa] at hRun + simp only [Option.bind_some] at hRun + refine ⟨newVars, ?_⟩ + -- Rewrite the block list to `front ++ [term]` and split the terminated run. + simp only [harr] at hRun + rw [interpretTerminatedOpList_append] at hRun + rcases hfront : interpretOpList front ⟨newVars, state.memory⟩ frontIn with _ | (⟨s', act⟩ | _) <;> + simp only [hfront] at hRun + · grind + · cases act with + | none => + rcases hterm : interpretOp term s' termIn with _ | (⟨s'', act'⟩ | _) <;> + simp only [hterm, interpretTerminatedOpList_cons] at hRun + · grind + · cases act' with + | none => simp only [interpretTerminatedOpList_nil] at hRun; grind + | some cf => exact ⟨s', rfl, rfl, by grind⟩ + · grind + | some cf => exact absurd hfront (hFrontNoCf _ _ _) + · grind + +/-- The terminator `term` (the last operation of `b`'s op list `front ++ [term]`) has `b` as parent +and is the block's last operation (`next = none`). -/ +theorem operationList_split_term_facts {ctx : WfIRContext OpCode} {b : BlockPtr} + (bIn : b.InBounds ctx.raw) {front : List OperationPtr} {term : OperationPtr} + (termIn : term.InBounds ctx.raw) + (harr : (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term]) : + (term.get! ctx.raw).parent = some b ∧ (term.get! ctx.raw).next = none ∧ + b.OpChainSlice ctx.raw (front ++ [term]) ∧ + (b.get! ctx.raw).firstOp = (front ++ [term]).head? := by + have chain := BlockPtr.operationListWF ctx.raw b bIn ctx.wellFormed + have hmemL : term ∈ (b.operationList ctx.raw ctx.wellFormed bIn).toList := by rw [harr]; simp + have hmem : term ∈ b.operationList ctx.raw ctx.wellFormed bIn := by simpa using hmemL + have hparent : (term.get! ctx.raw).parent = some b := chain.opParent hmem + have hlen : (b.operationList ctx.raw ctx.wellFormed bIn).size = front.length + 1 := by + have := congrArg List.length harr; simpa using this + have hlast : (b.get! ctx.raw).lastOp = some term := by + rw [chain.last, + show (b.operationList ctx.raw ctx.wellFormed bIn).size - 1 = front.length from by omega, + ← Array.getElem?_toList, harr] + simp + have hnext : (term.get! ctx.raw).next = none := + (BlockPtr.OpChain.next!_eq_none_iff_lastOp!_eq_self termIn chain hparent).mpr hlast + have hchain : b.OpChainSlice ctx.raw (front ++ [term]) := by + rw [← harr]; exact chain.opChainSlice + have hfirst : (b.get! ctx.raw).firstOp = (front ++ [term]).head? := by + rw [chain.first, ← harr] + simp [List.head?_eq_getElem?, Array.getElem?_toList] + exact ⟨hparent, hnext, hchain, hfirst⟩ + +/-- **Source-side cross-block re-establishment.** Threads `EquationLemmaAt` from `b`'s entry, through +its operation chain to the terminator's exit, then across the CFG edge into `succ`. -/ +theorem interpretBlock_branch_equationLemmaAt_succ + {ctx : WfIRContext OpCode} (ctxDom : ctx.Dom) + {b succ : BlockPtr} (bIn : b.InBounds ctx.raw) (succIn : succ.InBounds ctx.raw) + {values res : Array RuntimeValue} {state exitState : InterpreterState ctx} + {front : List OperationPtr} {term : OperationPtr} + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (termIn : term.InBounds ctx.raw) + (harr : (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term]) + (hFrontNoCf : ∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf))) + (hEntryInv : ∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → + ∀ fst (_hfst : (b.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) + (hRun : interpretBlock b values state bIn = some (.ok (exitState, .branch res succ))) : + ∀ newVars', exitState.variables.setArgumentValues? succ res succIn = some newVars' → + ∀ sfst (_hsfst : (succ.get! ctx.raw).firstOp = some sfst), + (InterpreterState.mk newVars' exitState.memory).EquationLemmaAt (.before sfst) + (by have := ctx.wellFormed.inBounds; grind) := by + intro newVars' hArgs sfst hsfst + obtain ⟨newVars, s', hSetArgs, hFrontRun, hTermRun⟩ := + interpretBlock_branch_split bIn frontIn termIn harr hFrontNoCf hRun + obtain ⟨hparent, hnext, hchain, hfirst⟩ := operationList_split_term_facts bIn termIn harr + -- Thread `EquationLemmaAt` from entry through `front` to the point before `term`. + have hBeforeTerm : s'.EquationLemmaAt (.before term) termIn := + interpretOpList_equationLemmaAt_before ctxDom frontIn termIn hchain + (fun fst _ hhead => by + refine hEntryInv newVars hSetArgs fst ?_ + rw [hfirst]; exact hhead) + hFrontRun + -- Step across the terminator to its exit, then across the CFG edge into `succ`. + have hAfterTerm := interpretOp_equationLemmaAt ctxDom hBeforeTerm hparent hTermRun + have succMem : succ ∈ term.getSuccessors! ctx.raw := + interpretOp_branch_dest_mem_getSuccessors! hTermRun + have hlast : (b.get! ctx.raw).lastOp = some term := by grind + have hSucc : succ ∈ b.getSuccessors! ctx.raw := by + simp only [BlockPtr.getSuccessors!, hlast]; exact succMem + have hAtEnd : InsertPoint.after term ctx.raw b hparent termIn = InsertPoint.atEnd b := by + grind [InsertPoint.after] + have hAtStart : InsertPoint.atStart! succ ctx.raw = InsertPoint.before sfst := by + simp [InsertPoint.atStart!, hsfst] + simp only [hAtEnd] at hAfterTerm + have result := InterpreterState.EquationLemmaAt.setArgumentValues?_succ_entry ctxDom bIn + hSucc hAfterTerm hArgs + simp only [hAtStart] at result + exact result + +/-- **Target-side cross-block re-establishment.** Threads `DefinesDominating` from `b`'s entry, through +its operation chain to the terminator's exit, then across the CFG edge into `succ`. -/ +theorem interpretBlock_branch_definesDominating_succ + {ctx : WfIRContext OpCode} (ctxDom : ctx.Dom) + {b succ : BlockPtr} (bIn : b.InBounds ctx.raw) (succIn : succ.InBounds ctx.raw) + {values res : Array RuntimeValue} {state exitState : InterpreterState ctx} + {front : List OperationPtr} {term : OperationPtr} + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (termIn : term.InBounds ctx.raw) + (harr : (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term]) + (hFrontNoCf : ∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf))) + (hEntryInv : ∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → + ∀ fst (_hfst : (b.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars state.memory).DefinesDominating (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) + (hRun : interpretBlock b values state bIn = some (.ok (exitState, .branch res succ))) : + ∀ newVars', exitState.variables.setArgumentValues? succ res succIn = some newVars' → + ∀ sfst (_hsfst : (succ.get! ctx.raw).firstOp = some sfst), + (InterpreterState.mk newVars' exitState.memory).DefinesDominating (.before sfst) + (by have := ctx.wellFormed.inBounds; grind) := by + intro newVars' hArgs sfst hsfst + obtain ⟨newVars, s', hSetArgs, hFrontRun, hTermRun⟩ := + interpretBlock_branch_split bIn frontIn termIn harr hFrontNoCf hRun + obtain ⟨hparent, hnext, hchain, hfirst⟩ := operationList_split_term_facts bIn termIn harr + have hBeforeTerm : s'.DefinesDominating (.before term) termIn := + interpretOpList_DefinesDominating_before ctxDom frontIn termIn hchain + (fun fst _ hhead => by + refine hEntryInv newVars hSetArgs fst ?_ + rw [hfirst]; exact hhead) + hFrontRun + have hAfterTerm := interpretOp_DefinesDominating ctxDom hBeforeTerm hparent hTermRun + have succMem : succ ∈ term.getSuccessors! ctx.raw := + interpretOp_branch_dest_mem_getSuccessors! hTermRun + have hlast : (b.get! ctx.raw).lastOp = some term := by grind + have hSucc : succ ∈ b.getSuccessors! ctx.raw := by + simp only [BlockPtr.getSuccessors!, hlast]; exact succMem + have hAtEnd : InsertPoint.after term ctx.raw b hparent termIn = InsertPoint.atEnd b := by + grind [InsertPoint.after] + have hAtStart : InsertPoint.atStart! succ ctx.raw = InsertPoint.before sfst := by + simp [InsertPoint.atStart!, hsfst] + simp only [hAtEnd] at hAfterTerm + have result := InterpreterState.DefinesDominating.setArgumentValues?_succ_entry ctxDom bIn + hSucc hAfterTerm hArgs + simp only [hAtStart] at result + exact result + +/-! ## Stage D: `interpretBlockCFG` refinement (the CFG walk) + +Lifts the per-block refinement (Stage C, `interpretBlock_refinement`) to the whole CFG walk via the +`partial_fixpoint` induction on `interpretBlockCFG`. The induction threads the source-entry SSA +invariant `EquationLemmaAt` across CFG edges with `interpretBlock_branch_equationLemmaAt_succ`. No +target-progress argument is needed: a source `ub` refines any target outcome (including a +non-terminating `none`), so the source-UB case closes trivially. +-/ + +/-- +**Stage D — `interpretBlockCFG` refinement.** Interpreting the CFG from any in-bounds block `b` in +the source context is refined by interpreting it from `b` in the target context, under the rewrite +renaming `σ`. The per-block terminator splits `hSrcSplit` (only the last operation of each block +branches) are supplied by the driver (PR 9). +-/ +theorem RewrittenAt.interpretBlockCFG_refinement + (hRW : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (newCtxVerif : newCtx.Verified) + (hCtxDom : ctx.Dom) + (hOpSim : OpStepSimulation op newOps hRW.σ opIn hRW.newOpsInBounds') + (hSrcSplit : ∀ (b : BlockPtr) (bIn : b.InBounds ctx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (_termIn : term.InBounds ctx.raw), + (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + {b : BlockPtr} (bIn : b.InBounds ctx.raw) + {values values' : Array RuntimeValue} + {state : InterpreterState ctx} {state' : InterpreterState newCtx} + (hState : state.isRefinedBy state' hRW.σ) + (hVals : values ⊒ values') + (hSrcInv : ∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → + ∀ fst (hfst : (b.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) : + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Array RuntimeValue) + (r₂ : InterpreterState newCtx × Array RuntimeValue) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + (interpretBlockCFG b values state bIn) + (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn)) := by + refine interpretBlockCFG.fixpoint_induct + (motive := fun g => ∀ (b : BlockPtr) (bIn : b.InBounds ctx.raw) + (values values' : Array RuntimeValue) + (state : InterpreterState ctx) (state' : InterpreterState newCtx), + state.isRefinedBy state' hRW.σ → values ⊒ values' → + (∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → + ∀ fst (hfst : (b.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) → + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Array RuntimeValue) + (r₂ : InterpreterState newCtx × Array RuntimeValue) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + (g b values state bIn) + (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) + ?admissible ?step b bIn values values' state state' hState hVals hSrcInv + case admissible => + -- Peel the (dependent) leading `∀ b` together with the `g b` application with + -- `admissible_pi_apply`, the remaining (non-dependent) binders with `admissible_pi`, the + -- `g b values state bIn` applications with `admissible_apply`, and close at the `none`-bottom. + apply Lean.Order.admissible_pi_apply + (P := fun (b : BlockPtr) (gb : Array RuntimeValue → InterpreterState ctx → + b.InBounds ctx.raw → Interp (InterpreterState ctx × Array RuntimeValue)) => + ∀ (bIn : b.InBounds ctx.raw) (values values' : Array RuntimeValue) + (state : InterpreterState ctx) (state' : InterpreterState newCtx), + state.isRefinedBy state' hRW.σ → values ⊒ values' → + (∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → + ∀ fst (hfst : (b.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) → + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Array RuntimeValue) + (r₂ : InterpreterState newCtx × Array RuntimeValue) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + (gb values state bIn) + (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) + intro b + apply Lean.Order.admissible_pi; intro bIn + apply Lean.Order.admissible_pi; intro values + apply Lean.Order.admissible_pi; intro values' + apply Lean.Order.admissible_pi; intro state + apply Lean.Order.admissible_pi; intro state' + apply Lean.Order.admissible_pi; intro hState + apply Lean.Order.admissible_pi; intro hVals + apply Lean.Order.admissible_pi; intro hSrcInv + apply Lean.Order.admissible_apply + (P := fun (_v : Array RuntimeValue) (gv : InterpreterState ctx → b.InBounds ctx.raw → + Interp (InterpreterState ctx × Array RuntimeValue)) => + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Array RuntimeValue) + (r₂ : InterpreterState newCtx × Array RuntimeValue) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + (gv state bIn) (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) + (x := values) + apply Lean.Order.admissible_apply + (P := fun (_s : InterpreterState ctx) (gs : b.InBounds ctx.raw → + Interp (InterpreterState ctx × Array RuntimeValue)) => + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Array RuntimeValue) + (r₂ : InterpreterState newCtx × Array RuntimeValue) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + (gs bIn) (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) + (x := state) + apply Lean.Order.admissible_apply + (P := fun (_h : b.InBounds ctx.raw) (gh : Interp (InterpreterState ctx × Array RuntimeValue)) => + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Array RuntimeValue) + (r₂ : InterpreterState newCtx × Array RuntimeValue) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + gh (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) + (x := bIn) + exact Lean.Order.admissible_flatOrder _ trivial + case step => + intro g IH b bIn values values' state state' hState hVals hSrcInv + have hBlk := hRW.interpretBlock_refinement newCtxVerif hCtxDom bIn hState hVals hSrcInv hOpSim + rw [interpretBlockCFG] + rcases hsrc : interpretBlock b values state bIn with _ | (⟨s, act⟩ | _) + · -- Source block run fails: the CFG step is `none`, refinement is trivial. + simp only [hsrc, Interp.isRefinedBy_none_target] + · -- Source block run succeeds with exit state `s` and control-flow action `act`. + rw [hsrc] at hBlk + simp only [Interp.isRefinedBy_ok_target_iff] at hBlk + obtain ⟨⟨s', act'⟩, htgt, hsRef, hactRef⟩ := hBlk + cases act with + | «return» r => + -- A `return`: both CFG walks stop here. The target action is a `return` of refined values. + obtain ⟨r', hact', hr⟩ : ∃ r', act' = .return r' ∧ r ⊒ r' := by + cases act' <;> simp_all [ControlFlowAction.isRefinedBy] + subst hact' + simp only [hsrc, htgt, Interp.isRefinedBy] + exact ⟨hsRef, hr⟩ + | branch r succ => + -- A `branch`: the target action branches to the *same* successor with refined values. + obtain ⟨r', hact', hr⟩ : ∃ r', act' = .branch r' succ ∧ r ⊒ r' := by + cases act' <;> simp_all [ControlFlowAction.isRefinedBy] + subst hact' + by_cases hsuccIn : succ.InBounds ctx.raw + · -- Successor in bounds: both walks recurse into `succ`; close with the IH, threading the + -- source-entry SSA invariant across the CFG edge. + obtain ⟨front, term, frontIn, termIn, harr, hFrontNoCf⟩ := hSrcSplit b bIn + have hSrcInvSucc := interpretBlock_branch_equationLemmaAt_succ hCtxDom bIn hsuccIn + frontIn termIn harr hFrontNoCf hSrcInv hsrc + simp only [hsrc, htgt, dif_pos hsuccIn, dif_pos (hRW.blocksInBounds succ hsuccIn)] + exact IH succ hsuccIn r r' s s' hsRef hr hSrcInvSucc + · -- Successor out of bounds in the source: the source CFG step is `none`, refinement trivial. + simp only [hsrc, dif_neg hsuccIn, Interp.isRefinedBy_none_target] + · -- Source block run is UB, which is refined by any target outcome. + simp only [hsrc, Interp.ub, Interp.isRefinedBy_ub_target] + +/-! ## Stage E: `interpretRegion` / `interpretFunction` refinement + +Wraps the CFG-walk refinement (Stage D) up through `interpretRegion` and `interpretFunction`. A +function operation `funcOp` survives the rewrite: it has exactly one region, whereas the matched op +`op` has not (clause 9 / `opNotFunction`), so `funcOp ≠ op`. The rewrite therefore preserves the +function's single region, its entry block, and the entry first-op, and the whole-function +interpretation refines. The fresh empty entry state is `σ`-refined in both contexts (no variables, +same memory); the source-entry SSA invariant on it is supplied by the caller (PR 9 / the module-level +driver), exactly as in Stage D. +-/ + +/-- The fresh, empty interpreter state (no variables, memory `mem`) in the source context is refined +by the fresh empty state in the target context under any renaming `σ`: they define no variables (so +the variable-refinement obligation is vacuous) and share the same memory. -/ +theorem InterpreterState.empty_isRefinedBy {ctx ctx' : WfIRContext OpCode} + (μ : ValueMapping ctx ctx') (mem : MemoryState) : + (InterpreterState.mk (VariableState.empty ctx) mem).isRefinedBy + (InterpreterState.mk (VariableState.empty ctx') mem) μ := by + refine ⟨rfl, ?_⟩ + intro val valIn sourceVar hget + simp [VariableState.getVar?, VariableState.empty] at hget + +/-- Lift a `σ`-refinement of two region runs to a `FunctionResult` refinement of the corresponding +function runs: `interpretFunction` post-processes `interpretRegion` by keeping only the final memory +and the returned values, and `InterpreterState.isRefinedBy` already entails equal memories, so the +refinement is preserved by that projection. -/ +theorem Interp.isRefinedBy_functionResult_of_region {ctx ctx' : WfIRContext OpCode} + {μ : ValueMapping ctx ctx'} + {a : Interp (InterpreterState ctx × Array RuntimeValue)} + {b : Interp (InterpreterState ctx' × Array RuntimeValue)} + (h : Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Array RuntimeValue) + (r₂ : InterpreterState ctx' × Array RuntimeValue) => + r₁.1.isRefinedBy r₂.1 μ ∧ r₁.2 ⊒ r₂.2) a b) : + Interp.isRefinedBy FunctionResult.isRefinedBy + (a >>= fun x => pure (x.1.memory, x.2)) + (b >>= fun x => pure (x.1.memory, x.2)) := by + rcases a with _ | (⟨sf, sres⟩ | _) + · exact Interp.isRefinedBy_none_target + · simp only [Interp.isRefinedBy_ok_target_iff] at h + obtain ⟨⟨sf', sres'⟩, htgt, hsRef, hresRef⟩ := h + subst htgt + exact ⟨hsRef.1, hresRef⟩ + · exact Interp.isRefinedBy_ub_target + +/-- Lift a `σ`-refinement of two region runs to an array refinement of the corresponding module runs: +`interpretModule` post-processes `interpretRegion` by keeping only the returned values, so the +value-array refinement carried by the region refinement is exactly what survives. -/ +theorem Interp.isRefinedBy_moduleResult_of_region {ctx ctx' : WfIRContext OpCode} + {μ : ValueMapping ctx ctx'} + {a : Interp (InterpreterState ctx × Array RuntimeValue)} + {b : Interp (InterpreterState ctx' × Array RuntimeValue)} + (h : Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Array RuntimeValue) + (r₂ : InterpreterState ctx' × Array RuntimeValue) => + r₁.1.isRefinedBy r₂.1 μ ∧ r₁.2 ⊒ r₂.2) a b) : + Interp.isRefinedBy RuntimeValue.arrayIsRefinedBy + (a >>= fun x => pure x.2) + (b >>= fun x => pure x.2) := by + rcases a with _ | (⟨sf, sres⟩ | _) + · exact Interp.isRefinedBy_none_target + · simp only [Interp.isRefinedBy_ok_target_iff] at h + obtain ⟨⟨sf', sres'⟩, htgt, _hsRef, hresRef⟩ := h + subst htgt + exact hresRef + · exact Interp.isRefinedBy_ub_target + +/-- +**Stage E — `interpretRegion` refinement.** Interpreting the source region `r` in the source context +is refined by interpreting the (equal) target region `r'` in the target context, under the rewrite +renaming `σ`. The two region pointers coincide (`hrr`) because the rewrite preserves the function's +single region; the rewrite further preserves `r`'s first block (clause 8), so both walks enter the CFG +at the same entry block, where the per-entry source SSA invariant `hSrcInv` feeds the Stage-D CFG +refinement. +-/ +theorem RewrittenAt.interpretRegion_refinement + (hRW : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (newCtxVerif : newCtx.Verified) + (hCtxDom : ctx.Dom) + (hOpSim : OpStepSimulation op newOps hRW.σ opIn hRW.newOpsInBounds') + (hSrcSplit : ∀ (b : BlockPtr) (bIn : b.InBounds ctx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (_termIn : term.InBounds ctx.raw), + (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + {r r' : RegionPtr} (rIn : r.InBounds ctx.raw) (rIn' : r'.InBounds newCtx.raw) + (hrr : r' = r) + {values values' : Array RuntimeValue} + {state : InterpreterState ctx} {state' : InterpreterState newCtx} + (hState : state.isRefinedBy state' hRW.σ) + (hVals : values ⊒ values') + (hSrcInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) + (newVars : VariableState ctx), + state.variables.setArgumentValues? entryBlock values entryIn = some newVars → + ∀ fst (hfst : (entryBlock.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) : + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Array RuntimeValue) + (r₂ : InterpreterState newCtx × Array RuntimeValue) => + r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + (interpretRegion r values state rIn) + (interpretRegion r' values' state' rIn') := by + subst hrr + unfold interpretRegion + -- The rewrite preserves the region's first block (clause 8): both walks enter the same entry block. + have hfb : (r'.get newCtx.raw rIn').firstBlock = (r'.get ctx.raw rIn).firstBlock := by + rw [← RegionPtr.get!_eq_get rIn, ← RegionPtr.get!_eq_get rIn'] + exact hRW.regionFirstBlockPreserved r' rIn + -- Case on the source first block; the target enters the same block by `hfb`. + split + · -- Empty region: the source run is `none`, refinement is trivial. + exact Interp.isRefinedBy_none_target + · rename_i entryBlock heq + -- The entry block is in bounds (it is the region's first block). + have entryIn : entryBlock.InBounds ctx.raw := by + have hmaybe := RegionPtr.firstBlock!_inBounds ctx.wellFormed.inBounds rIn + rw [Option.maybe_def] at hmaybe + exact hmaybe entryBlock (by rw [RegionPtr.get!_eq_get rIn]; exact heq) + have hentry : (r'.get ctx.raw rIn).firstBlock = some entryBlock := heq + split + · -- Target empty: contradicts `hfb` + `hentry`. + rename_i heqt + have h1 : (r'.get newCtx.raw rIn').firstBlock = none := heqt + rw [hfb, hentry] at h1 + simp at h1 + · rename_i entryBlock' heqt + have hee : entryBlock' = entryBlock := by + have h1 : (r'.get newCtx.raw rIn').firstBlock = some entryBlock' := heqt + rw [hfb, hentry] at h1 + simpa using h1.symm + subst entryBlock' + exact hRW.interpretBlockCFG_refinement newCtxVerif hCtxDom hOpSim hSrcSplit entryIn + hState hVals (fun newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) + +/-- +**Stage E — `interpretFunction` refinement (monotonicity).** Interpreting a function operation +`funcOp` on refined arguments in the source context is refined by interpreting it in the target +context, under the rewrite renaming `σ`. `funcOp` survives the rewrite because it is a function (one +region) while the matched op `op` is not (clause 9 / `opNotFunction`), so the single region — and with +it the entry CFG walk — is preserved. The empty entry state is `σ`-refined; the source-entry SSA +invariant on it (`hSrcInv`) is supplied by the caller. +-/ +theorem RewrittenAt.interpretFunction_refinement + (hRW : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (newCtxVerif : newCtx.Verified) + (hCtxDom : ctx.Dom) + (hOpSim : OpStepSimulation op newOps hRW.σ opIn hRW.newOpsInBounds') + (hSrcSplit : ∀ (b : BlockPtr) (bIn : b.InBounds ctx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (_termIn : term.InBounds ctx.raw), + (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + {funcOp : OperationPtr} (funcOpIn : funcOp.InBounds ctx.raw) + (funcOpIn' : funcOp.InBounds newCtx.raw) + {values values' : Array RuntimeValue} {mem : MemoryState} + (hVals : values ⊒ values') + (hSrcInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) + (newVars : VariableState ctx), + (VariableState.empty ctx).setArgumentValues? entryBlock values entryIn = some newVars → + ∀ fst (hfst : (entryBlock.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars mem).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) : + Interp.isRefinedBy FunctionResult.isRefinedBy + (interpretFunction funcOp values mem funcOpIn) + (interpretFunction funcOp values' mem funcOpIn') := by + unfold interpretFunction + by_cases hNum : funcOp.getNumRegions ctx.raw funcOpIn = 1 + · -- `funcOp` is a function (one region), so it is not the matched op `op` (clause 9), hence survives. + have hNe : funcOp ≠ op := by + rintro rfl + exact hRW.opNotFunction (by rw [OperationPtr.getNumRegions!_eq_getNumRegions funcOpIn]; exact hNum) + have hNum' : funcOp.getNumRegions newCtx.raw funcOpIn' = 1 := by + rw [show funcOp.getNumRegions newCtx.raw funcOpIn' + = funcOp.getNumRegions ctx.raw funcOpIn from hRW.getNumRegions_eq funcOpIn hNe] + exact hNum + -- Both functions proceed (the `≠ 1` guard is false on each side). + rw [dif_neg (by rw [hNum]; simp), dif_neg (by rw [hNum']; simp)] + -- The single region is preserved: same pointer, in bounds in both contexts. + have hi : (0 : Nat) < funcOp.getNumRegions ctx.raw funcOpIn := by rw [hNum]; omega + have hi' : (0 : Nat) < funcOp.getNumRegions newCtx.raw funcOpIn' := by rw [hNum']; omega + have hReg : funcOp.getRegion newCtx.raw 0 funcOpIn' hi' = funcOp.getRegion ctx.raw 0 funcOpIn hi := + hRW.getRegion_eq funcOpIn hNe 0 hi + have rIn : (funcOp.getRegion ctx.raw 0 funcOpIn hi).InBounds ctx.raw := by + rw [← OperationPtr.getRegion!_eq_getRegion hi] + exact OperationPtr.getRegions!_inBounds ctx.wellFormed.inBounds funcOpIn (by grind) + have rIn' : (funcOp.getRegion newCtx.raw 0 funcOpIn' hi').InBounds newCtx.raw := by + rw [← OperationPtr.getRegion!_eq_getRegion hi'] + exact OperationPtr.getRegions!_inBounds newCtx.wellFormed.inBounds funcOpIn' + (by rw [OperationPtr.getNumRegions!_eq_getNumRegions funcOpIn']; exact hi') + -- The single region is preserved, so its interpretation refines (Stage E region lemma). + have hregRef := hRW.interpretRegion_refinement newCtxVerif hCtxDom hOpSim hSrcSplit rIn rIn' hReg + (state := ⟨.empty ctx, mem⟩) (state' := ⟨.empty newCtx, mem⟩) + (InterpreterState.empty_isRefinedBy hRW.σ mem) hVals + (fun entryBlock entryIn newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) + -- The function result keeps only the final memory and returned values of the region run. + show Interp.isRefinedBy FunctionResult.isRefinedBy + ((interpretRegion (funcOp.getRegion ctx.raw 0 funcOpIn hi) values ⟨.empty ctx, mem⟩ rIn) + >>= fun x => pure (x.1.memory, x.2)) + ((interpretRegion (funcOp.getRegion newCtx.raw 0 funcOpIn' hi') values' ⟨.empty newCtx, mem⟩ rIn') + >>= fun x => pure (x.1.memory, x.2)) + exact Interp.isRefinedBy_functionResult_of_region hregRef + · -- `funcOp` is not a function: the source run is `none`, refinement is trivial. + rw [dif_pos (by simpa using hNum)] + exact Interp.isRefinedBy_none_target + +/-- +**Stage E — `interpretModule` refinement (monotonicity).** Interpreting a module operation +`moduleOp` in the source context is refined by interpreting it in the target context, under the +rewrite renaming `σ`. As with `interpretFunction`, `moduleOp` survives the rewrite because it has a +single region while the matched op `op` does not (clause 9 / `opNotFunction`), so the region — and its +entry CFG walk — is preserved. The module starts from the fresh empty state (no variables, empty +memory, no arguments); the source-entry SSA invariant on it (`hSrcInv`) is supplied by the caller. +-/ +theorem RewrittenAt.interpretModule_refinement + (hRW : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (newCtxVerif : newCtx.Verified) + (hCtxDom : ctx.Dom) + (hOpSim : OpStepSimulation op newOps hRW.σ opIn hRW.newOpsInBounds') + (hSrcSplit : ∀ (b : BlockPtr) (bIn : b.InBounds ctx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (_termIn : term.InBounds ctx.raw), + (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + {moduleOp : OperationPtr} (moduleOpIn : moduleOp.InBounds ctx.raw) + (moduleOpIn' : moduleOp.InBounds newCtx.raw) + (hSrcInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) + (newVars : VariableState ctx), + (VariableState.empty ctx).setArgumentValues? entryBlock #[] entryIn = some newVars → + ∀ fst (hfst : (entryBlock.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars MemoryState.empty).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) : + Interp.isRefinedBy RuntimeValue.arrayIsRefinedBy + (interpretModule ctx moduleOp moduleOpIn) + (interpretModule newCtx moduleOp moduleOpIn') := by + unfold interpretModule + by_cases hNum : moduleOp.getNumRegions ctx.raw moduleOpIn = 1 + · -- `moduleOp` has one region, so it is not the matched op `op` (clause 9), hence survives. + have hNe : moduleOp ≠ op := by + rintro rfl + exact hRW.opNotFunction (by rw [OperationPtr.getNumRegions!_eq_getNumRegions moduleOpIn]; exact hNum) + have hNum' : moduleOp.getNumRegions newCtx.raw moduleOpIn' = 1 := by + rw [show moduleOp.getNumRegions newCtx.raw moduleOpIn' + = moduleOp.getNumRegions ctx.raw moduleOpIn from hRW.getNumRegions_eq moduleOpIn hNe] + exact hNum + -- Both modules proceed (the `≠ 1` guard is false on each side). + rw [dif_neg (by rw [hNum]; simp), dif_neg (by rw [hNum']; simp)] + -- The single region is preserved: same pointer, in bounds in both contexts. + have hi : (0 : Nat) < moduleOp.getNumRegions ctx.raw moduleOpIn := by rw [hNum]; omega + have hi' : (0 : Nat) < moduleOp.getNumRegions newCtx.raw moduleOpIn' := by rw [hNum']; omega + have hReg : moduleOp.getRegion newCtx.raw 0 moduleOpIn' hi' = moduleOp.getRegion ctx.raw 0 moduleOpIn hi := + hRW.getRegion_eq moduleOpIn hNe 0 hi + have rIn : (moduleOp.getRegion ctx.raw 0 moduleOpIn hi).InBounds ctx.raw := by + rw [← OperationPtr.getRegion!_eq_getRegion hi] + exact OperationPtr.getRegions!_inBounds ctx.wellFormed.inBounds moduleOpIn (by grind) + have rIn' : (moduleOp.getRegion newCtx.raw 0 moduleOpIn' hi').InBounds newCtx.raw := by + rw [← OperationPtr.getRegion!_eq_getRegion hi'] + exact OperationPtr.getRegions!_inBounds newCtx.wellFormed.inBounds moduleOpIn' + (by rw [OperationPtr.getNumRegions!_eq_getNumRegions moduleOpIn']; exact hi') + -- The single region is preserved, so its interpretation refines (Stage E region lemma). + have hregRef := hRW.interpretRegion_refinement newCtxVerif hCtxDom hOpSim hSrcSplit rIn rIn' hReg + (state := InterpreterState.empty ctx) (state' := InterpreterState.empty newCtx) + (InterpreterState.empty_isRefinedBy hRW.σ MemoryState.empty) + (RuntimeValue.arrayIsRefinedBy_refl #[]) + (fun entryBlock entryIn newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) + -- The module result keeps only the returned values of the region run. + show Interp.isRefinedBy RuntimeValue.arrayIsRefinedBy + ((interpretRegion (moduleOp.getRegion ctx.raw 0 moduleOpIn hi) #[] (InterpreterState.empty ctx) rIn) + >>= fun x => pure x.2) + ((interpretRegion (moduleOp.getRegion newCtx.raw 0 moduleOpIn' hi') #[] (InterpreterState.empty newCtx) rIn') + >>= fun x => pure x.2) + exact Interp.isRefinedBy_moduleResult_of_region hregRef + · -- `moduleOp` has no single region: the source run is `none`, refinement is trivial. + rw [dif_pos (by simpa using hNum)] + exact Interp.isRefinedBy_none_target + +end Veir From 63a3d0eef4b760fd73fecc7870b361c987bab3a3 Mon Sep 17 00:00:00 2001 From: math-fehr Date: Sun, 21 Jun 2026 16:44:41 +0100 Subject: [PATCH 05/31] feat(soundness): bridge fromLocalRewrite to RewrittenAt (keystone + blockIn') Add RewrittenAt.of_fromLocalRewrite connecting the concrete LocalRewritePattern driver to the abstract RewrittenAt relation. Proves the keystone reduction (driver's worklist-threaded forIn loops reduce to a pure WfRewriter foldlM), the operationList split helper, a generic foldlM invariant-transport lemma, the insertOp/replaceValue/eraseOp ctx bridges, and the blockIn'/srcList/newValuesSize/ opNotFunction fields. Remaining RewrittenAt fields are scoped sorries. Co-Authored-By: Claude Opus 4.8 --- Veir/PatternRewriter/Soundness.lean | 216 ++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index b3ef0b39a..c2eebc678 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -1666,4 +1666,220 @@ theorem RewrittenAt.interpretModule_refinement rw [dif_pos (by simpa using hNum)] exact Interp.isRefinedBy_none_target +/-! ## PR 9: connecting the concrete driver `fromLocalRewrite` to `RewrittenAt` + +The whole soundness lift above is developed against the abstract `RewrittenAt` relation. This section +bridges it to the concrete driver `RewritePattern.fromLocalRewrite`: when the driver runs the rewrite +branch (the pattern matched `op`, producing `newOps`/`newValues`) and succeeds with output rewriter +`rewriter'`, the net edit `rewriter.ctx ↦ rewriter'.ctx` is exactly a `RewrittenAt` instance. + +### Keystone: the driver's net context is a pure `WfRewriter` fold + +`fromLocalRewrite` threads a `PatternRewriter` (carrying a worklist) through three IR edits: +*insert each `newOp` before `op`*, *redirect each result `op.getResult i` to `newValues[i]`*, and +*erase `op`*. Each `PatternRewriter` primitive (`insertOp`/`replaceValue`/`eraseOp`) only touches the +`.ctx` field through the corresponding `WfRewriter` call; the worklist bookkeeping is inert for the +IR. So `rewriter'.ctx` is the pure fold of the underlying `WfRewriter` operations over the pattern's +output context `newCtxPat`, *independent of the worklist*. This decomposition is the keystone: every +`RewrittenAt` field is a statement about `rewriter'.ctx`, so it is only approachable once `rewriter'.ctx` +is characterized this way (via the existing `operationList_rewriter_insertOp`/`_eraseOp` and the +`*_insertOp`/`*_detachOp` GetSet libraries). -/ + +/-- An in-bounds operation that lives in `block` splits `block`'s operation list as +`pre ++ [op] ++ post` (its predecessors and successors in the block's op chain). -/ +theorem BlockPtr.operationList_split_at_op {ctx : WfIRContext OpCode} + {op : OperationPtr} {block : BlockPtr} + (opIn : op.InBounds ctx.raw) (hParent : (op.get! ctx.raw).parent = some block) + (blockIn : block.InBounds ctx.raw) : + ∃ pre post, block.operationList ctx.raw ctx.wellFormed blockIn = pre ++ #[op] ++ post := by + have hmem : op ∈ block.operationList ctx.raw ctx.wellFormed blockIn := + (BlockPtr.operationList.mem opIn).mp hParent + obtain ⟨s, t, hst⟩ := + List.append_of_mem (a := op) + (l := (block.operationList ctx.raw ctx.wellFormed blockIn).toList) (by simpa using hmem) + exact ⟨s.toArray, t.toArray, by apply Array.toList_inj.mp; simp [hst]⟩ + +/-- Generic invariant transport across a monadic left fold in the `Option` monad: if a predicate `P` +is preserved by every successful step `f b a = some b'`, then it is preserved across the whole fold +(when it succeeds). The keystone fields below instantiate `P` with `InBounds`, `operationList`, … to +transport facts through the driver's `insertOp`/`replaceValue` folds. -/ +theorem List.foldlM_option_invariant {α β : Type} {f : β → α → Option β} {P : β → Prop} + (hstep : ∀ b a b', f b a = some b' → (P b' ↔ P b)) : + ∀ {init s : β} {l : List α}, l.foldlM f init = some s → (P s ↔ P init) := by + intro init s l + induction l generalizing init with + | nil => + intro h + rw [List.foldlM_nil] at h + obtain rfl : init = s := by simpa using h + rfl + | cons a t ih => + intro h + rw [List.foldlM_cons] at h + obtain ⟨b, hf, hb⟩ := Option.bind_eq_some_iff.mp h + rw [ih hb, hstep init a b hf] + +/-- `Array` version of `List.foldlM_option_invariant`. -/ +theorem Array.foldlM_option_invariant {α β : Type} {f : β → α → Option β} {P : β → Prop} + {init s : β} {xs : Array α} + (hstep : ∀ b a b', f b a = some b' → (P b' ↔ P b)) + (h : Array.foldlM f init xs = some s) : P s ↔ P init := by + rw [← Array.foldlM_toList] at h + exact List.foldlM_option_invariant hstep h + +/-- `PatternRewriter.insertOp` only edits the IR through its `WfRewriter.insertOp` call, which leaves +all `InBounds` facts unchanged. -/ +theorem PatternRewriter.insertOp_ctx_inBounds {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {ptr : GenericPtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + ptr.InBounds b'.ctx.raw ↔ ptr.InBounds b.ctx.raw := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h + subst h + exact WfRewriter.insertOp_inBounds_iff hwf + +/-- `PatternRewriter.replaceValue` only edits the IR through its `WfRewriter.replaceValue` call (the +worklist update leaves `.ctx` untouched), which leaves all `InBounds` facts unchanged. -/ +theorem PatternRewriter.replaceValue_ctx_inBounds {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {ptr : GenericPtr} : + ptr.InBounds (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw ↔ ptr.InBounds b.ctx.raw := by + unfold PatternRewriter.replaceValue + simp only [addUsersInWorklist_same_ctx] + exact WfRewriter.replaceValue_inBounds + +/-- `PatternRewriter.eraseOp` sets `.ctx` to the total `WfRewriter.eraseOp` of the input context. -/ +theorem PatternRewriter.eraseOp_ctx_eq {b b' : PatternRewriter OpCode} {op : OperationPtr} + {r u hop} (h : PatternRewriter.eraseOp b op r u hop = some b') : + b'.ctx = WfRewriter.eraseOp b.ctx op r u hop := by + unfold PatternRewriter.eraseOp at h + simp only [bind, Option.bind, Option.some.injEq] at h + subst h + rfl + +set_option warn.sorry false in +/-- +**PR 9 — bridge from the concrete driver.** When `fromLocalRewrite` runs the rewrite branch for a +matched, in-bounds, region-free `op` that lives inside a block, and the pattern satisfies the four +`Return*` correctness obligations, the driver's output context `rewriter'.ctx` is related to the input +`rewriter.ctx` by a `RewrittenAt` instance (the existential supplies the block and the surrounding +`pre`/`post` operation lists). This is the concrete instance the abstract soundness lift consumes. + +`opNotFunction` (clause 9) follows from `hOpRegions` (`op` has no regions, so in particular not one). +The remaining structural fields are discharged from the keystone fold decomposition plus the +`Return*` obligations; this is the deferred proof effort. +-/ +theorem RewrittenAt.of_fromLocalRewrite + {pattern : LocalRewritePattern OpCode} + (hReturnOps : pattern.ReturnOps) + (hReturnCtxChanges : pattern.ReturnCtxChanges) + (hReturnValuesInBounds : pattern.ReturnValuesInBounds) + (hReturnValues : pattern.ReturnValues) + {rewriter rewriter' : PatternRewriter OpCode} + {op : OperationPtr} (opInBounds : op.InBounds rewriter.ctx.raw) + {block : BlockPtr} (hOpParent : (op.get! rewriter.ctx.raw).parent = some block) + (hOpRegions : op.getNumRegions! rewriter.ctx.raw = 0) + {newCtxPat : WfIRContext OpCode} {newOps : Array OperationPtr} {newValues : Array ValuePtr} + (hpat : pattern rewriter.ctx op = some (newCtxPat, some (newOps, newValues))) + (hdriver : RewritePattern.fromLocalRewrite pattern rewriter op opInBounds = some rewriter') : + ∃ (pre post : Array OperationPtr) + (blockIn : block.InBounds rewriter.ctx.raw) (blockIn' : block.InBounds rewriter'.ctx.raw), + RewrittenAt rewriter.ctx op newOps newValues rewriter'.ctx opInBounds + block pre post blockIn blockIn' := by + -- `block` is in bounds of the source context: it is the parent of the in-bounds `op`. + have blockIn : block.InBounds rewriter.ctx.raw := by + have := rewriter.ctx.wellFormed.inBounds; grind + -- Split `block`'s source operation list at `op` into the surrounding `pre`/`post`. + obtain ⟨pre, post, hsrcList⟩ := + BlockPtr.operationList_split_at_op opInBounds hOpParent blockIn + -- Keystone reduction: the driver's worklist bookkeeping is inert for the IR, so `hdriver` reduces + -- to a pure `WfRewriter` fold (`bind_pure_comp` turns each loop body `· >>= pure ∘ .yield` into a + -- functor map; `Array.forIn_yield_eq_foldlM` turns the `forIn` loops into `foldlM`s). After this, + -- `hdriver` reads: insert every `newOp` before `op`, redirect each result to `newValues`, erase + -- `op` — the middle operands-collection loop is dead (its result is discarded). Every `RewrittenAt` + -- field below is a fact about the resulting `rewriter'.ctx` read off this fold. + unfold RewritePattern.fromLocalRewrite at hdriver + rw [hpat] at hdriver + simp only [bind_pure_comp, Array.forIn_yield_eq_foldlM, id_map'] at hdriver + -- Decompose the reduced driver into its three stages: insert-fold (→ `s₁`), replace-fold (→ `s₂`), + -- then the final `eraseOp` of `op`. The middle operands-collection loop is discarded. + obtain ⟨s₁, hfold1, hdriver⟩ := Option.bind_eq_some_iff.mp hdriver + obtain ⟨s₂, hfold2, hdriver⟩ := Option.bind_eq_some_iff.mp hdriver + obtain ⟨_arr, _hloop, herase⟩ := Option.bind_eq_some_iff.mp hdriver + -- Bounds transport across the insert/replace folds: both preserve every `InBounds` fact, so `s₂.ctx` + -- agrees with the pattern's output `newCtxPat` on bounds. + have hbnd : ∀ ptr : GenericPtr, ptr.InBounds s₂.ctx.raw ↔ ptr.InBounds newCtxPat.raw := by + intro ptr + have h1 := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => ptr.InBounds b.ctx.raw) + (fun b a b' h => PatternRewriter.insertOp_ctx_inBounds h) hfold1 + have h2 := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => ptr.InBounds b.ctx.raw) + (fun b a b' h => by + rw [Option.some.injEq] at h; subst h; exact PatternRewriter.replaceValue_ctx_inBounds) hfold2 + exact h2.trans h1 + -- `block` survives into the pattern's output context (the pattern only creates ops). + have hblockNewCtxPat : block.InBounds newCtxPat.raw := + (hReturnCtxChanges rewriter.ctx op newCtxPat newOps newValues hpat).inBounds_mono + (GenericPtr.block block) (by grind) + -- `block` survives the rewrite: bounds are preserved through the folds (`hbnd`) and the final + -- `eraseOp` only removes `op` (an operation), never a block. + have blockIn' : block.InBounds rewriter'.ctx.raw := by + have hb := (hbnd (GenericPtr.block block)).mpr hblockNewCtxPat + rw [PatternRewriter.eraseOp_ctx_eq herase] + grind [WfRewriter.eraseOp] + refine ⟨pre, post, blockIn, blockIn', ?_⟩ + exact { + -- Block-list shape: discharged for the source by the split lemma. + srcList := hsrcList + -- TODO(PR 9, keystone): `operationList_rewriter_insertOp` (newOps before op) + + -- `operationList_rewriter_eraseOp` (op removed) on the fold decomposition. + tgtList := by sorry + -- TODO(PR 9, keystone): insert/erase only touch `block`'s list (other blocks unchanged). + otherBlocks := by sorry + -- Number of produced values: directly from the pattern's `ReturnValues` obligation. + newValuesSize := hReturnValues rewriter.ctx op opInBounds newCtxPat newOps newValues hpat + -- TODO(PR 9, keystone): `ReturnValuesInBounds` gives in-bounds of `newCtxPat`; transport + -- through insert/replace/erase (bounds monotonic, none of the `newValues` is `op`'s results). + newValuesInBounds := by sorry + -- TODO(PR 9, keystone): `ReturnOps` characterizes `newOps` as fresh to `newCtxPat`; transport + -- the freshness across the insert/erase fold to `rewriter'.ctx`. + newOpsFresh := by sorry + -- TODO(PR 9, keystone): from `ReturnValuesInBounds` + bounds transport. + mapResultsInBounds := by sorry + -- TODO(PR 9, keystone): non-results survive (bounds transport minus erase-of-op). + mapNonResultsInBounds := by sorry + -- TODO(PR 9, keystone): `eraseOp op` removes `op` from bounds of `rewriter'.ctx`. + opErased := by sorry + -- TODO(PR 9, keystone): every operation `≠ op` survives insert/replace/erase. + survives := by sorry + -- TODO(PR 9, keystone): `CrossContextFrame` under `σ`; `*_insertOp`/`*_detachOp` GetSet lemmas + -- give unchanged type/props/results/successors, `replaceValue` redirects operands = `σ.applyToArray`. + frame := by sorry + -- TODO(PR 9, keystone): blocks stay in bounds across the fold. + blocksInBounds := by sorry + -- TODO(PR 9, keystone): parent ops of survivors preserved (op-list edits don't move other ops). + parentOps := by sorry + -- TODO(PR 9, keystone): `WfRewriter` ops preserve dominance well-formedness. + newCtxDom := by sorry + -- TODO(PR 9, keystone): `WfRewriter` ops preserve `Verified`. + newCtxVerif := by sorry + -- TODO(PR 9, keystone): `σ` (`rewriteMapping`) is the identity off `op`'s results. + mappingFixesNonResults := by sorry + -- TODO(PR 9, keystone): each produced value is a result of some `newOp` (from the driver). + newValuesAreResults := by sorry + -- TODO(PR 9, keystone): operation-list edits leave block-argument lists untouched. + blockArgsPreserved := by sorry + -- TODO(PR 9, keystone): regions stay in bounds across the fold. + regionsInBounds := by sorry + -- TODO(PR 9, keystone): op-list edits leave survivors' region lists untouched. + opRegionsPreserved := by sorry + -- TODO(PR 9, keystone): op-list edits leave region entry blocks untouched. + regionFirstBlockPreserved := by sorry + -- `op` is not a function: it has no regions, so in particular not exactly one. + opNotFunction := by simp [hOpRegions] + } + end Veir From 820a35fd157ee1f6d00c43d2b02ec9b976fb42a2 Mon Sep 17 00:00:00 2001 From: math-fehr Date: Sun, 21 Jun 2026 17:43:13 +0100 Subject: [PATCH 06/31] feat(soundness): prove survives/opErased/blocksInBounds/regionsInBounds fields Add WithCreatedOps + eraseOp survival helpers (hSurviveOp/Block/Region) and use them to discharge the bounds-preservation fields of RewrittenAt.of_fromLocalRewrite. Co-Authored-By: Claude Opus 4.8 --- Veir/PatternRewriter/Soundness.lean | 43 +++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index c2eebc678..6a0f50923 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -1830,6 +1830,27 @@ theorem RewrittenAt.of_fromLocalRewrite have hb := (hbnd (GenericPtr.block block)).mpr hblockNewCtxPat rw [PatternRewriter.eraseOp_ctx_eq herase] grind [WfRewriter.eraseOp] + -- The pattern only creates operations, so every source pointer survives into `newCtxPat`. + have hCreated : WfIRContext.WithCreatedOps rewriter.ctx newCtxPat := + hReturnCtxChanges rewriter.ctx op newCtxPat newOps newValues hpat + -- Survival of non-`op` pointers into the final context: through the insert/replace folds (`hbnd`), + -- then the final `eraseOp` (which only removes `op` and pointers it owns). + have hSurviveOp : ∀ o : OperationPtr, o ≠ op → o.InBounds newCtxPat.raw → + o.InBounds rewriter'.ctx.raw := by + intro o hne ho + have hb := (hbnd (GenericPtr.operation o)).mpr ho + rw [PatternRewriter.eraseOp_ctx_eq herase] + grind [WfRewriter.eraseOp] + have hSurviveBlock : ∀ b : BlockPtr, b.InBounds newCtxPat.raw → b.InBounds rewriter'.ctx.raw := by + intro b hb' + have hb := (hbnd (GenericPtr.block b)).mpr hb' + rw [PatternRewriter.eraseOp_ctx_eq herase] + grind [WfRewriter.eraseOp] + have hSurviveRegion : ∀ r : RegionPtr, r.InBounds newCtxPat.raw → r.InBounds rewriter'.ctx.raw := by + intro r hr' + have hb := (hbnd (GenericPtr.region r)).mpr hr' + rw [PatternRewriter.eraseOp_ctx_eq herase] + grind [WfRewriter.eraseOp] refine ⟨pre, post, blockIn, blockIn', ?_⟩ exact { -- Block-list shape: discharged for the source by the split lemma. @@ -1851,15 +1872,20 @@ theorem RewrittenAt.of_fromLocalRewrite mapResultsInBounds := by sorry -- TODO(PR 9, keystone): non-results survive (bounds transport minus erase-of-op). mapNonResultsInBounds := by sorry - -- TODO(PR 9, keystone): `eraseOp op` removes `op` from bounds of `rewriter'.ctx`. - opErased := by sorry - -- TODO(PR 9, keystone): every operation `≠ op` survives insert/replace/erase. - survives := by sorry + -- `eraseOp op` deallocates `op`, so it is no longer in bounds of `rewriter'.ctx`. + opErased := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + grind [WfRewriter.eraseOp, Rewriter.eraseOp, + OperationPtr.InBounds.ne_of_inBounds_OperationPtr_dealloc] + -- Every operation `≠ op` survives: into `newCtxPat` (pattern only creates), then the folds/erase. + survives := fun o hoIn hne => + hSurviveOp o hne (hCreated.inBounds_mono (GenericPtr.operation o) (by grind)) -- TODO(PR 9, keystone): `CrossContextFrame` under `σ`; `*_insertOp`/`*_detachOp` GetSet lemmas -- give unchanged type/props/results/successors, `replaceValue` redirects operands = `σ.applyToArray`. frame := by sorry - -- TODO(PR 9, keystone): blocks stay in bounds across the fold. - blocksInBounds := by sorry + -- Blocks stay in bounds: into `newCtxPat`, then the folds/erase (erase removes only `op`). + blocksInBounds := fun b hb => + hSurviveBlock b (hCreated.inBounds_mono (GenericPtr.block b) (by grind)) -- TODO(PR 9, keystone): parent ops of survivors preserved (op-list edits don't move other ops). parentOps := by sorry -- TODO(PR 9, keystone): `WfRewriter` ops preserve dominance well-formedness. @@ -1872,8 +1898,9 @@ theorem RewrittenAt.of_fromLocalRewrite newValuesAreResults := by sorry -- TODO(PR 9, keystone): operation-list edits leave block-argument lists untouched. blockArgsPreserved := by sorry - -- TODO(PR 9, keystone): regions stay in bounds across the fold. - regionsInBounds := by sorry + -- Regions stay in bounds: into `newCtxPat`, then the folds/erase (erase removes only `op`). + regionsInBounds := fun r hr => + hSurviveRegion r (hCreated.inBounds_mono (GenericPtr.region r) (by grind)) -- TODO(PR 9, keystone): op-list edits leave survivors' region lists untouched. opRegionsPreserved := by sorry -- TODO(PR 9, keystone): op-list edits leave region entry blocks untouched. From e291149b7e6a58b91b82a8e0d760acc6b087f8e7 Mon Sep 17 00:00:00 2001 From: math-fehr Date: Sun, 21 Jun 2026 17:46:49 +0100 Subject: [PATCH 07/31] feat(soundness): prove newOpsFresh and mappingFixesNonResults fields Add hOpBnd (bidirectional non-op bounds across the rewrite) to transport ReturnOps freshness; mappingFixesNonResults is the else-branch of rewriteMapping. Co-Authored-By: Claude Opus 4.8 --- Veir/PatternRewriter/Soundness.lean | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 6a0f50923..ac668f32e 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -1851,6 +1851,14 @@ theorem RewrittenAt.of_fromLocalRewrite have hb := (hbnd (GenericPtr.region r)).mpr hr' rw [PatternRewriter.eraseOp_ctx_eq herase] grind [WfRewriter.eraseOp] + -- Bidirectional bounds for a non-`op` operation: the folds preserve all bounds and the final + -- `eraseOp` removes only `op`, so an operation `≠ op` is in `rewriter'.ctx` iff it is in `newCtxPat`. + have hOpBnd : ∀ o : OperationPtr, o ≠ op → + (o.InBounds rewriter'.ctx.raw ↔ o.InBounds newCtxPat.raw) := by + intro o hne + have hb := hbnd (GenericPtr.operation o) + rw [PatternRewriter.eraseOp_ctx_eq herase] + grind [WfRewriter.eraseOp] refine ⟨pre, post, blockIn, blockIn', ?_⟩ exact { -- Block-list shape: discharged for the source by the split lemma. @@ -1865,9 +1873,19 @@ theorem RewrittenAt.of_fromLocalRewrite -- TODO(PR 9, keystone): `ReturnValuesInBounds` gives in-bounds of `newCtxPat`; transport -- through insert/replace/erase (bounds monotonic, none of the `newValues` is `op`'s results). newValuesInBounds := by sorry - -- TODO(PR 9, keystone): `ReturnOps` characterizes `newOps` as fresh to `newCtxPat`; transport - -- the freshness across the insert/erase fold to `rewriter'.ctx`. - newOpsFresh := by sorry + -- `ReturnOps` characterizes `newOps` as fresh to `newCtxPat`; a `newOp ≠ op` has the same bounds + -- in `newCtxPat` and `rewriter'.ctx` (`hOpBnd`), so the freshness transports. + newOpsFresh := by + intro newOp + have hfresh := hReturnOps rewriter.ctx op newCtxPat newOps newValues hpat newOp + constructor + · intro hmem + obtain ⟨h1, h2⟩ := hfresh.mp hmem + have hne : newOp ≠ op := by rintro rfl; exact h2 opInBounds + exact ⟨(hOpBnd newOp hne).mpr h1, h2⟩ + · rintro ⟨h1, h2⟩ + have hne : newOp ≠ op := by rintro rfl; exact h2 opInBounds + exact hfresh.mpr ⟨(hOpBnd newOp hne).mp h1, h2⟩ -- TODO(PR 9, keystone): from `ReturnValuesInBounds` + bounds transport. mapResultsInBounds := by sorry -- TODO(PR 9, keystone): non-results survive (bounds transport minus erase-of-op). @@ -1892,8 +1910,9 @@ theorem RewrittenAt.of_fromLocalRewrite newCtxDom := by sorry -- TODO(PR 9, keystone): `WfRewriter` ops preserve `Verified`. newCtxVerif := by sorry - -- TODO(PR 9, keystone): `σ` (`rewriteMapping`) is the identity off `op`'s results. - mappingFixesNonResults := by sorry + -- `σ` (`rewriteMapping`) is the identity off `op`'s results: it takes the `else` branch. + mappingFixesNonResults := fun v vIn hv => by + simp only [rewriteMapping, dif_neg hv] -- TODO(PR 9, keystone): each produced value is a result of some `newOp` (from the driver). newValuesAreResults := by sorry -- TODO(PR 9, keystone): operation-list edits leave block-argument lists untouched. From a927f6a81573e59f7abb805a82f5bbeb292b3955 Mon Sep 17 00:00:00 2001 From: math-fehr Date: Sun, 21 Jun 2026 17:49:52 +0100 Subject: [PATCH 08/31] feat(soundness): prove mapNonResultsInBounds field Add hSurviveVal (value survival across folds + eraseOp when the value's owner is not op) and discharge mapNonResultsInBounds via getResults! membership. Co-Authored-By: Claude Opus 4.8 --- Veir/PatternRewriter/Soundness.lean | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index ac668f32e..4f9bd7fb9 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -1859,6 +1859,15 @@ theorem RewrittenAt.of_fromLocalRewrite have hb := hbnd (GenericPtr.operation o) rw [PatternRewriter.eraseOp_ctx_eq herase] grind [WfRewriter.eraseOp] + -- Survival of a value that is not one of `op`'s results: the folds preserve bounds and `eraseOp` + -- removes only `op` and the pointers it owns (so a value whose owner is `≠ op` survives). + have hSurviveVal : ∀ v : ValuePtr, v.InBounds newCtxPat.raw → + (∀ orp : OpResultPtr, v = ValuePtr.opResult orp → orp.op ≠ op) → + v.InBounds rewriter'.ctx.raw := by + intro v hv hcond + have hb := (hbnd (GenericPtr.value v)).mpr hv + rw [PatternRewriter.eraseOp_ctx_eq herase] + grind [WfRewriter.eraseOp] refine ⟨pre, post, blockIn, blockIn', ?_⟩ exact { -- Block-list shape: discharged for the source by the split lemma. @@ -1888,8 +1897,18 @@ theorem RewrittenAt.of_fromLocalRewrite exact hfresh.mpr ⟨(hOpBnd newOp hne).mp h1, h2⟩ -- TODO(PR 9, keystone): from `ReturnValuesInBounds` + bounds transport. mapResultsInBounds := by sorry - -- TODO(PR 9, keystone): non-results survive (bounds transport minus erase-of-op). - mapNonResultsInBounds := by sorry + -- A value that is not a result of `op` survives: its owner (if any) is `≠ op`, so it is not one + -- of the pointers `eraseOp` removes. + mapNonResultsInBounds := by + intro v vIn hv + apply hSurviveVal v (hCreated.inBounds_mono (GenericPtr.value v) (by grind)) + rintro orp rfl horp + apply hv + rw [OperationPtr.getResults!.mem_iff_exists_index] + refine ⟨orp.index, by grind, ?_⟩ + rw [OperationPtr.getResult_def] + obtain ⟨o, i⟩ := orp + simp_all -- `eraseOp op` deallocates `op`, so it is no longer in bounds of `rewriter'.ctx`. opErased := by rw [PatternRewriter.eraseOp_ctx_eq herase] From d06bc7436d847a994d4b28677b5f6e541dea7c16 Mon Sep 17 00:00:00 2001 From: math-fehr Date: Sun, 21 Jun 2026 17:54:01 +0100 Subject: [PATCH 09/31] docs(soundness): categorize remaining RewrittenAt sorries Mark which fields need an extra pattern hypothesis (newValuesInBounds, mapResultsInBounds, newValuesAreResults, newCtxDom, newCtxVerif) vs deep operationList/GetSet plumbing through the foldlM (tgtList, otherBlocks, frame, parentOps, blockArgsPreserved, opRegionsPreserved, regionFirstBlockPreserved). Co-Authored-By: Claude Opus 4.8 --- Veir/PatternRewriter/Soundness.lean | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 4f9bd7fb9..96eac68db 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -1879,8 +1879,10 @@ theorem RewrittenAt.of_fromLocalRewrite otherBlocks := by sorry -- Number of produced values: directly from the pattern's `ReturnValues` obligation. newValuesSize := hReturnValues rewriter.ctx op opInBounds newCtxPat newOps newValues hpat - -- TODO(PR 9, keystone): `ReturnValuesInBounds` gives in-bounds of `newCtxPat`; transport - -- through insert/replace/erase (bounds monotonic, none of the `newValues` is `op`'s results). + -- TODO(PR 9): `ReturnValuesInBounds` gives in-bounds of `newCtxPat`; transport via `hbnd`, then + -- `eraseOp`. NEEDS EXTRA HYPOTHESIS: a `newValue` that is one of `op`'s own results would be erased, + -- so this requires the pattern to guarantee `newValues` do not reference `op`'s results (cf. + -- `hSurviveVal`, which is then directly applicable). newValuesInBounds := by sorry -- `ReturnOps` characterizes `newOps` as fresh to `newCtxPat`; a `newOp ≠ op` has the same bounds -- in `newCtxPat` and `rewriter'.ctx` (`hOpBnd`), so the freshness transports. @@ -1895,7 +1897,8 @@ theorem RewrittenAt.of_fromLocalRewrite · rintro ⟨h1, h2⟩ have hne : newOp ≠ op := by rintro rfl; exact h2 opInBounds exact hfresh.mpr ⟨(hOpBnd newOp hne).mp h1, h2⟩ - -- TODO(PR 9, keystone): from `ReturnValuesInBounds` + bounds transport. + -- TODO(PR 9): as `newValuesInBounds` (indexed form via `ReturnValues` size). NEEDS EXTRA + -- HYPOTHESIS: `newValues` must not reference `op`'s results (so they survive `eraseOp`). mapResultsInBounds := by sorry -- A value that is not a result of `op` survives: its owner (if any) is `≠ op`, so it is not one -- of the pointers `eraseOp` removes. @@ -1925,14 +1928,18 @@ theorem RewrittenAt.of_fromLocalRewrite hSurviveBlock b (hCreated.inBounds_mono (GenericPtr.block b) (by grind)) -- TODO(PR 9, keystone): parent ops of survivors preserved (op-list edits don't move other ops). parentOps := by sorry - -- TODO(PR 9, keystone): `WfRewriter` ops preserve dominance well-formedness. + -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS. Dominance/verification are not preserved by arbitrary + -- `insertOp`s, so this requires source `rewriter.ctx.Dom` plus a pattern obligation that the rewrite + -- produces a dominance-well-formed result (mirroring `PreservesSemantics`'s `ctxDom`). newCtxDom := by sorry - -- TODO(PR 9, keystone): `WfRewriter` ops preserve `Verified`. + -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS, as `newCtxDom` (source `rewriter.ctx.Verified` + pattern + -- obligation that the result is verified). newCtxVerif := by sorry -- `σ` (`rewriteMapping`) is the identity off `op`'s results: it takes the `else` branch. mappingFixesNonResults := fun v vIn hv => by simp only [rewriteMapping, dif_neg hv] - -- TODO(PR 9, keystone): each produced value is a result of some `newOp` (from the driver). + -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS. The four `Return*` props do not force `newValues` to be + -- operation results (a pattern could return a block argument), so this needs a pattern obligation. newValuesAreResults := by sorry -- TODO(PR 9, keystone): operation-list edits leave block-argument lists untouched. blockArgsPreserved := by sorry From 223395ff4e14fb325924148b66b3d00b46b94310 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Thu, 25 Jun 2026 02:55:37 +0100 Subject: [PATCH 10/31] --- Veir/Dominance.lean | 17 +- Veir/Interpreter/Refinement/Basic.lean | 33 ++ Veir/Interpreter/Refinement/Lemmas.lean | 16 + Veir/Interpreter/Refinement/Monotonicity.lean | 403 ++++++++++++++++++ Veir/Rewriter/InsertPoint.lean | 69 +++ 5 files changed, 537 insertions(+), 1 deletion(-) diff --git a/Veir/Dominance.lean b/Veir/Dominance.lean index 10ea02b9b..54fd1a10d 100644 --- a/Veir/Dominance.lean +++ b/Veir/Dominance.lean @@ -182,9 +182,24 @@ axiom WfIRContext.Dom.blockArgument_dominatesIp_entry (ctxDom : ctx.Dom) (hMem : value ∈ block.getArguments! ctx.raw) : value.dominatesIp (InsertPoint.atStart! block ctx.raw) ctx -/-- An argument of a block cannot dominate a program point that dominates hde block start. -/ +/-- An argument of a block cannot dominate a program point that dominates the block start. -/ axiom WfIRContext.Dom.blockArgument_not_dominatesIp_before_of_dominatesIp_firstOp (ctxDom : ctx.Dom) {op : OperationPtr} (opInBounds : op.InBounds ctx.raw) (opDom : op.dominatesIp (InsertPoint.atStart! block ctx.raw) ctx) (hMem : value ∈ block.getArguments! ctx.raw) : ¬ value.dominatesIp (InsertPoint.before op) ctx + +/-- A block argument dominates the end of its own block: it is in scope throughout the block body. -/ +axiom WfIRContext.Dom.blockArgument_dominatesIp_atEnd + (ctxDom : ctx.Dom) {block : BlockPtr} (blockIn : block.InBounds ctx.raw) + (hMem : value ∈ block.getArguments! ctx.raw) : + value.dominatesIp (InsertPoint.atEnd block) ctx + +/-- A result of an operation in a block body does not dominate that block's entry (the SSA +property: results are defined strictly inside the block, not before it). -/ +axiom WfIRContext.Dom.opResult_not_dominatesIp_atStart! + (ctxDom : ctx.Dom) {op : OperationPtr} (opIn : op.InBounds ctx.raw) + {block : BlockPtr} (blockIn : block.InBounds ctx.raw) + (opInBlock : op ∈ block.operationList ctx.raw ctx.wellFormed blockIn) + {r : ValuePtr} (rResult : r ∈ op.getResults! ctx.raw) : + ¬ r.dominatesIp (InsertPoint.atStart! block ctx.raw) ctx diff --git a/Veir/Interpreter/Refinement/Basic.lean b/Veir/Interpreter/Refinement/Basic.lean index 2125f103d..283c4e5da 100644 --- a/Veir/Interpreter/Refinement/Basic.lean +++ b/Veir/Interpreter/Refinement/Basic.lean @@ -1,5 +1,6 @@ import Veir.Interpreter.Basic import Veir.Data.Refinement +import Veir.Dominance /-! # Refinement of programs @@ -199,4 +200,36 @@ def InterpreterState.isRefinedBy {ctx ctx' : WfIRContext OpInfo} state.memory = state'.memory ∧ state.variables.isRefinedBy state'.variables mapping +/-- +A variable state `state` is refined by `state'` through the value renaming `mapping`, scoped to +the program points `p` (in `ctx`) and `p'` (in `ctx'`). Only values that are *in scope* at both +points are constrained: a source value `val` must dominate `p`, and its image `mapping val` must +dominate `p'`. This excuses stale values that remain in the persistent map from prior iterations +or prior blocks without constraining them. + +The relation uses `∀ sv tv` (not `∃ tv`) so existence is delegated to `DefinesDominating` +at the call site, which simplifies proof obligations at maintenance steps. +-/ +def VariableState.isRefinedByAt {ctx ctx' : WfIRContext OpInfo} + (state : VariableState ctx) (state' : VariableState ctx') + (mapping : ValueMapping ctx ctx') (p : InsertPoint) (p' : InsertPoint) + (_pIn : p.InBounds ctx.raw := by grind) (_p'In : p'.InBounds ctx'.raw := by grind) : Prop := + ∀ (val : ValuePtr) (valIn : val.InBounds ctx.raw), + val.dominatesIp p ctx → + (mapping ⟨val, valIn⟩).val.dominatesIp p' ctx' → + ∀ sv tv, state.getVar? val = some sv → + state'.getVar? (mapping ⟨val, valIn⟩) = some tv → sv ⊒ tv + +/-- +An interpreter state `state` is refined by `state'` through the value mapping `mapping`, scoped +to source point `p` and target point `p'`: they have the same memory, and the variable state of +`state` is scoped-refined by the variable state of `state'` through `mapping` at `(p, p')`. +-/ +def InterpreterState.isRefinedByAt {ctx ctx' : WfIRContext OpInfo} + (state : InterpreterState ctx) (state' : InterpreterState ctx') + (mapping : ValueMapping ctx ctx') (p : InsertPoint) (p' : InsertPoint) + (_pIn : p.InBounds ctx.raw := by grind) (_p'In : p'.InBounds ctx'.raw := by grind) : Prop := + state.memory = state'.memory ∧ + state.variables.isRefinedByAt state'.variables mapping p p' + end Veir diff --git a/Veir/Interpreter/Refinement/Lemmas.lean b/Veir/Interpreter/Refinement/Lemmas.lean index 608aa313c..1541c01bf 100644 --- a/Veir/Interpreter/Refinement/Lemmas.lean +++ b/Veir/Interpreter/Refinement/Lemmas.lean @@ -36,6 +36,22 @@ theorem InterpreterState.isRefinedBy_refl state.isRefinedBy state id := by grind [InterpreterState.isRefinedBy, VariableState.isRefinedBy] +/-- `isRefinedByAt` is reflexive under the identity mapping at any program point. -/ +@[simp, grind .] +theorem VariableState.isRefinedByAt_refl + {ctx : WfIRContext OpInfo} {state : VariableState ctx} + {p : InsertPoint} {pIn : p.InBounds ctx.raw} : + state.isRefinedByAt state id p p := by + grind [VariableState.isRefinedByAt] + +/-- `isRefinedByAt` is reflexive for `InterpreterState` under the identity mapping at any point. -/ +@[simp, grind .] +theorem InterpreterState.isRefinedByAt_refl + {ctx : WfIRContext OpInfo} {state : InterpreterState ctx} + {p : InsertPoint} {pIn : p.InBounds ctx.raw} : + state.isRefinedByAt state id p p := by + grind [InterpreterState.isRefinedByAt] + @[simp, grind .] theorem ControlFlowAction.isRefinedBy_refl (cf : ControlFlowAction) : cf ⊒ cf := by cases cf <;> simp [ControlFlowAction.isRefinedBy] diff --git a/Veir/Interpreter/Refinement/Monotonicity.lean b/Veir/Interpreter/Refinement/Monotonicity.lean index c4d331ac9..b520e2bde 100644 --- a/Veir/Interpreter/Refinement/Monotonicity.lean +++ b/Veir/Interpreter/Refinement/Monotonicity.lean @@ -1,6 +1,7 @@ import Veir.Interpreter.EquationLemma import Veir.Interpreter.Refinement.Basic import Veir.Interpreter.Refinement.Lemmas +import Veir.Dominance /-! # Monotonicity of the interpreter @@ -146,6 +147,408 @@ theorem VariableState.setArgumentValues?_isRefinedBy {ctx ctx' : WfIRContext OpC rw [VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! hσnotMem hTgt] exact htv +/-- `setArgumentValues?` preserves the *scoped* state refinement `isRefinedByAt`. + +Hypotheses compared to `setArgumentValues?_isRefinedBy`: +- `hRef` uses `isRefinedByAt` at `(p, p')` instead of unscoped `isRefinedBy` +- `hImageNotArg`: the mapping does not send a non-argument value that is *in scope at the block + entry* onto a block-argument slot. (Justified by dominance: a forwarded block argument dominates + the value it replaces, so it cannot also be dominated by it — hence no value dominating the block + entry maps onto one of the block's own arguments.) +- `ctxDom`/`ctxDom'`: the source/target contexts satisfy SSA dominance, which lets us conclude that + a non-argument value dominating the block entry already dominates the predecessor's end (via + `WfIRContext.Dom.value_dominatesIp_successor_entry`). -/ +theorem VariableState.setArgumentValues?_isRefinedByAt {ctx ctx' : WfIRContext OpCode} + {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} + {mapping : ValueMapping ctx ctx'} + {values values' : Array RuntimeValue} {newSrcVars : VariableState ctx} + (predInBounds : pred.InBounds ctx.raw) (predInBounds' : pred.InBounds ctx'.raw) + (ctxDom : ctx.Dom) (ctxDom' : ctx'.Dom) + (hRef : srcVars.isRefinedByAt tgtVars mapping (InsertPoint.atEnd pred) (InsertPoint.atEnd pred)) + (hVals : values ⊒ values') + (blockIn : block.InBounds ctx.raw) (blockIn' : block.InBounds ctx'.raw) + (blockIsSucc : block ∈ pred.getSuccessors! ctx.raw) + (blockIsSucc' : block ∈ pred.getSuccessors! ctx'.raw) + (hArgs : block.getArguments! ctx'.raw = mapping.applyToArray (block.getArguments! ctx.raw)) + /- A non-argument value that is in scope at the block entry is never mapped onto a block-argument + slot. Dischargeable from dominance: a forwarded block argument dominates the value it replaces, + so a value dominating the block entry cannot map onto one of that block's arguments. -/ + (hImageNotArg : ∀ (val : ValuePtr) (valIn : val.InBounds ctx.raw), + val ∉ block.getArguments! ctx.raw → + val.dominatesIp (InsertPoint.atStart! block ctx.raw) ctx → + (mapping ⟨val, valIn⟩).val ∉ block.getArguments! ctx'.raw) + (tgtConforms : ∀ j, j < block.getNumArguments! ctx'.raw → + (values'[j]!).Conforms ((block.getArguments! ctx'.raw)[j]!.getType! ctx'.raw)) + (hSrc : srcVars.setArgumentValues? block values blockIn = some newSrcVars) : + ∃ newTgtVars, tgtVars.setArgumentValues? block values' blockIn' = some newTgtVars ∧ + newSrcVars.isRefinedByAt newTgtVars mapping + (InsertPoint.atStart! block ctx.raw) (InsertPoint.atStart! block ctx'.raw) := by + have hNumArgs : block.getNumArguments! ctx'.raw = block.getNumArguments! ctx.raw := by + have := congrArg Array.size hArgs; simpa using this + have tgtConforms' : ∀ j, j < block.getNumArguments! ctx'.raw → + (values'[j]!).Conforms ((block.getArgument j : ValuePtr).getType! ctx'.raw) := by + intro j hj + rw [← BlockPtr.getArguments!.getElem!_eq_getArgument hj] + exact tgtConforms j hj + have ⟨newTgtVars, hTgt⟩ := + (VariableState.setArgumentValues?_isSome_iff_conforms tgtVars + (block := block) (blockInBounds := blockIn')).mp tgtConforms' + refine ⟨newTgtVars, hTgt, ?_⟩ + -- Pointwise refinement between source/target argument values. + have hPt : ∀ i, i < block.getNumArguments! ctx.raw → values[i]! ⊒ values'[i]! := by + intro i _hi + obtain ⟨hsize, hpt⟩ := hVals + by_cases h : i < values.size + · exact hpt i h + · rw [getElem!_neg values i h, getElem!_neg values' i (hsize ▸ h)] + exact RuntimeValue.isRefinedBy_refl _ + -- Prove `isRefinedByAt` at `(atStart! block ctx.raw, atStart! block ctx'.raw)`. + intro val valIn hValDom hσValDom sv tv hsv htv + by_cases hMem : val ∈ block.getArguments! ctx.raw + · -- `val` is a block argument: it was just set to `values[i]!`; target gets `values'[i]!`. + obtain ⟨i, hi, rfl⟩ := BlockPtr.getArguments!.mem_iff_exists_index.mp hMem + have hsrcval := VariableState.getVar?_getArgument_of_setArgumentValues? hi hSrc + rw [hsv] at hsrcval; obtain rfl : sv = values[i]! := (Option.some.injEq ..).mp hsrcval + have hfixI : (mapping ⟨block.getArgument i, by grind⟩).val = block.getArgument i := + ValueMapping.applyToArray_getArguments!_ext blockIn hArgs.symm i hi + rw [hfixI] at htv + have htv' := VariableState.getVar?_getArgument_of_setArgumentValues? (hNumArgs ▸ hi) hTgt + rw [htv] at htv'; obtain rfl : tv = values'[i]! := (Option.some.injEq ..).mp htv' + exact hPt i hi + · rw [VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! hMem hSrc] at hsv + -- `mapping val` is not a block argument either: `val` is in scope at the block entry, so by + -- `hImageNotArg` its image cannot land on one of the block's arguments. + have hσnotMem : (mapping ⟨val, valIn⟩).val ∉ block.getArguments! ctx'.raw := + hImageNotArg val valIn hMem hValDom + rw [VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! hσnotMem hTgt] at htv + -- A non-argument value dominating the block entry already dominates the predecessor's end, + -- by `value_dominatesIp_successor_entry` (the value is not a block argument, so the + -- "is an argument" disjunct is excluded). + have hValDomPred : val.dominatesIp (InsertPoint.atEnd pred) ctx := + (WfIRContext.Dom.value_dominatesIp_successor_entry ctxDom predInBounds blockIsSucc + hValDom).resolve_right hMem + have hσValDomPred : (mapping ⟨val, valIn⟩).val.dominatesIp (InsertPoint.atEnd pred) ctx' := + (WfIRContext.Dom.value_dominatesIp_successor_entry ctxDom' predInBounds' blockIsSucc' + hσValDom).resolve_right hσnotMem + exact hRef val valIn hValDomPred hσValDomPred sv tv hsv htv + +/-! ## Scoped (`isRefinedByAt`) variants of the monotonicity lemmas -/ + +/-- `getOperandValues` is monotone under scoped state refinement. The target operand values +exist by `DefinesDominating`; operand dominance comes from `ctx.Dom` and `ctx'.Dom`. -/ +theorem VariableState.getOperandValues_isRefinedByAt + {ctx ctx' : WfIRContext OpCode} + {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} + {mapping : ValueMapping ctx ctx'} + (opIn : op.InBounds ctx.raw) (opIn' : op'.InBounds ctx'.raw) + (hRef : srcVars.isRefinedByAt tgtVars mapping (.before op) (.before op')) + (ctxDom : ctx.Dom) + (ctxDom' : ctx'.Dom) + (hOperands : op'.getOperands! ctx'.raw = mapping.applyToArray (op.getOperands! ctx.raw)) + (tgtDef : ∀ (v : ValuePtr) (_vIn : v.InBounds ctx'.raw), + v.dominatesIp (.before op') ctx' → (tgtVars.getVar? v).isSome) + (hSrc : srcVars.getOperandValues op = some srcVal) : + ∃ tgtVal, tgtVars.getOperandValues op' = some tgtVal ∧ srcVal ⊒ tgtVal := by + simp only [VariableState.isRefinedByAt] at hRef + have ⟨hsize, hSrc'⟩ := VariableState.getOperandValues_eq_some_iff.mp hSrc + -- All target operands are defined (from `tgtDef` + target operand dominance via `ctxDom'`). + have hTgtDef : ∀ (i : Nat) (hi : i < (op'.getOperands! ctx'.raw).size), + (tgtVars.getVar? (op'.getOperands! ctx'.raw)[i]).isSome := by + intro i hi + have hmem : (op'.getOperands! ctx'.raw)[i] ∈ op'.getOperands! ctx'.raw := + Array.getElem_mem hi + have hdom : ((op'.getOperands! ctx'.raw)[i]).dominatesIp (.before op') ctx' := + ctxDom'.operand_dominates_op opIn' hmem + exact tgtDef _ (by grind) hdom + -- Hence the target operand array is fully defined, so `getOperandValues op'` succeeds. + obtain ⟨tgtVal, hTgt⟩ := Array.mapM_option_isSome (f := tgtVars.getVar?) + (l := op'.getOperands! ctx'.raw) hTgtDef + have hTgtEq : tgtVars.getOperandValues op' = some tgtVal := by + simpa [VariableState.getOperandValues] using hTgt + refine ⟨tgtVal, hTgtEq, ?_⟩ + -- Pointwise: each source operand value refines its target counterpart. + obtain ⟨htsize, hTgt'⟩ := VariableState.getOperandValues_eq_some_iff.mp hTgtEq + -- The `i`-th operand of `op'` is the image under `mapping` of the `i`-th operand of `op`. + have hOpEq : ∀ (i : Nat) (hi : i < op.getNumOperands! ctx.raw), + op'.getOperand! ctx'.raw i = (mapping ⟨op.getOperand! ctx.raw i, by grind⟩).val := by + intro i hi + simp only [ValueMapping.applyToArray, Array.ext_iff, Array.size_map, Array.size_attach, + OperationPtr.getOperands!.size_eq_getNumOperands!, Array.getElem_map, + Array.getElem_attach] at hOperands + obtain ⟨_, hpt⟩ := hOperands + have := hpt i (by grind) (by grind) + grind [OperationPtr.getOperands!, OperationPtr.getOperand!, OperationPtr.getNumOperands!] + refine ⟨by grind, ?_⟩ + intro i hi + have hi' : i < op.getNumOperands! ctx.raw := by grind + -- The `i`-th source operand, in scope at `.before op`. + have valIn : (op.getOperand! ctx.raw i).InBounds ctx.raw := by grind + have hmem : (op.getOperand! ctx.raw i) ∈ op.getOperands! ctx.raw := + OperationPtr.getOperands!.mem_getOperand hi' + have hsdom : (op.getOperand! ctx.raw i).dominatesIp (.before op) ctx := + ctxDom.operand_dominates_op opIn hmem + -- Its image is the `i`-th target operand, in scope at `.before op'`. + have hσmem : (mapping ⟨op.getOperand! ctx.raw i, valIn⟩).val ∈ op'.getOperands! ctx'.raw := by + rw [← hOpEq i hi'] + exact OperationPtr.getOperands!.mem_getOperand (by grind) + have hσdom : (mapping ⟨op.getOperand! ctx.raw i, valIn⟩).val.dominatesIp (.before op') ctx' := + ctxDom'.operand_dominates_op opIn' hσmem + have htv : tgtVars.getVar? (mapping ⟨op.getOperand! ctx.raw i, valIn⟩) = some tgtVal[i]! := by + rw [← hOpEq i hi'] + exact hTgt' i (by grind) + exact hRef (op.getOperand! ctx.raw i) valIn hsdom hσdom _ _ (hSrc' i hi') htv + +/-- `setResultValues?` preserves the scoped state refinement, advancing the scope from +`(before op, before op')` to `(after op, after op')`. Newly-in-scope results are refined +by `hVals`; pre-existing values carry through via `value_dominatesIp_after_iff`. -/ +theorem VariableState.setResultValues?_isRefinedByAt + {ctx ctx' : WfIRContext OpCode} + {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} + {mapping : ValueMapping ctx ctx'} + (opIn : op.InBounds ctx.raw) (opIn' : op'.InBounds ctx'.raw) + (hRef : srcVars.isRefinedByAt tgtVars mapping (.before op) (.before op')) + {newSrcVars : VariableState ctx} + {srcVals tgtVals : Array RuntimeValue} (hVals : srcVals ⊒ tgtVals) + (hResults : op'.getResults! ctx'.raw = mapping.applyToArray (op.getResults! ctx.raw)) + (hReflect : mapping.ReflectsResults op op') + (hSrc : srcVars.setResultValues? op srcVals opIn = some newSrcVars) + (tgtValsConforms : RuntimeValue.ArrayConforms tgtVals (op'.getResultTypes! ctx'.raw)) + (ctxDom : ctx.Dom) (ctxDom' : ctx'.Dom) + (opHasParent : (op.get! ctx.raw).parent = some block) + (opHasParent' : (op'.get! ctx'.raw).parent = some block') : + ∃ newTgtVars, tgtVars.setResultValues? op' tgtVals opIn' = some newTgtVars ∧ + newSrcVars.isRefinedByAt newTgtVars mapping + (InsertPoint.after op ctx.raw block opHasParent opIn) + (InsertPoint.after op' ctx'.raw block' opHasParent' opIn') := by + have ⟨newTgtVars, hTgt⟩ := + (VariableState.setResultValues?_isSome_iff_conforms + (varState := tgtVars) (inBounds := opIn')).mp tgtValsConforms + refine ⟨newTgtVars, hTgt, ?_⟩ + intro val valIn hValDomAfter hσValDomAfter sv tv hsv htv + -- By `value_dominatesIp_after_iff`: val dominates (before op) or is a result of op. + rw [ctxDom.value_dominatesIp_after_iff] at hValDomAfter + rw [ctxDom'.value_dominatesIp_after_iff] at hσValDomAfter + cases OperationPtr.getResults!_not_mem_or_eq_getResult ctx.raw val op with + | inl hNotMem => + -- `val` is not a result of `op`: unchanged by `setResultValues?` on both sides. + rw [VariableState.getVar?_setResultValues?_of_notMem_getResults! hNotMem hSrc] at hsv + have hσNotMem := (hReflect.not_mem_getResults valIn hNotMem) + rw [VariableState.getVar?_setResultValues?_of_notMem_getResults! hσNotMem hTgt] at htv + have hValDomBefore : val.dominatesIp (.before op) ctx := + hValDomAfter.resolve_right hNotMem + have hσValDomBefore : (mapping ⟨val, valIn⟩).val.dominatesIp (.before op') ctx' := + hσValDomAfter.resolve_right hσNotMem + exact hRef val valIn hValDomBefore hσValDomBefore sv tv hsv htv + | inr hMem => + -- `val` is a result of `op`: newly set by `setResultValues?`, refined by `hVals`. + have hfix := ValueMapping.applyToArray_getResults!_ext opIn hResults.symm + grind [RuntimeValue.arrayIsRefinedBy] + +/-- `interpretOp` is monotone under a *cross-context* scoped interpreter-state refinement. +The source/target points are `(.before op, .before op')` at entry and advance to +`(.after op, .after op')` at exit. -/ +theorem interpretOp_monotone_at + {ctx ctx' : WfIRContext OpCode} + {state : InterpreterState ctx} {state' : InterpreterState ctx'} + {mapping : ValueMapping ctx ctx'} + (opIn : op.InBounds ctx.raw) (opIn' : op'.InBounds ctx'.raw) + (hState : state.isRefinedByAt state' mapping (.before op) (.before op')) + (hPreserves : mapping.PreservesOperation op op') + (opVerif' : op'.Verified ctx' opIn') + (ctxDom : ctx.Dom) (ctxDom' : ctx'.Dom) + (tgtDefDom : state'.DefinesDominating (.before op') opIn') + (opHasParent : (op.get! ctx.raw).parent = some block) + (opHasParent' : (op'.get! ctx'.raw).parent = some block') : + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) + (r₂ : InterpreterState ctx' × Option ControlFlowAction) => + r₁.1.isRefinedByAt r₂.1 mapping + (InsertPoint.after op ctx.raw block opHasParent opIn) + (InsertPoint.after op' ctx'.raw block' opHasParent' opIn') ∧ + ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + (interpretOp op state opIn) + (interpretOp op' state' opIn') := by + -- If the source interpretation fails, then the refinement is trivial. + by_cases hsource : interpretOp op state opIn = none; simp [hsource, Interp.isRefinedBy] + -- Source/target operands are defined and refined (target existence via `tgtDefDom`). + have ⟨operands, hSrcOps⟩ : ∃ operands, state.variables.getOperandValues op = some operands := by + grind [interpretOp] + obtain ⟨operands', hTgtOps, hOpsRef⟩ := + VariableState.getOperandValues_isRefinedByAt opIn opIn' hState.2 ctxDom ctxDom' + hPreserves.operands tgtDefDom hSrcOps + have hMem : state.memory = state'.memory := hState.1 + -- Refinement of the pure `interpretOp'` on the refined operand arrays. + have hPR1 := interpretOp'_monotone (op.getOpType! ctx.raw) + (op.getProperties! ctx.raw (op.getOpType! ctx.raw)) (op.getResultTypes! ctx.raw) + operands operands' (op.getSuccessors! ctx.raw) state.memory hOpsRef + have hInterp'Eq : op'.interpret ctx'.raw operands' state'.memory = + op.interpret ctx.raw operands' state.memory := by + grind [interpretOp'_opType_cast, cases ValueMapping.PreservesOperation] + rcases hsrc : interpretOp op state opIn with _ | (⟨state₂, act⟩ | _) + · simp [Interp.isRefinedBy] + · simp only [Interp.isRefinedBy_ok_target_iff, Prod.exists] + have ⟨resValues, hinterp', hResValues⟩ := + (interpretOp_ok_iff_of_getOperandValues_eq_some hSrcOps).mp hsrc + simp only [hinterp', Interp.isRefinedBy_ok_target_iff, Prod.exists] at hPR1 + have ⟨resValues', memory'₂, act', hinterp'Tgt, resValuesRef, memoryEq, actRef⟩ := hPR1 + subst memory'₂ + simp only [← hInterp'Eq] at hinterp'Tgt + simp only [interpretOp, hTgtOps, bind, hinterp'Tgt, liftM, monadLift, MonadLift.monadLift] + have tgtConf := interpretOp'_results_conform (opInBounds := opIn') opVerif' + (VariableState.getOperandValues_conforms hTgtOps) hinterp'Tgt + -- Bind the results on both sides; the scoped relation advances to `(after op, after op')`. + obtain ⟨newTgtVars, hSetTgt, hRefAt⟩ := + VariableState.setResultValues?_isRefinedByAt opIn opIn' hState.2 resValuesRef + hPreserves.results hPreserves.reflect hResValues tgtConf ctxDom ctxDom' + opHasParent opHasParent' + simp only [Interp, hSetTgt, pure, Option.some.injEq, UBOr.ok.injEq, Prod.mk.injEq] + grind [InterpreterState.isRefinedByAt] + · simp + +/-- `interpretOpList` is monotone under scoped state refinement, over an *identical* op list that +forms an operation chain slice of `block`/`block'` in both contexts. The scoped invariant is carried +through each step by `interpretOp_monotone_at`, the target `DefinesDominating` invariant by +`interpretOp_DefinesDominating`, and the scope point advanced across each op via the chain +`.next`-link. The end point is `InsertPoint.afterLast ops _ p`: the point after the last operation +(or the start point `p`/`p'` if the list is empty). Only the **last** operation may produce a +control-flow action (`hOnlyLastAction`), so an early stop cannot leave the result state at a scope +point past where interpretation actually halted. -/ +theorem interpretOpList_monoAt + {ctx ctx' : WfIRContext OpCode} (hVerif : ctx'.Verified) + (ctxDom : ctx.Dom) (ctxDom' : ctx'.Dom) + {block block' : BlockPtr} {ops : List OperationPtr} + (opsInBounds : ∀ op, op ∈ ops → op.InBounds ctx.raw) + (opsInBounds' : ∀ op, op ∈ ops → op.InBounds ctx'.raw) + (hChain : block.OpChainSlice ctx.raw ops) + (hChain' : block'.OpChainSlice ctx'.raw ops) + {mapping : ValueMapping ctx ctx'} + {state : InterpreterState ctx} {state' : InterpreterState ctx'} + {p p' : InsertPoint} + (pIn : p.InBounds ctx.raw) (p'In : p'.InBounds ctx'.raw) + (hState : state.isRefinedByAt state' mapping p p') + (tgtDefDom : state'.DefinesDominating p' p'In) + (hPreserves : ∀ op, (h : op ∈ ops) → mapping.PreservesOperation op op) + (hPointsHead : ∀ (h : ops ≠ []), p = .before (ops.head h) ∧ p' = .before (ops.head h)) + (hOnlyLastAction : ∀ op, op ∈ ops.dropLast → + ∀ (st : InterpreterState ctx) (stIn : op.InBounds ctx.raw) + (st' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOp op st stIn ≠ some (.ok (st', some cf))) : + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) + (r₂ : InterpreterState ctx' × Option ControlFlowAction) => + r₁.1.isRefinedByAt r₂.1 mapping + (InsertPoint.afterLast ops ctx.raw p) (InsertPoint.afterLast ops ctx'.raw p') ∧ + ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + (interpretOpList ops state opsInBounds) + (interpretOpList ops state' opsInBounds') := by + induction ops generalizing state state' p p' with + | nil => + simp only [interpretOpList_nil, InsertPoint.afterLast_nil, Interp.isRefinedBy_ok_target_iff] + exact ⟨_, rfl, hState, by simp [ControlFlowAction.optionIsRefinedBy]⟩ + | cons a l ih => + rw [BlockPtr.OpChainSlice.cons_iff] at hChain hChain' + obtain ⟨aIn, aParent, aNext, hChainL⟩ := hChain + obtain ⟨aIn', aParent', aNext', hChainL'⟩ := hChain' + obtain ⟨hpa, hpa'⟩ := hPointsHead (by simp) + simp only [List.head_cons] at hpa hpa' + subst hpa hpa' + -- Advance the end point past the head: `afterLast (a :: l)` from `.before a` is + -- `afterLast l` from `after a`, matching how the recursion advances the start point. + simp only [InsertPoint.afterLast_cons_after l (.before a) aParent aIn, + InsertPoint.afterLast_cons_after l (.before a) aParent' aIn'] + have refinesHead := interpretOp_monotone_at aIn aIn' hState + (hPreserves a (by simp)) (by grind) ctxDom ctxDom' tgtDefDom aParent aParent' + simp only [interpretOpList_cons] + rcases hsrc : interpretOp a state aIn with _ | (⟨s, act⟩ | _) + · simp [Interp.isRefinedBy] + · simp only [hsrc, Interp.isRefinedBy_ok_target_iff] at refinesHead + obtain ⟨⟨s', act'⟩, htgt, hsRef, hactRef⟩ := refinesHead + simp only [htgt] + cases act with + | none => + have hact' : act' = none := by grind [ControlFlowAction.optionIsRefinedBy] + subst hact' + simp only + have tgtDomAfter : s'.DefinesDominating (InsertPoint.after a ctx'.raw block') := + interpretOp_DefinesDominating ctxDom' tgtDefDom aParent' htgt + have hdropSub : ∀ op, op ∈ l.dropLast → op ∈ (a :: l).dropLast := by + intro op hop + cases l with + | nil => simp at hop + | cons b rest => simp_all [List.dropLast] + refine ih (hChain := hChainL) (hChain' := hChainL') + (p := InsertPoint.after a ctx.raw block aParent aIn) + (p' := InsertPoint.after a ctx'.raw block' aParent' aIn') + (pIn := by grind) (p'In := by grind) (hState := hsRef) (tgtDefDom := tgtDomAfter) + (hPreserves := fun op hop => hPreserves op (List.mem_cons_of_mem a hop)) + (hPointsHead := ?_) + (hOnlyLastAction := fun op hop => hOnlyLastAction op (hdropSub op hop)) + · intro hl + have hb : l.head? = some (l.head hl) := by + cases l with + | nil => exact absurd rfl hl + | cons b rest => rfl + exact ⟨InsertPoint.after_eq_of_some_next (aNext _ hb), + InsertPoint.after_eq_of_some_next (aNext' _ hb)⟩ + | some cf => + obtain rfl : l = [] := by + rcases l with _ | ⟨b, rest⟩ + · rfl + · exact absurd hsrc (hOnlyLastAction a (by simp [List.dropLast]) state aIn s cf) + -- `afterLast [] _ (after a ..)` is just `after a ..`, where `hsRef` already lands. + have ⟨cf', hact', hcfRef⟩ : ∃ cf', act' = some cf' ∧ cf.isRefinedBy cf' := by + cases act' <;> simp_all [ControlFlowAction.optionIsRefinedBy] + subst hact' + simp [Interp, hsRef, InsertPoint.afterLast_nil, ControlFlowAction.optionIsRefinedBy, hcfRef] + · simp + +/-- `interpretTerminatedOpList` is monotone under scoped state refinement. Wraps +`interpretOpList_monoAt`; the terminator is the last operation, so its action coincides with the end +point `InsertPoint.afterLast ops _ p`. -/ +theorem interpretTerminatedOpList_monoAt + {ctx ctx' : WfIRContext OpCode} (ctx'Verif : ctx'.Verified) + (ctxDom : ctx.Dom) (ctxDom' : ctx'.Dom) + {block block' : BlockPtr} {ops : List OperationPtr} + (opsInBounds : ∀ op, op ∈ ops → op.InBounds ctx.raw) + (opsInBounds' : ∀ op, op ∈ ops → op.InBounds ctx'.raw) + (hChain : block.OpChainSlice ctx.raw ops) + (hChain' : block'.OpChainSlice ctx'.raw ops) + {state : InterpreterState ctx} {state' : InterpreterState ctx'} + {mapping : ValueMapping ctx ctx'} {p p' : InsertPoint} + (pIn : p.InBounds ctx.raw) (p'In : p'.InBounds ctx'.raw) + (hState : state.isRefinedByAt state' mapping p p') + (tgtDefDom : state'.DefinesDominating p' p'In) + (hFrame : ∀ op, (h : op ∈ ops) → mapping.PreservesOperation op op) + (hPointsHead : ∀ (h : ops ≠ []), p = .before (ops.head h) ∧ p' = .before (ops.head h)) + (hOnlyLastAction : ∀ op, op ∈ ops.dropLast → + ∀ (st : InterpreterState ctx) (stIn : op.InBounds ctx.raw) + (st' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOp op st stIn ≠ some (.ok (st', some cf))) : + Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × ControlFlowAction) + (r₂ : InterpreterState ctx' × ControlFlowAction) => + r₁.1.isRefinedByAt r₂.1 mapping + (InsertPoint.afterLast ops ctx.raw p) (InsertPoint.afterLast ops ctx'.raw p') ∧ + r₁.2.isRefinedBy r₂.2) + (interpretTerminatedOpList ops state opsInBounds) + (interpretTerminatedOpList ops state' opsInBounds') := by + have hList := interpretOpList_monoAt ctx'Verif ctxDom ctxDom' opsInBounds opsInBounds' + hChain hChain' pIn p'In hState tgtDefDom hFrame hPointsHead hOnlyLastAction + simp only [interpretTerminatedOpList, bind] + rcases hsrc : interpretOpList ops state opsInBounds with _ | (⟨s, act⟩ | _) + · simp [Interp.isRefinedBy] + · simp only [hsrc, Interp.isRefinedBy_ok_target_iff] at hList + obtain ⟨⟨s', act'⟩, htgt, hsRef, hactRef⟩ := hList + simp only [htgt] + cases act with + | none => simp [Interp.isRefinedBy] + | some cf => + have ⟨cf', hact', hcfRef⟩ : ∃ cf', act' = some cf' ∧ cf.isRefinedBy cf' := by + cases act' <;> simp_all [ControlFlowAction.optionIsRefinedBy] + subst hact' + exact ⟨hsRef, hcfRef⟩ + · exact Interp.isRefinedBy_ub_target + /-- `interpretOp` is monotone under a *cross-context* interpreter-state refinement. diff --git a/Veir/Rewriter/InsertPoint.lean b/Veir/Rewriter/InsertPoint.lean index a97616783..ffc505371 100644 --- a/Veir/Rewriter/InsertPoint.lean +++ b/Veir/Rewriter/InsertPoint.lean @@ -81,11 +81,24 @@ def InsertPoint.after? (op : OperationPtr) (ctx : IRContext OpInfo) : Option Ins | none => some (.atEnd block) | none => none +/-- Witness-free variant of `InsertPoint.after`: the point just after `op` in its parent block, +falling back to `before op` when `op` has no parent (out of bounds). Equals `InsertPoint.after` +whenever `op` has a parent. -/ +def InsertPoint.after! (op : OperationPtr) (ctx : IRContext OpInfo) : InsertPoint := + (InsertPoint.after? op ctx).getD (.before op) + theorem InsertPoint.after?_eq_some_of_after_eq : InsertPoint.after op ctx block h₁ h₂ = ip → InsertPoint.after? op ctx = some ip := by grind [InsertPoint.after, InsertPoint.after?] +theorem InsertPoint.after!_eq_after + (hblock : (op.get! ctx).parent = some block) (hop : op.InBounds ctx) : + InsertPoint.after! op ctx = InsertPoint.after op ctx block hblock hop := by + have h := InsertPoint.after?_eq_some_of_after_eq + (ip := InsertPoint.after op ctx block hblock hop) rfl + simp [InsertPoint.after!, h] + theorem InsertPoint.after?_eq_of_after?_eq_some (h : InsertPoint.after? op ctx = some ip) (hblock : (op.get! ctx).parent = some block) @@ -98,6 +111,62 @@ theorem InsertPoint.after_inBounds (ctxWf : ctx.WellFormed) : (InsertPoint.after op ctx blockPtr opHasParent opInBounds).InBounds ctx := by grind [InsertPoint.after] +@[grind .] +theorem InsertPoint.after!_inBounds (ctxWf : ctx.WellFormed) + (hblock : (op.get! ctx).parent = some block) (hop : op.InBounds ctx) : + (InsertPoint.after! op ctx).InBounds ctx := by + rw [InsertPoint.after!_eq_after hblock hop] + exact InsertPoint.after_inBounds ctxWf + +/-- The point just after the **last** operation of `ops` (witness-free, via `InsertPoint.after!`), +or `fallback` when `ops` is empty. Characterizes where interpretation of an operation list ends: +`fallback` is the start point, advanced past the whole list. -/ +@[expose] +def InsertPoint.afterLast (ops : List OperationPtr) (ctx : IRContext OpInfo) + (fallback : InsertPoint) : InsertPoint := + match ops.getLast? with + | some last => InsertPoint.after! last ctx + | none => fallback + +@[simp, grind =] +theorem InsertPoint.afterLast_nil (ctx : IRContext OpInfo) (fallback : InsertPoint) : + InsertPoint.afterLast [] ctx fallback = fallback := rfl + +@[simp, grind =] +theorem InsertPoint.afterLast_singleton (a : OperationPtr) (ctx : IRContext OpInfo) + (fallback : InsertPoint) : + InsertPoint.afterLast [a] ctx fallback = InsertPoint.after! a ctx := rfl + +/-- The end point of `a :: l` is the end point of `l` started from just after `a` (the fallback is +only consulted for the empty list, so advancing it past `a` is harmless). -/ +theorem InsertPoint.afterLast_cons (a : OperationPtr) (l : List OperationPtr) + (ctx : IRContext OpInfo) (fallback : InsertPoint) : + InsertPoint.afterLast (a :: l) ctx fallback + = InsertPoint.afterLast l ctx (InsertPoint.after! a ctx) := by + cases l <;> rfl + +/-- `afterLast_cons` phrased with the witnessed `InsertPoint.after`, matching how the recursion +advances the start point across each operation. -/ +theorem InsertPoint.afterLast_cons_after (l : List OperationPtr) (fallback : InsertPoint) + (hblock : (a.get! ctx).parent = some block) (hop : a.InBounds ctx) : + InsertPoint.afterLast (a :: l) ctx fallback + = InsertPoint.afterLast l ctx (InsertPoint.after a ctx block hblock hop) := by + rw [InsertPoint.afterLast_cons, InsertPoint.after!_eq_after hblock hop] + +@[grind .] +theorem InsertPoint.afterLast_inBounds {ops : List OperationPtr} (ctxWf : ctx.WellFormed) + (hfb : fallback.InBounds ctx) + (hparent : ∀ op ∈ ops, ∃ block, (op.get! ctx).parent = some block) + (hin : ∀ op ∈ ops, op.InBounds ctx) : + (InsertPoint.afterLast ops ctx fallback).InBounds ctx := by + unfold InsertPoint.afterLast + cases h : ops.getLast? with + | none => exact hfb + | some last => + have hmem := List.mem_of_getLast? h + obtain ⟨block, hb⟩ := hparent last hmem + exact InsertPoint.after!_inBounds ctxWf hb (hin last hmem) + theorem InsertPoint.after_eq_of_some_next : (op.get! ctx).next = some nextOp → InsertPoint.after op ctx blockPtr opHasParent opInBounds = .before nextOp := by From 2377b3c110c8ca9410540f99a5c70d625273448b Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Thu, 25 Jun 2026 15:28:14 +0100 Subject: [PATCH 11/31] --- Veir/Interpreter/Refinement/Monotonicity.lean | 34 ++++---- Veir/PatternRewriter/Semantics.lean | 2 +- Veir/PatternRewriter/Soundness.lean | 78 +++++++++++++++---- 3 files changed, 76 insertions(+), 38 deletions(-) diff --git a/Veir/Interpreter/Refinement/Monotonicity.lean b/Veir/Interpreter/Refinement/Monotonicity.lean index b520e2bde..19f1a1f0b 100644 --- a/Veir/Interpreter/Refinement/Monotonicity.lean +++ b/Veir/Interpreter/Refinement/Monotonicity.lean @@ -149,26 +149,25 @@ theorem VariableState.setArgumentValues?_isRefinedBy {ctx ctx' : WfIRContext OpC /-- `setArgumentValues?` preserves the *scoped* state refinement `isRefinedByAt`. +The input relation `hRef` holds at the block entry `(atStart! block, atStart! block)`. On the +pre-argument input state the block's own arguments are not yet defined, so `isRefinedByAt` at the +entry constrains them only vacuously; it does constrain the non-argument values already in scope at +the entry, which is exactly what the proof reuses for the surviving (non-argument) values. + Hypotheses compared to `setArgumentValues?_isRefinedBy`: -- `hRef` uses `isRefinedByAt` at `(p, p')` instead of unscoped `isRefinedBy` +- `hRef` uses `isRefinedByAt` at the block entry instead of unscoped `isRefinedBy`. - `hImageNotArg`: the mapping does not send a non-argument value that is *in scope at the block entry* onto a block-argument slot. (Justified by dominance: a forwarded block argument dominates the value it replaces, so it cannot also be dominated by it — hence no value dominating the block - entry maps onto one of the block's own arguments.) -- `ctxDom`/`ctxDom'`: the source/target contexts satisfy SSA dominance, which lets us conclude that - a non-argument value dominating the block entry already dominates the predecessor's end (via - `WfIRContext.Dom.value_dominatesIp_successor_entry`). -/ + entry maps onto one of the block's own arguments.) -/ theorem VariableState.setArgumentValues?_isRefinedByAt {ctx ctx' : WfIRContext OpCode} {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} {mapping : ValueMapping ctx ctx'} {values values' : Array RuntimeValue} {newSrcVars : VariableState ctx} - (predInBounds : pred.InBounds ctx.raw) (predInBounds' : pred.InBounds ctx'.raw) - (ctxDom : ctx.Dom) (ctxDom' : ctx'.Dom) - (hRef : srcVars.isRefinedByAt tgtVars mapping (InsertPoint.atEnd pred) (InsertPoint.atEnd pred)) - (hVals : values ⊒ values') (blockIn : block.InBounds ctx.raw) (blockIn' : block.InBounds ctx'.raw) - (blockIsSucc : block ∈ pred.getSuccessors! ctx.raw) - (blockIsSucc' : block ∈ pred.getSuccessors! ctx'.raw) + (hRef : srcVars.isRefinedByAt tgtVars mapping + (InsertPoint.atStart! block ctx.raw) (InsertPoint.atStart! block ctx'.raw)) + (hVals : values ⊒ values') (hArgs : block.getArguments! ctx'.raw = mapping.applyToArray (block.getArguments! ctx.raw)) /- A non-argument value that is in scope at the block entry is never mapped onto a block-argument slot. Dischargeable from dominance: a forwarded block argument dominates the value it replaces, @@ -221,16 +220,9 @@ theorem VariableState.setArgumentValues?_isRefinedByAt {ctx ctx' : WfIRContext O have hσnotMem : (mapping ⟨val, valIn⟩).val ∉ block.getArguments! ctx'.raw := hImageNotArg val valIn hMem hValDom rw [VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! hσnotMem hTgt] at htv - -- A non-argument value dominating the block entry already dominates the predecessor's end, - -- by `value_dominatesIp_successor_entry` (the value is not a block argument, so the - -- "is an argument" disjunct is excluded). - have hValDomPred : val.dominatesIp (InsertPoint.atEnd pred) ctx := - (WfIRContext.Dom.value_dominatesIp_successor_entry ctxDom predInBounds blockIsSucc - hValDom).resolve_right hMem - have hσValDomPred : (mapping ⟨val, valIn⟩).val.dominatesIp (InsertPoint.atEnd pred) ctx' := - (WfIRContext.Dom.value_dominatesIp_successor_entry ctxDom' predInBounds' blockIsSucc' - hσValDom).resolve_right hσnotMem - exact hRef val valIn hValDomPred hσValDomPred sv tv hsv htv + -- The surviving value `val` is in scope at the block entry on both sides, so the entry-point + -- input relation `hRef` constrains it directly. + exact hRef val valIn hValDom hσValDom sv tv hsv htv /-! ## Scoped (`isRefinedByAt`) variants of the monotonicity lemmas -/ diff --git a/Veir/PatternRewriter/Semantics.lean b/Veir/PatternRewriter/Semantics.lean index 85da60365..c54da985d 100644 --- a/Veir/PatternRewriter/Semantics.lean +++ b/Veir/PatternRewriter/Semantics.lean @@ -147,7 +147,7 @@ def LocalRewritePattern.PreservesSemantics ∀ newState cf, interpretOp op state = some (newState, cf) → ∀ sourceValues, (op.getResults ctx.raw).mapM (newState.variables.getVar? ·) = some sourceValues → ∀ (state' : InterpreterState newCtx), state'.EquationLemmaAt (InsertPoint.before op) → - state.isRefinedBy state' (LocalRewritePattern.mapping hpattern) → + state.isRefinedByAt state' (LocalRewritePattern.mapping hpattern) (.before op) (.before op) → ∃ newState', interpretOpList newOps.toList state' (by grind [ReturnOps]) = some (newState', cf) ∧ newState.memory = newState'.memory ∧ diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 96eac68db..26464effb 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -525,13 +525,19 @@ def OpStepSimulation {ctx newCtx : WfIRContext OpCode} (op : OperationPtr) (newOps : Array OperationPtr) (μ : ValueMapping ctx newCtx) (opIn : op.InBounds ctx.raw) (newOpsIn' : ∀ o ∈ newOps.toList, o.InBounds newCtx.raw) : Prop := - ∀ (s : InterpreterState ctx) (s' : InterpreterState newCtx), - s.isRefinedBy s' μ → + ∀ (s : InterpreterState ctx) (s' : InterpreterState newCtx) + (p' : InsertPoint) (p'In : p'.InBounds newCtx.raw) + (qIn : (InsertPoint.after! op ctx.raw).InBounds ctx.raw) + (q'In : (InsertPoint.afterLast newOps.toList newCtx.raw p').InBounds newCtx.raw), + s.isRefinedByAt s' μ (InsertPoint.before op) p' → s.EquationLemmaAt (InsertPoint.before op) → Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) (r₂ : InterpreterState newCtx × Option ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 μ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + r₁.1.isRefinedByAt r₂.1 μ + (InsertPoint.after! op ctx.raw) (InsertPoint.afterLast newOps.toList newCtx.raw p') + qIn q'In ∧ + ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) (interpretOp op s) (interpretOpList newOps.toList s' newOpsIn') @@ -754,39 +760,71 @@ theorem interpretOpList_singleton {ctx : WfIRContext OpCode} {op : OperationPtr} · cases a <;> simp [interpretOpList_nil] · rfl +/-- The end point of `l₁ ++ l₂` is the end point of `l₂` started from the end point of `l₁`. -/ +theorem InsertPoint.afterLast_append (l₁ l₂ : List OperationPtr) (ctx : IRContext OpInfo) + (fb : InsertPoint) : + InsertPoint.afterLast (l₁ ++ l₂) ctx fb + = InsertPoint.afterLast l₂ ctx (InsertPoint.afterLast l₁ ctx fb) := by + induction l₁ generalizing fb with + | nil => simp + | cons a xs ih => + simp only [List.cons_append, InsertPoint.afterLast_cons] + exact ih _ + /-- -Sequential composition of cross-context refinement over `interpretOpList`. If interpreting the -prefix `l₁`/`m₁` refines (`hPrefix`), and — whenever the prefixes both run to completion without a -terminator into `σ`-refined states — interpreting the suffixes `l₂`/`m₂` refines (`hCont`), then -interpreting `l₁ ++ l₂` refines `m₁ ++ m₂`. The target must make progress under a UB source -(`hTgtProgress`), which the caller discharges from the verified, SSA-well-formed target chain. +Sequential composition of *scoped* cross-context refinement over `interpretOpList`. If interpreting +the prefix `l₁`/`m₁` refines at the points `(afterLast l₁ p, afterLast m₁ p')` (`hPrefix`), and — +whenever the prefixes both run to completion without a terminator into scoped-refined states — +interpreting the suffixes `l₂`/`m₂` refines at the final points (`hCont`), then interpreting +`l₁ ++ l₂` refines `m₁ ++ m₂` at the final points. + +The prefix may terminate (produce a control-flow action) only when the source suffix `l₂` is empty +(`hPreNoTerm`), in which case the target suffix `m₂` is also empty (`hM2Nil`); this keeps the result +scope point pinned to the prefix end, which is then the final point. -/ theorem isRefinedBy_interpretOpList_seqCompose {ctx ctx' : WfIRContext OpCode} {μ : ValueMapping ctx ctx'} {l₁ l₂ m₁ m₂ : List OperationPtr} {s : InterpreterState ctx} {s' : InterpreterState ctx'} + {p p' : InsertPoint} + (qIn : (InsertPoint.afterLast l₁ ctx.raw p).InBounds ctx.raw) + (q'In : (InsertPoint.afterLast m₁ ctx'.raw p').InBounds ctx'.raw) + (rIn : (InsertPoint.afterLast (l₁ ++ l₂) ctx.raw p).InBounds ctx.raw) + (r'In : (InsertPoint.afterLast (m₁ ++ m₂) ctx'.raw p').InBounds ctx'.raw) (hl : ∀ o ∈ l₁ ++ l₂, o.InBounds ctx.raw) (hm : ∀ o ∈ m₁ ++ m₂, o.InBounds ctx'.raw) + (hM2Nil : l₂ = [] → m₂ = []) + (hPreNoTerm : l₂ ≠ [] → ∀ s2 cf, + interpretOpList l₁ s (fun o ho => hl o (List.mem_append_left _ ho)) ≠ some (.ok (s2, some cf))) (hPrefix : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) (r₂ : InterpreterState ctx' × Option ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 μ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + r₁.1.isRefinedByAt r₂.1 μ + (InsertPoint.afterLast l₁ ctx.raw p) (InsertPoint.afterLast m₁ ctx'.raw p') qIn q'In ∧ + ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) (interpretOpList l₁ s (fun o ho => hl o (List.mem_append_left _ ho))) (interpretOpList m₁ s' (fun o ho => hm o (List.mem_append_left _ ho)))) (hCont : ∀ (s2 : InterpreterState ctx) (s2' : InterpreterState ctx'), - s2.isRefinedBy s2' μ → + s2.isRefinedByAt s2' μ + (InsertPoint.afterLast l₁ ctx.raw p) (InsertPoint.afterLast m₁ ctx'.raw p') qIn q'In → interpretOpList l₁ s (fun o ho => hl o (List.mem_append_left _ ho)) = some (.ok (s2, none)) → interpretOpList m₁ s' (fun o ho => hm o (List.mem_append_left _ ho)) = some (.ok (s2', none)) → Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) (r₂ : InterpreterState ctx' × Option ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 μ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + r₁.1.isRefinedByAt r₂.1 μ + (InsertPoint.afterLast (l₁ ++ l₂) ctx.raw p) + (InsertPoint.afterLast (m₁ ++ m₂) ctx'.raw p') rIn r'In ∧ + ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) (interpretOpList l₂ s2 (fun o ho => hl o (List.mem_append_right _ ho))) (interpretOpList m₂ s2' (fun o ho => hm o (List.mem_append_right _ ho)))) : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) (r₂ : InterpreterState ctx' × Option ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 μ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + r₁.1.isRefinedByAt r₂.1 μ + (InsertPoint.afterLast (l₁ ++ l₂) ctx.raw p) + (InsertPoint.afterLast (m₁ ++ m₂) ctx'.raw p') rIn r'In ∧ + ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) (interpretOpList (l₁ ++ l₂) s hl) (interpretOpList (m₁ ++ m₂) s' hm) := by rw [interpretOpList_append, interpretOpList_append] @@ -803,10 +841,18 @@ theorem isRefinedBy_interpretOpList_seqCompose subst ha' exact hCont s2 s2' hsRef hsrc htgt | some cf => - obtain ⟨cf', ha', hcf⟩ : ∃ cf', a' = some cf' ∧ cf.isRefinedBy cf' := by - cases a' <;> simp_all [ControlFlowAction.optionIsRefinedBy] - subst ha' - exact ⟨hsRef, hcf⟩ + -- The prefix terminated: only possible when `l₂ = []` (else `hPreNoTerm`), and then `m₂ = []`. + by_cases hl2 : l₂ = [] + · obtain rfl := hl2 + obtain rfl := hM2Nil rfl + obtain ⟨cf', ha', hcf⟩ : ∃ cf', a' = some cf' ∧ cf.isRefinedBy cf' := by + cases a' <;> simp_all [ControlFlowAction.optionIsRefinedBy] + subst ha' + simp only [interpretOpList_nil, Interp.isRefinedBy_ok_target_iff] + refine ⟨_, rfl, ?_, by simpa [ControlFlowAction.optionIsRefinedBy] using hcf⟩ + -- The result point `afterLast (l₁ ++ []) = afterLast l₁`, where `hsRef` already lands. + simpa using hsRef + · exact absurd hsrc (hPreNoTerm hl2 s2 cf) · simp /-- From 72ab4f714d77cc1f3c028b8bd7063e251f0c7d93 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Sat, 27 Jun 2026 01:43:50 +0200 Subject: [PATCH 12/31] --- Veir/Interpreter/Refinement/Monotonicity.lean | 36 +-- Veir/PatternRewriter/Soundness.lean | 271 ++++++++++-------- 2 files changed, 176 insertions(+), 131 deletions(-) diff --git a/Veir/Interpreter/Refinement/Monotonicity.lean b/Veir/Interpreter/Refinement/Monotonicity.lean index 19f1a1f0b..dc3872c7d 100644 --- a/Veir/Interpreter/Refinement/Monotonicity.lean +++ b/Veir/Interpreter/Refinement/Monotonicity.lean @@ -421,10 +421,9 @@ theorem interpretOpList_monoAt (tgtDefDom : state'.DefinesDominating p' p'In) (hPreserves : ∀ op, (h : op ∈ ops) → mapping.PreservesOperation op op) (hPointsHead : ∀ (h : ops ≠ []), p = .before (ops.head h) ∧ p' = .before (ops.head h)) - (hOnlyLastAction : ∀ op, op ∈ ops.dropLast → - ∀ (st : InterpreterState ctx) (stIn : op.InBounds ctx.raw) - (st' : InterpreterState ctx) (cf : ControlFlowAction), - interpretOp op st stIn ≠ some (.ok (st', some cf))) : + (hInitNoCf : ∀ (s2 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList ops.dropLast state + (fun o ho => opsInBounds o (List.dropLast_subset ops ho)) ≠ some (.ok (s2, some cf))) : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) (r₂ : InterpreterState ctx' × Option ControlFlowAction) => @@ -463,18 +462,22 @@ theorem interpretOpList_monoAt simp only have tgtDomAfter : s'.DefinesDominating (InsertPoint.after a ctx'.raw block') := interpretOp_DefinesDominating ctxDom' tgtDefDom aParent' htgt - have hdropSub : ∀ op, op ∈ l.dropLast → op ∈ (a :: l).dropLast := by - intro op hop - cases l with - | nil => simp at hop - | cons b rest => simp_all [List.dropLast] refine ih (hChain := hChainL) (hChain' := hChainL') (p := InsertPoint.after a ctx.raw block aParent aIn) (p' := InsertPoint.after a ctx'.raw block' aParent' aIn') (pIn := by grind) (p'In := by grind) (hState := hsRef) (tgtDefDom := tgtDomAfter) (hPreserves := fun op hop => hPreserves op (List.mem_cons_of_mem a hop)) (hPointsHead := ?_) - (hOnlyLastAction := fun op hop => hOnlyLastAction op (hdropSub op hop)) + (hInitNoCf := ?_) + rotate_left + · -- The tail's init segment never branches: the head `a` ran without an action, so a tail + -- branch would make the head's init segment `(a :: l).dropLast` branch — contra `hInitNoCf`. + intro s2 cf hcontra + rcases l with _ | ⟨b, rest⟩ + · simp only [List.dropLast_nil, interpretOpList_nil] at hcontra; grind + · refine hInitNoCf s2 cf ?_ + simp only [List.dropLast_cons_cons, interpretOpList_cons, hsrc] + exact hcontra · intro hl have hb : l.head? = some (l.head hl) := by cases l with @@ -486,7 +489,9 @@ theorem interpretOpList_monoAt obtain rfl : l = [] := by rcases l with _ | ⟨b, rest⟩ · rfl - · exact absurd hsrc (hOnlyLastAction a (by simp [List.dropLast]) state aIn s cf) + · exfalso + apply hInitNoCf s cf + simp only [List.dropLast_cons_cons, interpretOpList_cons, hsrc] -- `afterLast [] _ (after a ..)` is just `after a ..`, where `hsRef` already lands. have ⟨cf', hact', hcfRef⟩ : ∃ cf', act' = some cf' ∧ cf.isRefinedBy cf' := by cases act' <;> simp_all [ControlFlowAction.optionIsRefinedBy] @@ -512,10 +517,9 @@ theorem interpretTerminatedOpList_monoAt (tgtDefDom : state'.DefinesDominating p' p'In) (hFrame : ∀ op, (h : op ∈ ops) → mapping.PreservesOperation op op) (hPointsHead : ∀ (h : ops ≠ []), p = .before (ops.head h) ∧ p' = .before (ops.head h)) - (hOnlyLastAction : ∀ op, op ∈ ops.dropLast → - ∀ (st : InterpreterState ctx) (stIn : op.InBounds ctx.raw) - (st' : InterpreterState ctx) (cf : ControlFlowAction), - interpretOp op st stIn ≠ some (.ok (st', some cf))) : + (hInitNoCf : ∀ (s2 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList ops.dropLast state + (fun o ho => opsInBounds o (List.dropLast_subset ops ho)) ≠ some (.ok (s2, some cf))) : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × ControlFlowAction) (r₂ : InterpreterState ctx' × ControlFlowAction) => @@ -525,7 +529,7 @@ theorem interpretTerminatedOpList_monoAt (interpretTerminatedOpList ops state opsInBounds) (interpretTerminatedOpList ops state' opsInBounds') := by have hList := interpretOpList_monoAt ctx'Verif ctxDom ctxDom' opsInBounds opsInBounds' - hChain hChain' pIn p'In hState tgtDefDom hFrame hPointsHead hOnlyLastAction + hChain hChain' pIn p'In hState tgtDefDom hFrame hPointsHead hInitNoCf simp only [interpretTerminatedOpList, bind] rcases hsrc : interpretOpList ops state opsInBounds with _ | (⟨s, act⟩ | _) · simp [Interp.isRefinedBy] diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 26464effb..f221dd502 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -855,137 +855,178 @@ theorem isRefinedBy_interpretOpList_seqCompose · exact absurd hsrc (hPreNoTerm hl2 s2 cf) · simp +/-- For a block whose op-chain starts with a slice `l ++ [x]`, running `l` from the block entry +`atStart!` ends at the point just before `x`. Used to bridge the scoped `interpretOpList_monoAt` +end point (`afterLast l (atStart! block)`) to the next operation's entry point in the block. -/ +theorem afterLast_atStart!_eq_before_of_chain {ctx : WfIRContext OpCode} {block : BlockPtr} + {l : List OperationPtr} {x : OperationPtr} + (hChain : block.OpChainSlice ctx.raw (l ++ [x])) + (hHead : (block.get! ctx.raw).firstOp = (l ++ [x]).head?) : + InsertPoint.afterLast l ctx.raw (InsertPoint.atStart! block ctx.raw) = InsertPoint.before x := by + cases l with + | nil => + simp only [List.nil_append, List.head?_cons] at hHead + simp only [InsertPoint.afterLast_nil, InsertPoint.atStart!, hHead] + | cons a t => + obtain ⟨lastOp, hlast⟩ : ∃ lastOp, (a :: t).getLast? = some lastOp := by + cases hg : (a :: t).getLast? with + | none => simp at hg + | some y => exact ⟨y, rfl⟩ + have hmem : lastOp ∈ (a :: t) ++ [x] := + List.mem_append_left _ (List.mem_of_getLast? hlast) + have hnext : (lastOp.get! ctx.raw).next = some x := hChain.getLast?_next_eq hlast + have lastIn : lastOp.InBounds ctx.raw := hChain.inBounds_of_mem lastOp hmem + have lastParent : (lastOp.get! ctx.raw).parent = some block := hChain.parent_of_mem lastOp hmem + simp only [InsertPoint.afterLast, hlast] + rw [InsertPoint.after!_eq_after lastParent lastIn, InsertPoint.after_eq_of_some_next hnext] + +/-- If running `a ++ b` never produces a control-flow action, then running the prefix `a` never does +either: an action from `a` would short-circuit `interpretOpList (a ++ b)` to that same action. +Bridges the whole-list `hFrontNoCf` (from `hSrcSplit`) to the prefix non-termination obligations of +`isRefinedBy_interpretOpList_seqCompose`. -/ +theorem interpretOpList_append_noCf_left {ctx : WfIRContext OpCode} {a b : List OperationPtr} + {state : InterpreterState ctx} (hab : ∀ o ∈ a ++ b, o.InBounds ctx.raw) + (h : ∀ (s2 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList (a ++ b) state hab ≠ some (.ok (s2, some cf))) : + ∀ (s2 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList a state (fun o ho => hab o (List.mem_append_left _ ho)) + ≠ some (.ok (s2, some cf)) := by + intro s2 cf hc + refine h s2 cf ?_ + rw [interpretOpList_append] + simp only [hc] + +/-- If running `a ++ b` never produces a control-flow action, and `a` runs to completion without one, +then running the suffix `b` from the resulting state never produces one either. The run-local suffix +analogue of `interpretOpList_append_noCf_left`. -/ +theorem interpretOpList_append_noCf_right {ctx : WfIRContext OpCode} {a b : List OperationPtr} + {state s2 : InterpreterState ctx} (hab : ∀ o ∈ a ++ b, o.InBounds ctx.raw) + (h : ∀ (s3 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList (a ++ b) state hab ≠ some (.ok (s3, some cf))) + (hrun : interpretOpList a state (fun o ho => hab o (List.mem_append_left _ ho)) + = some (.ok (s2, none))) : + ∀ (s3 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList b s2 (fun o ho => hab o (List.mem_append_right _ ho)) + ≠ some (.ok (s3, some cf)) := by + intro s3 cf hc + refine h s3 cf ?_ + rw [interpretOpList_append, hrun] + simp only [hc] + +/-- If running the whole list never produces a control-flow action, neither does running its init +segment `dropLast`. Feeds the run-local `hInitNoCf` of `interpretOpList_monoAt` from a whole-list +non-branching fact (used for the `pre` segment). -/ +theorem interpretOpList_dropLast_noCf {ctx : WfIRContext OpCode} : + ∀ (ops : List OperationPtr) (state : InterpreterState ctx) (hIn : ∀ o ∈ ops, o.InBounds ctx.raw), + (∀ (s2 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList ops state hIn ≠ some (.ok (s2, some cf))) → + ∀ (s2 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList ops.dropLast state (fun o ho => hIn o (List.dropLast_subset ops ho)) + ≠ some (.ok (s2, some cf)) := by + intro ops + induction ops with + | nil => intro state hIn _ s2 cf hc; simp only [List.dropLast_nil, interpretOpList_nil] at hc; grind + | cons a l ih => + rcases l with _ | ⟨b, rest⟩ + · intro state hIn _ s2 cf hc + simp only [List.dropLast, interpretOpList_nil] at hc; grind + · intro state hIn hwhole s2 cf hc + simp only [List.dropLast_cons_cons, interpretOpList_cons] at hc + rcases hi : interpretOp a state (hIn a (by simp)) with _ | (⟨s, act⟩ | _) + · simp only [hi] at hc; grind + · simp only [hi] at hc + cases act with + | none => + refine ih s (fun o ho => hIn o (List.mem_cons_of_mem a ho)) ?_ s2 cf hc + intro s3 cf3 hc3 + refine hwhole s3 cf3 ?_ + rw [interpretOpList_cons]; simp only [hi]; exact hc3 + | some cf' => + exact hwhole s cf' (by rw [interpretOpList_cons]; simp only [hi]) + · simp only [hi] at hc; grind + /-- -**Block-`B` simulation.** Interpreting the source block list `pre ++ [op] ++ post` is refined by -interpreting the target block list `pre ++ newOps ++ post` (the `post` operations are the same -pointers — their operands are redirected, recorded by `σ` in clause 4). - -Stated over the op-lists directly; PR 8 obtains the lists and the source/target chain-and-parent -structure from `RewrittenAt.srcList`/`tgtList` (clause 3), the source-entry `EquationLemmaAt`/target -`DefinesDominating` invariants from the per-block invariant `I`, and supplies `hOpSim` from -`PreservesSemantics`. - -The proof composes the three regimes via `isRefinedBy_interpretOpList_seqCompose`: -* **`pre` (identical list)** — cross-context monotonicity `hPre` (PR 3), fed the clause-4 frames. -* **`[op]` vs `newOps`** — the local op-step simulation `hOpSim`, after threading the SSA invariant - `EquationLemmaAt` from block entry through `pre` to `op` (`interpretOpList_equationLemmaAt`). -* **`post` (same pointers, redirected operands)** — cross-context monotonicity `hPost` (PR 3) under - `σ`, applicable from any pair of `σ`-refined post-`op` states. - -No target-progress argument is needed: a source `ub` refines any target outcome, so cross-context -monotonicity (`interpretOpList_mono`) and `isRefinedBy_interpretOpList_seqCompose` discharge the -source-UB case directly. The target-side `DefinesDominating` invariant is therefore unused by the -proof, but kept in the signature (`_hDefDom'`) for the Stage D / PR 8 contract. +**Block-`B` simulation (scoped).** Interpreting the source block list `pre ++ [op] ++ post` is +refined by interpreting the target block list `pre ++ newOps ++ post`, with the scoped state +refinement `isRefinedByAt` carried from the block entry `(atStart! block)` to the end of the block. + +The proof composes the three regimes via the scoped `isRefinedBy_interpretOpList_seqCompose`: +* **`pre` (identical list)** — scoped cross-context monotonicity `interpretOpList_monoAt`, advancing + the scope point from `atStart! block` to `before op` (source) / the corresponding target point. +* **`[op]` vs `newOps`** — the scoped local op-step simulation `hOpSim`, after threading the SSA + invariant `EquationLemmaAt` from block entry through `pre` to `op`. +* **`post` (same pointers, redirected operands)** — scoped cross-context monotonicity, fed the + target `DefinesDominating` derived from `hTgtDefDom` and the completed target prefix run. + +Non-branching is supplied as the single whole-list `hSrcSplit` (the source list splits as +`front ++ [term]` with `front` never branching from any state), mirroring the driver's `hSrcSplit` +clause so `interpretBlock_refinement` can forward it verbatim. From it the proof derives: +* the `hPreNoTerm` hypotheses of the scoped `seqCompose` — `pre` (and `pre ++ [op]` when + `post ≠ []`) is a prefix of `front`, so `interpretOpList_append_noCf_left` makes it non-branching; +* the per-segment non-branching fed to `interpretOpList_monoAt`, threaded along the actual run + (`pre`/`post.dropLast` are run-local prefixes of `front`). -/ theorem RewrittenAt.blockSimulation (hRW : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') (newCtxVerif : newCtx.Verified) - {state : InterpreterState ctx} {state' : InterpreterState newCtx} - (hState : state.isRefinedBy state' hRW.σ) - -- Source dominance well-formedness (PR 8 supplies it from the function-level `ctx.Dom` assumption). (hCtxDom : ctx.Dom) - -- Source-side SSA invariant at block entry: threads `EquationLemmaAt` up to `op` (PR 8 supplies - -- it from invariant `I`; the source `pre`-chain is derived via `RewrittenAt.preChainOp`). + {state : InterpreterState ctx} {state' : InterpreterState newCtx} + (hState : state.isRefinedByAt state' hRW.σ + (InsertPoint.atStart! block ctx.raw) (InsertPoint.atStart! block newCtx.raw)) + (hTgtDefDom : state'.DefinesDominating (InsertPoint.atStart! block newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn')) (hEqLem : ∀ fst (hfst : (block.get! ctx.raw).firstOp = some fst), state.EquationLemmaAt (.before fst)) + (hSrcSplit : ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw), + (pre.toList ++ [op] ++ post.toList) = front ++ [term] ∧ + (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) (hOpSim : OpStepSimulation op newOps hRW.σ opIn hRW.newOpsInBounds') : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × ControlFlowAction) (r₂ : InterpreterState newCtx × ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2.isRefinedBy r₂.2) + r₁.1.isRefinedByAt r₂.1 hRW.σ + (InsertPoint.afterLast (pre.toList ++ [op] ++ post.toList) ctx.raw + (InsertPoint.atStart! block ctx.raw)) + (InsertPoint.afterLast (pre.toList ++ newOps.toList ++ post.toList) newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) + (InsertPoint.afterLast_inBounds ctx.wellFormed + ((InsertPoint.inBounds_atStart! ctx.wellFormed blockIn).mpr blockIn) + (fun o ho => ⟨block, hRW.srcChain.parent_of_mem o ho⟩) + (sourceListIn opIn hRW.preInBounds hRW.postInBounds)) + (InsertPoint.afterLast_inBounds newCtx.wellFormed + ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn') + (fun o ho => ⟨block, hRW.tgtChain.parent_of_mem o ho⟩) + (targetListIn hRW.preInBounds' hRW.newOpsInBounds' hRW.postInBounds')) ∧ + r₁.2.isRefinedBy r₂.2) (interpretTerminatedOpList (pre.toList ++ [op] ++ post.toList) state (sourceListIn opIn hRW.preInBounds hRW.postInBounds)) (interpretTerminatedOpList (pre.toList ++ newOps.toList ++ post.toList) state' (targetListIn hRW.preInBounds' hRW.newOpsInBounds' hRW.postInBounds')) := by - -- In-bounds witnesses for `pre`/`post`/`newOps`, derived from the block lists `hRW.srcList` / - -- `hRW.tgtList` (membership in an `operationList` implies in-bounds). - have preIn := hRW.preInBounds - have postIn := hRW.postInBounds - have preIn' := hRW.preInBounds' - have newOpsIn' := hRW.newOpsInBounds' - have postIn' := hRW.postInBounds' - -- Regime 1 (`pre`, identical list): cross-context monotonicity (PR 3), fed the - -- `CrossContextFrame`s of clause 4 (`hRW.frame_of_ne`, since every `pre` op survives and `≠ op`). - -- Threads `σ`-refinement and (in the full proof) the `EquationLemmaAt`/`DefinesDominating` - -- invariants from block entry up to `op`. - have hPre : Interp.isRefinedBy - (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) - (r₂ : InterpreterState newCtx × Option ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) - (interpretOpList pre.toList state preIn) - (interpretOpList pre.toList state' preIn') := by - exact interpretOpList_mono newCtxVerif preIn preIn' hState - (fun o h => hRW.frame o (preIn o h) (preIn' o h) (fun heq => hRW.opNotMemPre (heq ▸ h))) - -- Regime 3 (`post`, same pointers / redirected operands): cross-context monotonicity (PR 3) - -- under `σ`, applicable from any pair of `σ`-refined post-`op` states. Each `post` op survives - -- and `≠ op`, so clause 4 supplies its frame under `σ` (operands redirected through `σ`). - have hPost : ∀ (s : InterpreterState ctx) (s' : InterpreterState newCtx), - s.isRefinedBy s' hRW.σ → - Interp.isRefinedBy - (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) - (r₂ : InterpreterState newCtx × Option ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) - (interpretOpList post.toList s postIn) - (interpretOpList post.toList s' postIn') := by - intro s s' hs - exact interpretOpList_mono newCtxVerif postIn postIn' hs - (fun o h => hRW.frame o (postIn o h) (postIn' o h) (fun heq => hRW.opNotMemPost (heq ▸ h))) - -- Assembly. Prove the op-list refinement over `pre ++ [op] ++ post` vs `pre ++ newOps ++ post` by - -- two nested `isRefinedBy_interpretOpList_seqCompose` applications (inner: `pre`/`[op]` vs - -- `pre`/`newOps`; outer: append `post`), threading `EquationLemmaAt` (source, for `hOpSim`) and - -- `DefinesDominating` (target, for `hPost`); the UB-source obligation is the verified target run's - -- progress. Then convert to the terminated lists, dispatching the source-UB case via `hPostTerm`. - -- In-bounds helpers for the intermediate prefixes `pre ++ [op]` (source) and `pre ++ newOps` - -- (target). - have hpreOpIn : ∀ o ∈ pre.toList ++ [op], o.InBounds ctx.raw := by - intro o ho; simp only [List.mem_append, List.mem_singleton] at ho - rcases ho with h | h - · exact preIn o h - · exact h ▸ opIn - have hpreNewIn' : ∀ o ∈ pre.toList ++ newOps.toList, o.InBounds newCtx.raw := by - intro o ho; simp only [List.mem_append] at ho - rcases ho with h | h - · exact preIn' o h - · exact newOpsIn' o h - -- Inner composition: `pre ++ [op]` (source) refined by `pre ++ newOps` (target). - have hInner := isRefinedBy_interpretOpList_seqCompose - (l₁ := pre.toList) (l₂ := [op]) (m₁ := pre.toList) (m₂ := newOps.toList) - hpreOpIn hpreNewIn' hPre - (fun s1 s1' hs1Ref hrunSrc hrunTgt => by - -- Thread the SSA invariant `EquationLemmaAt` from block entry through `pre` to `op`. - have hEq1 : s1.EquationLemmaAt (.before op) opIn := - interpretOpList_equationLemmaAt_before hCtxDom preIn opIn hRW.preChainOp - (fun fst _ hhead => hEqLem fst - (by rw [hRW.srcFirstOp]; simp only [List.head?_append, hhead, Option.some_or])) - hrunSrc - rw [interpretOpList_singleton] - exact hOpSim s1 s1' hs1Ref hEq1) - -- Outer composition: append `post` on both sides. - have hFull := isRefinedBy_interpretOpList_seqCompose - (l₁ := pre.toList ++ [op]) (l₂ := post.toList) - (m₁ := pre.toList ++ newOps.toList) (m₂ := post.toList) - (sourceListIn opIn preIn postIn) (targetListIn preIn' newOpsIn' postIn') - hInner - (fun s2 s2' hs2Ref hrunSrc hrunTgt => hPost s2 s2' hs2Ref) - -- Convert the op-list refinement to the terminated-list refinement: the source-UB case is - -- refined by any target outcome (monotonicity now discharges target progress internally). - simp only [interpretTerminatedOpList, bind] - rcases hs : interpretOpList (pre.toList ++ [op] ++ post.toList) state - (sourceListIn opIn preIn postIn) with _ | (⟨sf, act⟩ | _) - · simp [Interp.isRefinedBy] - · rw [hs] at hFull - simp only [Interp.isRefinedBy_ok_target_iff] at hFull - obtain ⟨⟨sf', act'⟩, htgt, hsRef, hactRef⟩ := hFull - rw [htgt] - cases act with - | none => simp [Interp.isRefinedBy] - | some cf => - obtain ⟨cf', hact', hcfRef⟩ : ∃ cf', act' = some cf' ∧ cf.isRefinedBy cf' := by - cases act' <;> simp_all [ControlFlowAction.optionIsRefinedBy] - subst hact' - exact ⟨hsRef, hcfRef⟩ - · -- Source-UB case: a source `ub` is refined by any target outcome. - simp [Interp.isRefinedBy] + -- TODO(step 5, blockSimulation body): three-way scoped composition. All helper lemmas it relies on + -- are proved above and `interpretOpList_monoAt` already takes the run-local `hInitNoCf`; this is the + -- remaining assembly. + -- Destructure `hSrcSplit` into `front`/`term`/`hFrontNoCf`; `front = pre ++ ([op] ++ post).dropLast` + -- (via `List.dropLast_concat` + `List.dropLast_append_of_ne_nil`), then `subst` so `hFrontNoCf` is + -- stated over that. Non-branching facts: + -- * `pre` whole: `interpretOpList_append_noCf_left` (prefix of `front`) → inner `hPreNoTerm`; + -- * `pre.dropLast`: `interpretOpList_dropLast_noCf` of the above → `pre` segment `hInitNoCf`; + -- * `pre ++ [op]` whole (when `post ≠ []`): `append_noCf_left` → outer `hPreNoTerm`; + -- * `post.dropLast` (inside outer `hCont`, from the actual prefix run): `append_noCf_right` → + -- `post` segment `hInitNoCf`. + -- 1. `pre` via `interpretOpList_monoAt` from `(atStart! block, atStart! block)`; end point bridged + -- to `before op` (source) by `afterLast_atStart!_eq_before_of_chain`; `hPointsHead` from + -- `srcFirstOp`/`tgtFirstOp`; chains from `preChainOp.append_left`/`preChain'`. + -- 2. `[op]`/`newOps` via scoped `hOpSim` (its target point `p' := afterLast pre ps'`), `EquationLemmaAt` + -- threaded via `interpretOpList_equationLemmaAt_before` as in the old proof; output points bridged + -- by `afterLast_append`/`afterLast_singleton`. + -- 3. `post` via `interpretOpList_monoAt`; target `DefinesDominating` at the post-entry from + -- `hTgtDefDom` advanced through `pre ++ newOps` by `interpretOpList_DefinesDominating`. + -- Glue with the scoped `seqCompose` (inner `pre`/`[op]`, outer append `post`); `hM2Nil` is `by simp` + -- (`[op] ≠ []`) / `id` (`post`/`post`); `afterLast` in-bounds via `InsertPoint.afterLast_inBounds`. + -- Finish: convert op-list → terminated list as in the pre-refactor proof; source-UB refines any target. + sorry /-- Bridge `interpretBlock` to a `setArgumentValues?` followed by `interpretTerminatedOpList` over the block's operation list. When the block is empty (`firstOp = none`) the operation list is empty and both From 4eed63dbf682f75014237b8f7c3f7ca882c58c03 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Sun, 28 Jun 2026 20:56:36 +0200 Subject: [PATCH 13/31] --- Veir/Interpreter/Refinement/Basic.lean | 99 ++- Veir/Interpreter/Refinement/Monotonicity.lean | 23 +- Veir/PatternRewriter/Semantics.lean | 2 +- Veir/PatternRewriter/Soundness.lean | 772 ++++++++++++++++-- 4 files changed, 793 insertions(+), 103 deletions(-) diff --git a/Veir/Interpreter/Refinement/Basic.lean b/Veir/Interpreter/Refinement/Basic.lean index 283c4e5da..c836036fa 100644 --- a/Veir/Interpreter/Refinement/Basic.lean +++ b/Veir/Interpreter/Refinement/Basic.lean @@ -200,36 +200,109 @@ def InterpreterState.isRefinedBy {ctx ctx' : WfIRContext OpInfo} state.memory = state'.memory ∧ state.variables.isRefinedBy state'.variables mapping +/-- +A *refinement point* selects which values a scoped refinement relation constrains. It is the +position parameter of `isRefinedByAt`, richer than a bare `InsertPoint`: + +* `.at p` — the usual scope: the values dominating the program point `p`. +* `.blockEntry b` — the *incoming-edge* scope of a block `b`: the values dominating `b`'s entry, + **minus** `b`'s own arguments. This is the scope on the pre-argument input state of a block: + `setArgumentValues?` immediately overwrites `b`'s arguments with fresh (refined) values, so their + stale incoming values need not be constrained. At a loop back-edge the successor's stale arguments + cannot be transported from the predecessor's end, so excusing them is what makes the cross-edge + transport sound. +-/ +inductive RefinementPoint where + | at (p : InsertPoint) + | blockEntry (b : BlockPtr) + +/-- An `InsertPoint` is used as a refinement point via the `.at` scope. -/ +instance : Coe InsertPoint RefinementPoint := ⟨.at⟩ + +/-- The values *in scope* at a refinement point. For `.at p` this is exactly the values dominating +`p`; for `.blockEntry b` it additionally excludes `b`'s own arguments. -/ +def RefinementPoint.inScope {OpInfo : Type} [HasOpInfo OpInfo] : + RefinementPoint → ValuePtr → WfIRContext OpInfo → Prop + | .at p, val, ctx => val.dominatesIp p ctx + | .blockEntry b, val, ctx => + val.dominatesIp (InsertPoint.atStart! b ctx.raw) ctx ∧ val ∉ b.getArguments! ctx.raw + +/-- `inScope (.at p)` is, definitionally, just domination of `p`. -/ +@[simp, grind =] +theorem RefinementPoint.inScope_at {OpInfo : Type} [HasOpInfo OpInfo] + {p : InsertPoint} {val : ValuePtr} {ctx : WfIRContext OpInfo} : + RefinementPoint.inScope (.at p) val ctx = val.dominatesIp p ctx := rfl + +/-- In-bounds witness carried by `isRefinedByAt` for a refinement point. -/ +def RefinementPoint.InBounds : RefinementPoint → IRContext OpInfo → Prop + | .at p, ctx => p.InBounds ctx + | .blockEntry b, ctx => b.InBounds ctx + +@[simp, grind =] +theorem RefinementPoint.inBounds_at {p : InsertPoint} {ctx : IRContext OpInfo} : + (RefinementPoint.at p).InBounds ctx = p.InBounds ctx := rfl + +@[simp, grind =] +theorem RefinementPoint.inBounds_blockEntry {b : BlockPtr} {ctx : IRContext OpInfo} : + (RefinementPoint.blockEntry b).InBounds ctx = b.InBounds ctx := rfl + /-- A variable state `state` is refined by `state'` through the value renaming `mapping`, scoped to -the program points `p` (in `ctx`) and `p'` (in `ctx'`). Only values that are *in scope* at both -points are constrained: a source value `val` must dominate `p`, and its image `mapping val` must -dominate `p'`. This excuses stale values that remain in the persistent map from prior iterations -or prior blocks without constraining them. +the refinement points `s` (in `ctx`) and `s'` (in `ctx'`). Only values that are *in scope* at both +points are constrained. This excuses stale values that remain in the persistent map from prior +iterations or prior blocks without constraining them; the `.blockEntry` scope additionally excuses +a block's own arguments at its entry. The relation uses `∀ sv tv` (not `∃ tv`) so existence is delegated to `DefinesDominating` at the call site, which simplifies proof obligations at maintenance steps. -/ def VariableState.isRefinedByAt {ctx ctx' : WfIRContext OpInfo} (state : VariableState ctx) (state' : VariableState ctx') - (mapping : ValueMapping ctx ctx') (p : InsertPoint) (p' : InsertPoint) - (_pIn : p.InBounds ctx.raw := by grind) (_p'In : p'.InBounds ctx'.raw := by grind) : Prop := + (mapping : ValueMapping ctx ctx') (s : RefinementPoint) (s' : RefinementPoint) + (_sIn : s.InBounds ctx.raw := by grind) (_s'In : s'.InBounds ctx'.raw := by grind) : Prop := ∀ (val : ValuePtr) (valIn : val.InBounds ctx.raw), - val.dominatesIp p ctx → - (mapping ⟨val, valIn⟩).val.dominatesIp p' ctx' → + s.inScope val ctx → + s'.inScope (mapping ⟨val, valIn⟩).val ctx' → ∀ sv tv, state.getVar? val = some sv → state'.getVar? (mapping ⟨val, valIn⟩) = some tv → sv ⊒ tv /-- An interpreter state `state` is refined by `state'` through the value mapping `mapping`, scoped -to source point `p` and target point `p'`: they have the same memory, and the variable state of -`state` is scoped-refined by the variable state of `state'` through `mapping` at `(p, p')`. +to source point `s` and target point `s'`: they have the same memory, and the variable state of +`state` is scoped-refined by the variable state of `state'` through `mapping` at `(s, s')`. -/ def InterpreterState.isRefinedByAt {ctx ctx' : WfIRContext OpInfo} (state : InterpreterState ctx) (state' : InterpreterState ctx') - (mapping : ValueMapping ctx ctx') (p : InsertPoint) (p' : InsertPoint) - (_pIn : p.InBounds ctx.raw := by grind) (_p'In : p'.InBounds ctx'.raw := by grind) : Prop := + (mapping : ValueMapping ctx ctx') (s : RefinementPoint) (s' : RefinementPoint) + (_sIn : s.InBounds ctx.raw := by grind) (_s'In : s'.InBounds ctx'.raw := by grind) : Prop := state.memory = state'.memory ∧ - state.variables.isRefinedByAt state'.variables mapping p p' + state.variables.isRefinedByAt state'.variables mapping s s' + +/-- Scope-weakening (antitone): `isRefinedByAt` at a *wider* pair of scopes implies it at a +*narrower* pair. If every value in scope at `(t, t')` is in scope at `(s, s')`, the relation +transports from `(s, s')` to `(t, t')`. -/ +theorem VariableState.isRefinedByAt.weaken {ctx ctx' : WfIRContext OpInfo} + {state : VariableState ctx} {state' : VariableState ctx'} + {mapping : ValueMapping ctx ctx'} {s s' t t' : RefinementPoint} + {sIn : s.InBounds ctx.raw} {s'In : s'.InBounds ctx'.raw} + {tIn : t.InBounds ctx.raw} {t'In : t'.InBounds ctx'.raw} + (h : state.isRefinedByAt state' mapping s s' sIn s'In) + (hsrc : ∀ (val : ValuePtr), t.inScope val ctx → s.inScope val ctx) + (htgt : ∀ (val : ValuePtr), t'.inScope val ctx' → s'.inScope val ctx') : + state.isRefinedByAt state' mapping t t' tIn t'In := + fun val valIn hsc htsc sv tv hsv htv => + h val valIn (hsrc val hsc) (htgt _ htsc) sv tv hsv htv + +/-- Interpreter-state version of `VariableState.isRefinedByAt.weaken`. -/ +theorem InterpreterState.isRefinedByAt.weaken {ctx ctx' : WfIRContext OpInfo} + {state : InterpreterState ctx} {state' : InterpreterState ctx'} + {mapping : ValueMapping ctx ctx'} {s s' t t' : RefinementPoint} + {sIn : s.InBounds ctx.raw} {s'In : s'.InBounds ctx'.raw} + {tIn : t.InBounds ctx.raw} {t'In : t'.InBounds ctx'.raw} + (h : state.isRefinedByAt state' mapping s s' sIn s'In) + (hsrc : ∀ (val : ValuePtr), t.inScope val ctx → s.inScope val ctx) + (htgt : ∀ (val : ValuePtr), t'.inScope val ctx' → s'.inScope val ctx') : + state.isRefinedByAt state' mapping t t' tIn t'In := + ⟨h.1, h.2.weaken hsrc htgt⟩ end Veir diff --git a/Veir/Interpreter/Refinement/Monotonicity.lean b/Veir/Interpreter/Refinement/Monotonicity.lean index dc3872c7d..b77fb76bf 100644 --- a/Veir/Interpreter/Refinement/Monotonicity.lean +++ b/Veir/Interpreter/Refinement/Monotonicity.lean @@ -155,18 +155,20 @@ entry constrains them only vacuously; it does constrain the non-argument values the entry, which is exactly what the proof reuses for the surviving (non-argument) values. Hypotheses compared to `setArgumentValues?_isRefinedBy`: -- `hRef` uses `isRefinedByAt` at the block entry instead of unscoped `isRefinedBy`. +- `hRef` uses `isRefinedByAt` at the **incoming-edge** scope `.blockEntry block` (which excuses + `block`'s own arguments) instead of unscoped `isRefinedBy`. - `hImageNotArg`: the mapping does not send a non-argument value that is *in scope at the block entry* onto a block-argument slot. (Justified by dominance: a forwarded block argument dominates the value it replaces, so it cannot also be dominated by it — hence no value dominating the block - entry maps onto one of the block's own arguments.) -/ + entry maps onto one of the block's own arguments.) It places `σval` in the *target* `.blockEntry` + scope. -/ theorem VariableState.setArgumentValues?_isRefinedByAt {ctx ctx' : WfIRContext OpCode} {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} {mapping : ValueMapping ctx ctx'} {values values' : Array RuntimeValue} {newSrcVars : VariableState ctx} (blockIn : block.InBounds ctx.raw) (blockIn' : block.InBounds ctx'.raw) (hRef : srcVars.isRefinedByAt tgtVars mapping - (InsertPoint.atStart! block ctx.raw) (InsertPoint.atStart! block ctx'.raw)) + (.blockEntry block) (.blockEntry block)) (hVals : values ⊒ values') (hArgs : block.getArguments! ctx'.raw = mapping.applyToArray (block.getArguments! ctx.raw)) /- A non-argument value that is in scope at the block entry is never mapped onto a block-argument @@ -220,9 +222,9 @@ theorem VariableState.setArgumentValues?_isRefinedByAt {ctx ctx' : WfIRContext O have hσnotMem : (mapping ⟨val, valIn⟩).val ∉ block.getArguments! ctx'.raw := hImageNotArg val valIn hMem hValDom rw [VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! hσnotMem hTgt] at htv - -- The surviving value `val` is in scope at the block entry on both sides, so the entry-point - -- input relation `hRef` constrains it directly. - exact hRef val valIn hValDom hσValDom sv tv hsv htv + -- The surviving value `val` is a non-argument in scope at the block entry on both sides (its + -- image likewise, by `hσnotMem`), so the incoming-edge relation `hRef` constrains it directly. + exact hRef val valIn ⟨hValDom, hMem⟩ ⟨hσValDom, hσnotMem⟩ sv tv hsv htv /-! ## Scoped (`isRefinedByAt`) variants of the monotonicity lemmas -/ @@ -233,7 +235,7 @@ theorem VariableState.getOperandValues_isRefinedByAt {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} {mapping : ValueMapping ctx ctx'} (opIn : op.InBounds ctx.raw) (opIn' : op'.InBounds ctx'.raw) - (hRef : srcVars.isRefinedByAt tgtVars mapping (.before op) (.before op')) + (hRef : srcVars.isRefinedByAt tgtVars mapping (.at (.before op)) (.at (.before op'))) (ctxDom : ctx.Dom) (ctxDom' : ctx'.Dom) (hOperands : op'.getOperands! ctx'.raw = mapping.applyToArray (op.getOperands! ctx.raw)) @@ -241,7 +243,7 @@ theorem VariableState.getOperandValues_isRefinedByAt v.dominatesIp (.before op') ctx' → (tgtVars.getVar? v).isSome) (hSrc : srcVars.getOperandValues op = some srcVal) : ∃ tgtVal, tgtVars.getOperandValues op' = some tgtVal ∧ srcVal ⊒ tgtVal := by - simp only [VariableState.isRefinedByAt] at hRef + simp only [VariableState.isRefinedByAt, RefinementPoint.inScope_at] at hRef have ⟨hsize, hSrc'⟩ := VariableState.getOperandValues_eq_some_iff.mp hSrc -- All target operands are defined (from `tgtDef` + target operand dominance via `ctxDom'`). have hTgtDef : ∀ (i : Nat) (hi : i < (op'.getOperands! ctx'.raw).size), @@ -298,7 +300,7 @@ theorem VariableState.setResultValues?_isRefinedByAt {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} {mapping : ValueMapping ctx ctx'} (opIn : op.InBounds ctx.raw) (opIn' : op'.InBounds ctx'.raw) - (hRef : srcVars.isRefinedByAt tgtVars mapping (.before op) (.before op')) + (hRef : srcVars.isRefinedByAt tgtVars mapping (.at (.before op)) (.at (.before op'))) {newSrcVars : VariableState ctx} {srcVals tgtVals : Array RuntimeValue} (hVals : srcVals ⊒ tgtVals) (hResults : op'.getResults! ctx'.raw = mapping.applyToArray (op.getResults! ctx.raw)) @@ -317,6 +319,7 @@ theorem VariableState.setResultValues?_isRefinedByAt (varState := tgtVars) (inBounds := opIn')).mp tgtValsConforms refine ⟨newTgtVars, hTgt, ?_⟩ intro val valIn hValDomAfter hσValDomAfter sv tv hsv htv + simp only [RefinementPoint.inScope_at] at hValDomAfter hσValDomAfter -- By `value_dominatesIp_after_iff`: val dominates (before op) or is a result of op. rw [ctxDom.value_dominatesIp_after_iff] at hValDomAfter rw [ctxDom'.value_dominatesIp_after_iff] at hσValDomAfter @@ -344,7 +347,7 @@ theorem interpretOp_monotone_at {state : InterpreterState ctx} {state' : InterpreterState ctx'} {mapping : ValueMapping ctx ctx'} (opIn : op.InBounds ctx.raw) (opIn' : op'.InBounds ctx'.raw) - (hState : state.isRefinedByAt state' mapping (.before op) (.before op')) + (hState : state.isRefinedByAt state' mapping (.at (.before op)) (.at (.before op'))) (hPreserves : mapping.PreservesOperation op op') (opVerif' : op'.Verified ctx' opIn') (ctxDom : ctx.Dom) (ctxDom' : ctx'.Dom) diff --git a/Veir/PatternRewriter/Semantics.lean b/Veir/PatternRewriter/Semantics.lean index c54da985d..176f9637f 100644 --- a/Veir/PatternRewriter/Semantics.lean +++ b/Veir/PatternRewriter/Semantics.lean @@ -147,7 +147,7 @@ def LocalRewritePattern.PreservesSemantics ∀ newState cf, interpretOp op state = some (newState, cf) → ∀ sourceValues, (op.getResults ctx.raw).mapM (newState.variables.getVar? ·) = some sourceValues → ∀ (state' : InterpreterState newCtx), state'.EquationLemmaAt (InsertPoint.before op) → - state.isRefinedByAt state' (LocalRewritePattern.mapping hpattern) (.before op) (.before op) → + state.isRefinedByAt state' (LocalRewritePattern.mapping hpattern) (.at (.before op)) (.at (.before op)) → ∃ newState', interpretOpList newOps.toList state' (by grind [ReturnOps]) = some (newState', cf) ∧ newState.memory = newState'.memory ∧ diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index f221dd502..145928ef5 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -421,6 +421,34 @@ theorem mapping_getResult_mem_newValues h.mapNonResultsInBounds h.newValuesSize] at hx exact hx +/-- The block-argument array of `bl` is identical across the two contexts (the rewrite only edits +operation lists, never block arguments). -/ +theorem getArguments!_eq + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) : + bl.getArguments! newCtx.raw = bl.getArguments! ctx.raw := by + simp only [BlockPtr.getArguments!, BlockPtr.getNumArguments!, h.blockArgsPreserved bl blIn] + +/-- `σ` never maps a value onto one of `bl`'s block arguments unless it already is that block +argument: a value not in `bl`'s arguments is either fixed by `σ` (so stays out of the arguments) or a +result of `op` (so maps into `newValues`, which are operation results, never block arguments). +Discharges the `hImageNotArg` hypothesis of `setArgumentValues?_isRefinedByAt`. -/ +theorem mappingImageNotArg + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) {val : ValuePtr} (valIn : val.InBounds ctx.raw) + (hNotArg : val ∉ bl.getArguments! ctx.raw) : + (h.σ ⟨val, valIn⟩).val ∉ bl.getArguments! newCtx.raw := by + rw [h.getArguments!_eq blIn] + by_cases hMem : val ∈ op.getResults! ctx.raw + · have hmem := h.mapping_getResult_mem_newValues valIn hMem + obtain ⟨opr, hopr⟩ := h.newValuesAreResults _ hmem + rw [hopr] + simp only [BlockPtr.getArguments!.mem_iff_exists_index, not_exists, not_and] + intro i _ heq + simp [BlockPtr.getArgument] at heq + · rw [h.mappingFixesNonResults val valIn hMem] + exact hNotArg + /-- `σ` reflects block-argument pointers: the only value it maps onto `bl.getArgument i` is `bl.getArgument i` itself (results map into `newValues`, which are never block arguments). Discharges the `hReflectArgs` hypothesis of `setArgumentValues?_isRefinedBy`. -/ @@ -880,6 +908,38 @@ theorem afterLast_atStart!_eq_before_of_chain {ctx : WfIRContext OpCode} {block simp only [InsertPoint.afterLast, hlast] rw [InsertPoint.after!_eq_after lastParent lastIn, InsertPoint.after_eq_of_some_next hnext] +/-- Running a block's *entire* operation list from its entry lands at the block end: the end point of +the full chain is `atEnd block`. (For an empty block, `firstOp = none`, so `atStart! = atEnd` already.) -/ +theorem afterLast_operationList_atStart!_eq_atEnd {ctx : WfIRContext OpCode} {b : BlockPtr} + (bIn : b.InBounds ctx.raw) : + InsertPoint.afterLast (b.operationList ctx.raw ctx.wellFormed bIn).toList ctx.raw + (InsertPoint.atStart! b ctx.raw) = InsertPoint.atEnd b := by + have hchain := BlockPtr.operationListWF ctx.raw b bIn ctx.wellFormed + cases hg : (b.operationList ctx.raw ctx.wellFormed bIn).toList.getLast? with + | none => + have hnil : (b.operationList ctx.raw ctx.wellFormed bIn).toList = [] := + List.getLast?_eq_none_iff.mp hg + have hsize : (b.operationList ctx.raw ctx.wellFormed bIn).size = 0 := by + rw [← Array.length_toList, hnil]; rfl + have hfirst : (b.get! ctx.raw).firstOp = none := by + rw [hchain.first, Array.getElem?_eq_none (by omega)] + rw [hnil] + simp only [InsertPoint.afterLast_nil, InsertPoint.atStart!, hfirst] + | some last => + have hmem : last ∈ (b.operationList ctx.raw ctx.wellFormed bIn).toList := List.mem_of_getLast? hg + have hmem' : last ∈ b.operationList ctx.raw ctx.wellFormed bIn := by simpa using hmem + have lastParent : (last.get! ctx.raw).parent = some b := hchain.opParent hmem' + have lastIn : last.InBounds ctx.raw := hchain.arrayInBounds hmem' + have hlastOp : (b.get! ctx.raw).lastOp = some last := by + rw [hchain.last, ← Array.getElem?_toList, ← Array.length_toList, + ← List.getLast?_eq_getElem?] + exact hg + have hnext : (last.get! ctx.raw).next = none := + (BlockPtr.OpChain.next!_eq_none_iff_lastOp!_eq_self lastIn hchain lastParent).mpr hlastOp + simp only [InsertPoint.afterLast, hg] + rw [InsertPoint.after!_eq_after lastParent lastIn] + grind [InsertPoint.after] + /-- If running `a ++ b` never produces a control-flow action, then running the prefix `a` never does either: an action from `a` would short-circuit `interpretOpList (a ++ b)` to that same action. Bridges the whole-list `hFrontNoCf` (from `hSrcSplit`) to the prefix non-termination obligations of @@ -1004,29 +1064,300 @@ theorem RewrittenAt.blockSimulation (sourceListIn opIn hRW.preInBounds hRW.postInBounds)) (interpretTerminatedOpList (pre.toList ++ newOps.toList ++ post.toList) state' (targetListIn hRW.preInBounds' hRW.newOpsInBounds' hRW.postInBounds')) := by - -- TODO(step 5, blockSimulation body): three-way scoped composition. All helper lemmas it relies on - -- are proved above and `interpretOpList_monoAt` already takes the run-local `hInitNoCf`; this is the - -- remaining assembly. - -- Destructure `hSrcSplit` into `front`/`term`/`hFrontNoCf`; `front = pre ++ ([op] ++ post).dropLast` - -- (via `List.dropLast_concat` + `List.dropLast_append_of_ne_nil`), then `subst` so `hFrontNoCf` is - -- stated over that. Non-branching facts: - -- * `pre` whole: `interpretOpList_append_noCf_left` (prefix of `front`) → inner `hPreNoTerm`; - -- * `pre.dropLast`: `interpretOpList_dropLast_noCf` of the above → `pre` segment `hInitNoCf`; - -- * `pre ++ [op]` whole (when `post ≠ []`): `append_noCf_left` → outer `hPreNoTerm`; - -- * `post.dropLast` (inside outer `hCont`, from the actual prefix run): `append_noCf_right` → - -- `post` segment `hInitNoCf`. - -- 1. `pre` via `interpretOpList_monoAt` from `(atStart! block, atStart! block)`; end point bridged - -- to `before op` (source) by `afterLast_atStart!_eq_before_of_chain`; `hPointsHead` from - -- `srcFirstOp`/`tgtFirstOp`; chains from `preChainOp.append_left`/`preChain'`. - -- 2. `[op]`/`newOps` via scoped `hOpSim` (its target point `p' := afterLast pre ps'`), `EquationLemmaAt` - -- threaded via `interpretOpList_equationLemmaAt_before` as in the old proof; output points bridged - -- by `afterLast_append`/`afterLast_singleton`. - -- 3. `post` via `interpretOpList_monoAt`; target `DefinesDominating` at the post-entry from - -- `hTgtDefDom` advanced through `pre ++ newOps` by `interpretOpList_DefinesDominating`. - -- Glue with the scoped `seqCompose` (inner `pre`/`[op]`, outer append `post`); `hM2Nil` is `by simp` - -- (`[op] ≠ []`) / `id` (`post`/`post`); `afterLast` in-bounds via `InsertPoint.afterLast_inBounds`. - -- Finish: convert op-list → terminated list as in the pre-refactor proof; source-UB refines any target. - sorry + have ctxDom' : newCtx.Dom := hRW.newCtxDom + -- Proof-irrelevant congruence for `interpretOpList`'s in-bounds witness, used to move + -- non-branching facts between syntactically-different-but-equal op lists. + have iopl_congr : ∀ {cc : WfIRContext OpCode} {l l' : List OperationPtr} (s : InterpreterState cc) + (hl : ∀ o ∈ l, o.InBounds cc.raw) (hl' : ∀ o ∈ l', o.InBounds cc.raw), + l = l' → interpretOpList l s hl = interpretOpList l' s hl' := by + intro cc l l' s hl hl' h; subst h; rfl + -- The source list and its non-branching `front` prefix (from `hSrcSplit`). + obtain ⟨front, term, frontIn, hSplit, hFrontNoCf⟩ := hSrcSplit + have hfrontEq : front = (pre.toList ++ [op] ++ post.toList).dropLast := by + rw [hSplit, List.dropLast_concat] + subst hfrontEq + -- `pre` never branches from any state (it is a prefix of `front`). + have hpreNB : ∀ (s s2 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList pre.toList s hRW.preInBounds ≠ some (.ok (s2, some cf)) := by + intro s s2 cf hc + refine hFrontNoCf s s2 cf ?_ + rw [iopl_congr s frontIn (l' := pre.toList ++ ([op] ++ post.toList).dropLast) + (by intro o ho; exact frontIn o (by + rw [List.append_assoc, List.dropLast_append_of_ne_nil (by simp)]; exact ho)) + (by rw [List.append_assoc, List.dropLast_append_of_ne_nil (by simp)]), + interpretOpList_append] + simp only [hc] + -- `pre ++ [op]` never branches from any state (used when `post ≠ []`). + have hpreOpNB : post.toList ≠ [] → ∀ (s s2 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList (pre.toList ++ [op]) s + (fun o ho => hRW.srcChain.inBounds_of_mem o (by + simp only [List.mem_append] at ho ⊢; exact Or.inl ho)) ≠ some (.ok (s2, some cf)) := by + intro hpost s s2 cf hc + refine hFrontNoCf s s2 cf ?_ + rw [iopl_congr s frontIn (l' := (pre.toList ++ [op]) ++ post.toList.dropLast) + (by intro o ho; exact frontIn o (by + rw [List.dropLast_append_of_ne_nil hpost]; exact ho)) + (by rw [List.dropLast_append_of_ne_nil hpost]), + interpretOpList_append] + simp only [hc] + -- Point bridges: running `pre` from the block start lands just before `op`. + have hPreEndSrc : InsertPoint.afterLast pre.toList ctx.raw (InsertPoint.atStart! block ctx.raw) + = InsertPoint.before op := + afterLast_atStart!_eq_before_of_chain hRW.preChainOp + (by rw [hRW.srcFirstOp, List.head?_append]; simp) + -- The `interpretOpList` refinement, assembled below by nested scoped `seqCompose`. + have hOpList : Interp.isRefinedBy + (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) + (r₂ : InterpreterState newCtx × Option ControlFlowAction) => + r₁.1.isRefinedByAt r₂.1 hRW.σ + (InsertPoint.afterLast (pre.toList ++ [op] ++ post.toList) ctx.raw + (InsertPoint.atStart! block ctx.raw)) + (InsertPoint.afterLast (pre.toList ++ newOps.toList ++ post.toList) newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) + (InsertPoint.afterLast_inBounds ctx.wellFormed + ((InsertPoint.inBounds_atStart! ctx.wellFormed blockIn).mpr blockIn) + (fun o ho => ⟨block, hRW.srcChain.parent_of_mem o ho⟩) + (sourceListIn opIn hRW.preInBounds hRW.postInBounds)) + (InsertPoint.afterLast_inBounds newCtx.wellFormed + ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn') + (fun o ho => ⟨block, hRW.tgtChain.parent_of_mem o ho⟩) + (targetListIn hRW.preInBounds' hRW.newOpsInBounds' hRW.postInBounds')) ∧ + ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) + (interpretOpList (pre.toList ++ [op] ++ post.toList) state + (sourceListIn opIn hRW.preInBounds hRW.postInBounds)) + (interpretOpList (pre.toList ++ newOps.toList ++ post.toList) state' + (targetListIn hRW.preInBounds' hRW.newOpsInBounds' hRW.postInBounds')) := by + refine isRefinedBy_interpretOpList_seqCompose (l₂ := post.toList) (m₂ := post.toList) + (p := InsertPoint.atStart! block ctx.raw) (p' := InsertPoint.atStart! block newCtx.raw) + ?_ ?_ ?_ ?_ ?_ ?_ ?_ ?_ ?_ ?_ + -- qIn + · exact InsertPoint.afterLast_inBounds ctx.wellFormed + ((InsertPoint.inBounds_atStart! ctx.wellFormed blockIn).mpr blockIn) + (fun o ho => ⟨block, hRW.preChainOp.parent_of_mem o ho⟩) + (fun o ho => hRW.preChainOp.inBounds_of_mem o ho) + -- q'In + · exact InsertPoint.afterLast_inBounds newCtx.wellFormed + ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn') + (fun o ho => ⟨block, hRW.tgtChain.append_left.parent_of_mem o ho⟩) + (fun o ho => hRW.tgtChain.append_left.inBounds_of_mem o ho) + -- hM2Nil + · exact id + -- hPreNoTerm (only when `post ≠ []`) + · exact fun h => hpreOpNB h state + -- hPrefix: `pre` then `[op]` vs `newOps` (inner seqCompose) + · refine isRefinedBy_interpretOpList_seqCompose (l₂ := [op]) (m₂ := newOps.toList) + (p := InsertPoint.atStart! block ctx.raw) (p' := InsertPoint.atStart! block newCtx.raw) + ?_ ?_ ?_ ?_ ?_ ?_ ?_ ?_ ?_ ?_ + -- qIn' + · exact InsertPoint.afterLast_inBounds ctx.wellFormed + ((InsertPoint.inBounds_atStart! ctx.wellFormed blockIn).mpr blockIn) + (fun o ho => ⟨block, hRW.preChainOp.append_left.parent_of_mem o ho⟩) + (fun o ho => hRW.preInBounds o ho) + -- q'In' + · exact InsertPoint.afterLast_inBounds newCtx.wellFormed + ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn') + (fun o ho => ⟨block, hRW.preChain'.parent_of_mem o ho⟩) + (fun o ho => hRW.preInBounds' o ho) + -- hM2Nil' + · intro h; simp at h + -- hPreNoTerm' + · exact fun _ => hpreNB state + -- hPrefix': `pre` via cross-context monotonicity + · refine interpretOpList_monoAt newCtxVerif hCtxDom ctxDom' + (fun o ho => hRW.preInBounds o ho) (fun o ho => hRW.preInBounds' o ho) + hRW.preChainOp.append_left hRW.preChain' + ((InsertPoint.inBounds_atStart! ctx.wellFormed blockIn).mpr blockIn) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn') + hState hTgtDefDom + (fun o ho => hRW.frame o (hRW.preInBounds o ho) (hRW.preInBounds' o ho) + (fun heq => hRW.opNotMemPre (heq ▸ ho))) ?_ ?_ + · -- hPointsHead + intro h + have hsf : (block.get! ctx.raw).firstOp = some (pre.toList.head h) := by + rw [hRW.srcFirstOp]; simp [List.head?_append, List.head?_eq_some_head h] + have htf : (block.get! newCtx.raw).firstOp = some (pre.toList.head h) := by + rw [hRW.tgtFirstOp]; simp [List.head?_append, List.head?_eq_some_head h] + exact ⟨by simp only [InsertPoint.atStart!, hsf], by simp only [InsertPoint.atStart!, htf]⟩ + · -- hInitNoCf + exact interpretOpList_dropLast_noCf pre.toList state + (fun o ho => hRW.preInBounds o ho) (hpreNB state) + -- hCont': `[op]` vs `newOps` via `hOpSim` + · intro s2 s2' hsRef hrunS hrunT + have p'In : (InsertPoint.afterLast pre.toList newCtx.raw + (InsertPoint.atStart! block newCtx.raw)).InBounds newCtx.raw := + InsertPoint.afterLast_inBounds newCtx.wellFormed + ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn') + (fun o ho => ⟨block, hRW.preChain'.parent_of_mem o ho⟩) + (fun o ho => hRW.preInBounds' o ho) + have hEq : s2.EquationLemmaAt (InsertPoint.before op) opIn := + interpretOpList_equationLemmaAt_before hCtxDom hRW.preInBounds opIn hRW.preChainOp + (fun fst _ hhd => hEqLem fst (by rw [hRW.srcFirstOp, List.head?_append, hhd]; rfl)) hrunS + -- Transport the source scope point `afterLast pre (atStart!)` to `before op` (witness-free). + have congrPt : ∀ {p₁ p₂ : InsertPoint} {w1 : p₁.InBounds ctx.raw} {w1' w2'} + {w2 : p₂.InBounds ctx.raw}, p₁ = p₂ → + s2.isRefinedByAt s2' hRW.σ p₁ (InsertPoint.afterLast pre.toList newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) w1 w1' → + s2.isRefinedByAt s2' hRW.σ p₂ (InsertPoint.afterLast pre.toList newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) w2 w2' := by + intro p₁ p₂ w1 w1' w2' w2 hp h; subst hp; exact h + have hres := hOpSim s2 s2' + (InsertPoint.afterLast pre.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) + p'In + (InsertPoint.after!_inBounds ctx.wellFormed hRW.opParent opIn) + (InsertPoint.afterLast_inBounds newCtx.wellFormed p'In hRW.newOpsParent' + (fun o ho => hRW.newOpsInBounds' o ho)) + (congrPt hPreEndSrc hsRef) hEq + rw [interpretOpList_singleton] + have hP1 : InsertPoint.afterLast (pre.toList ++ [op]) ctx.raw + (InsertPoint.atStart! block ctx.raw) = InsertPoint.after! op ctx.raw := by + rw [InsertPoint.afterLast_append, InsertPoint.afterLast_singleton] + have hP2 : InsertPoint.afterLast (pre.toList ++ newOps.toList) newCtx.raw + (InsertPoint.atStart! block newCtx.raw) + = InsertPoint.afterLast newOps.toList newCtx.raw + (InsertPoint.afterLast pre.toList newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) := by + rw [InsertPoint.afterLast_append] + simp only [hP1, hP2] + exact hres + -- hCont: `post` via cross-context monotonicity + · intro s2 s2' hsRef2 hrunS2 hrunT2 + have pInMono : (InsertPoint.afterLast (pre.toList ++ [op]) ctx.raw + (InsertPoint.atStart! block ctx.raw)).InBounds ctx.raw := + InsertPoint.afterLast_inBounds ctx.wellFormed + ((InsertPoint.inBounds_atStart! ctx.wellFormed blockIn).mpr blockIn) + (fun o ho => ⟨block, hRW.preChainOp.parent_of_mem o ho⟩) + (fun o ho => hRW.preChainOp.inBounds_of_mem o ho) + have p'InMono : (InsertPoint.afterLast (pre.toList ++ newOps.toList) newCtx.raw + (InsertPoint.atStart! block newCtx.raw)).InBounds newCtx.raw := + InsertPoint.afterLast_inBounds newCtx.wellFormed + ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn') + (fun o ho => ⟨block, hRW.tgtChain.append_left.parent_of_mem o ho⟩) + (fun o ho => hRW.tgtChain.append_left.inBounds_of_mem o ho) + -- Witness-free transport of a `DefinesDominating` scope point along an equality. + have ddT : ∀ {st : InterpreterState newCtx} {p₁ p₂ : InsertPoint} + {w1 : p₁.InBounds newCtx.raw} {w2 : p₂.InBounds newCtx.raw}, + p₁ = p₂ → st.DefinesDominating p₁ w1 → st.DefinesDominating p₂ w2 := by + intro st p₁ p₂ w1 w2 hp h; subst hp; exact h + -- Target `DefinesDominating` at the post entry, advancing `hTgtDefDom` through `pre ++ newOps`. + have tgtDefDomPost : s2'.DefinesDominating + (InsertPoint.afterLast (pre.toList ++ newOps.toList) newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) p'InMono := by + by_cases hpn : pre.toList ++ newOps.toList = [] + · have hs2' : state' = s2' := by + have hr := hrunT2 + rw [iopl_congr state' _ (by simp) hpn, interpretOpList_nil] at hr + exact (Prod.mk.inj (UBOr.ok.inj (Option.some.inj hr))).1 + exact ddT (by rw [hpn]; rfl) (hs2' ▸ hTgtDefDom) + · obtain ⟨fstOp, hfst⟩ : ∃ fstOp, (pre.toList ++ newOps.toList).head? = some fstOp := by + cases hc : pre.toList ++ newOps.toList with + | nil => exact absurd hc hpn + | cons a t => exact ⟨a, rfl⟩ + obtain ⟨lastOp, hlast⟩ : ∃ lastOp, (pre.toList ++ newOps.toList).getLast? = some lastOp := by + cases hc : (pre.toList ++ newOps.toList).getLast? with + | none => rw [List.getLast?_eq_none_iff] at hc; exact absurd hc hpn + | some x => exact ⟨x, rfl⟩ + have hStartTgt : InsertPoint.atStart! block newCtx.raw = InsertPoint.before fstOp := by + have hf : (block.get! newCtx.raw).firstOp = some fstOp := by + rw [hRW.tgtFirstOp, List.head?_append, hfst]; rfl + simp only [InsertPoint.atStart!, hf] + have hdd := interpretOpList_DefinesDominating ctxDom' hRW.tgtChain.append_left hfst + (ddT hStartTgt hTgtDefDom) hlast hrunT2 + have lastIn := hRW.tgtChain.append_left.inBounds_of_mem lastOp (List.mem_of_getLast? hlast) + have lastParent := hRW.tgtChain.append_left.parent_of_mem lastOp (List.mem_of_getLast? hlast) + refine ddT ?_ hdd + rw [InsertPoint.afterLast, hlast] + exact (InsertPoint.after!_eq_after lastParent lastIn).symm + -- Both points coincide at `before (post.head)` when `post ≠ []`. + have hPointsHeadPost : ∀ (h : post.toList ≠ []), + InsertPoint.afterLast (pre.toList ++ [op]) ctx.raw (InsertPoint.atStart! block ctx.raw) + = InsertPoint.before (post.toList.head h) ∧ + InsertPoint.afterLast (pre.toList ++ newOps.toList) newCtx.raw + (InsertPoint.atStart! block newCtx.raw) = InsertPoint.before (post.toList.head h) := by + intro h + obtain ⟨hd, tl, htl⟩ : ∃ hd tl, post.toList = hd :: tl := by + cases hc : post.toList with + | nil => exact absurd hc h + | cons a t => exact ⟨a, t, rfl⟩ + have hhd : post.toList.head h = hd := by simp [htl] + rw [hhd] + have hreassoc : ∀ (l : List OperationPtr), + (l ++ [hd]) ++ tl = l ++ [op] ++ (hd :: tl) → True := fun _ _ => trivial + refine ⟨afterLast_atStart!_eq_before_of_chain ?_ ?_, afterLast_atStart!_eq_before_of_chain ?_ ?_⟩ + · have hc := hRW.srcChain + rw [htl] at hc + have hc2 : block.OpChainSlice ctx.raw (((pre.toList ++ [op]) ++ [hd]) ++ tl) := by + rw [show ((pre.toList ++ [op]) ++ [hd]) ++ tl = pre.toList ++ [op] ++ (hd :: tl) from by + simp] + exact hc + exact hc2.append_left + · rw [hRW.srcFirstOp, htl]; simp [List.head?_append, List.append_assoc] + · have hc := hRW.tgtChain + rw [htl] at hc + have hc2 : block.OpChainSlice newCtx.raw (((pre.toList ++ newOps.toList) ++ [hd]) ++ tl) := by + rw [show ((pre.toList ++ newOps.toList) ++ [hd]) ++ tl + = pre.toList ++ newOps.toList ++ (hd :: tl) from by simp] + exact hc + exact hc2.append_left + · rw [hRW.tgtFirstOp, htl]; simp [List.head?_append, List.append_assoc] + -- `post.dropLast` never branches from `s2` (suffix of the non-branching `front`). + have hInitNoCfPost : ∀ (s3 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList post.toList.dropLast s2 + (fun o ho => hRW.postInBounds o (List.dropLast_subset post.toList ho)) + ≠ some (.ok (s3, some cf)) := by + by_cases hpe : post.toList = [] + · intro s3 cf hc + rw [iopl_congr s2 _ (by simp) (show post.toList.dropLast = [] by simp [hpe]), + interpretOpList_nil] at hc + exact absurd (Prod.mk.inj (UBOr.ok.inj (Option.some.inj hc))).2 (by simp) + · have hfpost : (pre.toList ++ [op] ++ post.toList).dropLast + = (pre.toList ++ [op]) ++ post.toList.dropLast := List.dropLast_append_of_ne_nil hpe + have hab : ∀ o ∈ (pre.toList ++ [op]) ++ post.toList.dropLast, o.InBounds ctx.raw := + fun o ho => frontIn o (by rw [hfpost]; exact ho) + have h : ∀ (s3 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList ((pre.toList ++ [op]) ++ post.toList.dropLast) state hab + ≠ some (.ok (s3, some cf)) := by + intro s3 cf hc + exact hFrontNoCf state s3 cf ((iopl_congr state hab frontIn hfpost.symm).symm.trans hc) + exact interpretOpList_append_noCf_right hab h hrunS2 + have hresPost := interpretOpList_monoAt newCtxVerif hCtxDom ctxDom' + (fun o ho => hRW.postInBounds o ho) (fun o ho => hRW.postInBounds' o ho) + hRW.srcChain.append_right hRW.postChain' + pInMono p'InMono hsRef2 tgtDefDomPost + (fun o ho => hRW.frame o (hRW.postInBounds o ho) (hRW.postInBounds' o ho) + (fun heq => hRW.opNotMemPost (heq ▸ ho))) + hPointsHeadPost hInitNoCfPost + have hSp : InsertPoint.afterLast (pre.toList ++ [op] ++ post.toList) ctx.raw + (InsertPoint.atStart! block ctx.raw) + = InsertPoint.afterLast post.toList ctx.raw + (InsertPoint.afterLast (pre.toList ++ [op]) ctx.raw + (InsertPoint.atStart! block ctx.raw)) := + InsertPoint.afterLast_append (pre.toList ++ [op]) post.toList ctx.raw _ + have hTp : InsertPoint.afterLast (pre.toList ++ newOps.toList ++ post.toList) newCtx.raw + (InsertPoint.atStart! block newCtx.raw) + = InsertPoint.afterLast post.toList newCtx.raw + (InsertPoint.afterLast (pre.toList ++ newOps.toList) newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) := + InsertPoint.afterLast_append (pre.toList ++ newOps.toList) post.toList newCtx.raw _ + simp only [hSp, hTp] + exact hresPost + -- Convert the op-list refinement to the terminated-list refinement (source UB refines anything). + simp only [interpretTerminatedOpList, bind] + rcases hsrc : interpretOpList (pre.toList ++ [op] ++ post.toList) state + (sourceListIn opIn hRW.preInBounds hRW.postInBounds) with _ | (⟨s, act⟩ | _) + · simp [Interp.isRefinedBy] + · simp only [hsrc, Interp.isRefinedBy_ok_target_iff] at hOpList + obtain ⟨⟨s', act'⟩, htgt, hsRef, hactRef⟩ := hOpList + simp only [htgt] + cases act with + | none => simp [Interp.isRefinedBy] + | some cf => + have ⟨cf', hact', hcfRef⟩ : ∃ cf', act' = some cf' ∧ cf.isRefinedBy cf' := by + cases act' with + | none => exact absurd hactRef (by simp [ControlFlowAction.optionIsRefinedBy]) + | some cf' => exact ⟨cf', rfl, hactRef⟩ + subst hact' + exact ⟨hsRef, hcfRef⟩ + · exact Interp.isRefinedBy_ub_target /-- Bridge `interpretBlock` to a `setArgumentValues?` followed by `interpretTerminatedOpList` over the block's operation list. When the block is empty (`firstOp = none`) the operation list is empty and both @@ -1060,6 +1391,20 @@ theorem interpretBlock_eq_setArgumentValues?_interpretTerminatedOpList rw [interpretOpChain_eq_interpretTerminatedOpList_of_firstOp bIn (by rw [BlockPtr.get!_eq_get bIn]; exact h)] +/-- The block entry point `atStart!` of a non-empty block is exactly the point before its first +operation (the head of its operation list). Bridges the `hPointsHead` obligation of +`interpretTerminatedOpList_monoAt` when the scope point is the block entry. -/ +theorem atStart!_eq_before_head {ctx : WfIRContext OpCode} {b : BlockPtr} + (bIn : b.InBounds ctx.raw) + (hne : (b.operationList ctx.raw ctx.wellFormed bIn).toList ≠ []) : + InsertPoint.atStart! b ctx.raw + = InsertPoint.before ((b.operationList ctx.raw ctx.wellFormed bIn).toList.head hne) := by + have hchain := BlockPtr.operationListWF ctx.raw b bIn ctx.wellFormed + have hfirst : (b.get! ctx.raw).firstOp + = some ((b.operationList ctx.raw ctx.wellFormed bIn).toList.head hne) := by + rw [hchain.first, ← Array.getElem?_toList, ← List.head?_eq_getElem?, List.head?_eq_some_head hne] + simp [InsertPoint.atStart!, hfirst] + /-! ## Stage C: `interpretBlock` refinement for every block Lifts the block-`B` simulation (and cross-context monotonicity for the unchanged blocks) to the full @@ -1080,20 +1425,38 @@ theorem RewrittenAt.interpretBlock_refinement {b : BlockPtr} (bIn : b.InBounds ctx.raw) {values values' : Array RuntimeValue} {state : InterpreterState ctx} {state' : InterpreterState newCtx} - (hState : state.isRefinedBy state' hRW.σ) + (hState : state.isRefinedByAt state' hRW.σ (.blockEntry b) (.blockEntry b) + bIn (hRW.blocksInBounds b bIn)) (hVals : values ⊒ values') (hSrcInv : ∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → ∀ fst (hfst : (b.get! ctx.raw).firstOp = some fst), (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) (by have := ctx.wellFormed.inBounds; grind)) + (hTgtInv : ∀ newVars', + state'.variables.setArgumentValues? b values' (hRW.blocksInBounds b bIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).DefinesDominating + (InsertPoint.atStart! b newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr + (hRW.blocksInBounds b bIn))) + (hSrcSplitB : ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (_termIn : term.InBounds ctx.raw), + (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) (hOpSim : OpStepSimulation op newOps hRW.σ opIn hRW.newOpsInBounds') : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × ControlFlowAction) (r₂ : InterpreterState newCtx × ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2.isRefinedBy r₂.2) + r₁.1.isRefinedByAt r₂.1 hRW.σ (InsertPoint.atEnd b) (InsertPoint.atEnd b) + bIn (hRW.blocksInBounds b bIn) ∧ r₁.2.isRefinedBy r₂.2) (interpretBlock b values state bIn) (interpretBlock b values' state' (hRW.blocksInBounds b bIn)) := by have bIn' := hRW.blocksInBounds b bIn + -- Proof-irrelevant `interpretOpList` list-congruence (used to relabel `dropLast`/`front`). + have iopl_congr : ∀ {cc : WfIRContext OpCode} {l l' : List OperationPtr} (s : InterpreterState cc) + (hl : ∀ o ∈ l, o.InBounds cc.raw) (hl' : ∀ o ∈ l', o.InBounds cc.raw), + l = l' → interpretOpList l s hl = interpretOpList l' s hl' := by + intro cc l l' s hl hl' h; subst h; rfl rw [interpretBlock_eq_setArgumentValues?_interpretTerminatedOpList bIn, interpretBlock_eq_setArgumentValues?_interpretTerminatedOpList bIn'] rcases hsa : state.variables.setArgumentValues? b values bIn with _ | newVars @@ -1115,15 +1478,19 @@ theorem RewrittenAt.interpretBlock_refinement exact RuntimeValue.Conforms_of_isRefinedBy hPt ((VariableState.setArgumentValues?_isSome_iff_conforms state.variables).mpr ⟨newVars, hsa⟩ j (hRW.numArgsEq bIn ▸ hj)) - obtain ⟨newVars', hsa', hpsRefVar⟩ := VariableState.setArgumentValues?_isRefinedBy - hState.2 hVals bIn bIn' (hRW.argsApplyToArray bIn) - (fun val valIn arg hMem heq => by - obtain ⟨i, _hi, rfl⟩ := BlockPtr.getArguments!.mem_iff_exists_index.mp hMem - exact hRW.mappingReflectsArg val valIn i heq) + obtain ⟨newVars', hsa', hpsRefVar⟩ := VariableState.setArgumentValues?_isRefinedByAt + bIn bIn' hState.2 hVals (hRW.argsApplyToArray bIn) + (fun val valIn hNotArg _hdom => hRW.mappingImageNotArg bIn valIn hNotArg) tgtConforms hsa - have hpsRef : (InterpreterState.mk newVars state.memory).isRefinedBy - ⟨newVars', state'.memory⟩ hRW.σ := ⟨hState.1, hpsRefVar⟩ + have hpsRef : (InterpreterState.mk newVars state.memory).isRefinedByAt + ⟨newVars', state'.memory⟩ hRW.σ + (InsertPoint.atStart! b ctx.raw) (InsertPoint.atStart! b newCtx.raw) := ⟨hState.1, hpsRefVar⟩ + have hTgtDD := hTgtInv newVars' hsa' simp only [hsa', Option.bind_some] + -- Running `b`'s whole operation list from the entry lands at `atEnd b` (both contexts). + have hSp : InsertPoint.afterLast (b.operationList ctx.raw ctx.wellFormed bIn).toList ctx.raw + (InsertPoint.atStart! b ctx.raw) = InsertPoint.atEnd b := + afterLast_operationList_atStart!_eq_atEnd bIn by_cases hbB : b = block · -- Rewritten block `B`: rewrite the op-lists and apply the block-`B` simulation. subst hbB @@ -1131,25 +1498,82 @@ theorem RewrittenAt.interpretBlock_refinement = pre.toList ++ [op] ++ post.toList := by rw [hRW.srcList]; simp have htgt : (b.operationList newCtx.raw newCtx.wellFormed bIn').toList = pre.toList ++ newOps.toList ++ post.toList := by rw [hRW.tgtList]; simp + have hTp : InsertPoint.afterLast (pre.toList ++ newOps.toList ++ post.toList) newCtx.raw + (InsertPoint.atStart! b newCtx.raw) = InsertPoint.atEnd b := by + rw [← htgt]; exact afterLast_operationList_atStart!_eq_atEnd bIn' + have hSplit : ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw), + (pre.toList ++ [op] ++ post.toList) = front ++ [term] ∧ + (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf))) := by + obtain ⟨front, term, frontIn, _termIn, harr, hno⟩ := hSrcSplitB + exact ⟨front, term, frontIn, by rw [← hsrc]; exact harr, hno⟩ + have hEqLemArg : ∀ fst (hfst : (b.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind) := + fun fst hfst => hSrcInv newVars hsa fst hfst + have hbs := hRW.blockSimulation newCtxVerif hCtxDom hpsRef hTgtDD hEqLemArg hSplit hOpSim + simp only [hsrc] at hSp simp only [hsrc, htgt] - grind [hRW.blockSimulation] - · -- Other block: op-lists identical, apply cross-context monotonicity. + simp only [hSp, hTp] at hbs + exact hbs + · -- Other block: op-lists identical, apply scoped cross-context monotonicity. have hother := hRW.otherBlocks b bIn bIn' hbB have chainSrc := BlockPtr.operationListWF ctx.raw b bIn ctx.wellFormed have chainTgt := BlockPtr.operationListWF newCtx.raw b bIn' newCtx.wellFormed - simp only [hother] - have opsIn : ∀ o ∈ (b.operationList newCtx.raw newCtx.wellFormed bIn').toList, - o.InBounds ctx.raw := by - intro o ho - rw [← hother] at ho - exact chainSrc.arrayInBounds (by simpa using ho) - have opsIn' : ∀ o ∈ (b.operationList newCtx.raw newCtx.wellFormed bIn').toList, - o.InBounds newCtx.raw := fun o ho => chainTgt.arrayInBounds (by simpa using ho) - have hne_op : ∀ o ∈ (b.operationList newCtx.raw newCtx.wellFormed bIn').toList, o ≠ op := by + have hlistEq : (b.operationList newCtx.raw newCtx.wellFormed bIn').toList + = (b.operationList ctx.raw ctx.wellFormed bIn).toList := + (congrArg Array.toList hother).symm + have hTp : InsertPoint.afterLast (b.operationList ctx.raw ctx.wellFormed bIn).toList newCtx.raw + (InsertPoint.atStart! b newCtx.raw) = InsertPoint.atEnd b := by + rw [← hlistEq]; exact afterLast_operationList_atStart!_eq_atEnd bIn' + have opsIn : ∀ o ∈ (b.operationList ctx.raw ctx.wellFormed bIn).toList, + o.InBounds ctx.raw := fun o ho => chainSrc.arrayInBounds (by simpa using ho) + have opsIn' : ∀ o ∈ (b.operationList ctx.raw ctx.wellFormed bIn).toList, + o.InBounds newCtx.raw := by + intro o ho; rw [← hlistEq] at ho; exact chainTgt.arrayInBounds (by simpa using ho) + have hChainSrc : b.OpChainSlice ctx.raw (b.operationList ctx.raw ctx.wellFormed bIn).toList := + chainSrc.opChainSlice + have hChainTgt : b.OpChainSlice newCtx.raw (b.operationList ctx.raw ctx.wellFormed bIn).toList := by + rw [← hlistEq]; exact chainTgt.opChainSlice + have hne_op : ∀ o ∈ (b.operationList ctx.raw ctx.wellFormed bIn).toList, o ≠ op := by intro o ho heq; subst heq; exact hRW.opErased (opsIn' o ho) - have hFrame : ∀ o, (h : o ∈ (b.operationList newCtx.raw newCtx.wellFormed bIn').toList) → + have hFrame : ∀ o, (h : o ∈ (b.operationList ctx.raw ctx.wellFormed bIn).toList) → (hRW.σ).PreservesOperation o o := fun o h => hRW.frame_of_ne (opsIn o h) (hne_op o h) - exact interpretTerminatedOpList_mono newCtxVerif opsIn opsIn' hpsRef hFrame + obtain ⟨front, term, frontIn, _termIn, harr, hno⟩ := hSrcSplitB + have hdrop : (b.operationList ctx.raw ctx.wellFormed bIn).toList.dropLast = front := by + rw [harr, List.dropLast_concat] + have hPH : ∀ (h : (b.operationList ctx.raw ctx.wellFormed bIn).toList ≠ []), + InsertPoint.atStart! b ctx.raw + = .before ((b.operationList ctx.raw ctx.wellFormed bIn).toList.head h) ∧ + InsertPoint.atStart! b newCtx.raw + = .before ((b.operationList ctx.raw ctx.wellFormed bIn).toList.head h) := by + intro h + refine ⟨atStart!_eq_before_head bIn h, ?_⟩ + have hne' : (b.operationList newCtx.raw newCtx.wellFormed bIn').toList ≠ [] := by + rw [hlistEq]; exact h + rw [atStart!_eq_before_head bIn' hne'] + congr 1 + have hh : (b.operationList newCtx.raw newCtx.wellFormed bIn').toList.head? + = (b.operationList ctx.raw ctx.wellFormed bIn).toList.head? := by rw [hlistEq] + rw [List.head?_eq_some_head hne', List.head?_eq_some_head h] at hh + exact Option.some.inj hh + have hInitNoCf : ∀ (s2 : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList (b.operationList ctx.raw ctx.wellFormed bIn).toList.dropLast + ⟨newVars, state.memory⟩ + (fun o ho => opsIn o (List.dropLast_subset _ ho)) ≠ some (.ok (s2, some cf)) := by + intro s2 cf hcontra + apply hno ⟨newVars, state.memory⟩ s2 cf + rw [← iopl_congr ⟨newVars, state.memory⟩ + (fun o ho => opsIn o (List.dropLast_subset _ ho)) frontIn hdrop] + exact hcontra + have hmono := interpretTerminatedOpList_monoAt newCtxVerif hCtxDom hRW.newCtxDom + opsIn opsIn' hChainSrc hChainTgt + (p := InsertPoint.atStart! b ctx.raw) (p' := InsertPoint.atStart! b newCtx.raw) + (by grind) (by grind) hpsRef hTgtDD hFrame hPH hInitNoCf + simp only [hlistEq] + simp only [hSp, hTp] at hmono + exact hmono /-! ## Stage B bundling: cross-block invariant re-establishment @@ -1324,6 +1748,94 @@ theorem interpretBlock_branch_definesDominating_succ simp only [hAtStart] at result exact result +/-- **Target-side cross-block re-establishment (`atStart!` framing).** Like +`interpretBlock_branch_definesDominating_succ`, but states both the entry invariant and the +re-established successor invariant at the block-entry point `atStart!` (rather than `before` the first +operation), so no first-operation case split is needed by callers. -/ +theorem interpretBlock_branch_definesDominating_succ_atStart + {ctx : WfIRContext OpCode} (ctxDom : ctx.Dom) + {b succ : BlockPtr} (bIn : b.InBounds ctx.raw) (succIn : succ.InBounds ctx.raw) + {values res : Array RuntimeValue} {state exitState : InterpreterState ctx} + {front : List OperationPtr} {term : OperationPtr} + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (termIn : term.InBounds ctx.raw) + (harr : (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term]) + (hFrontNoCf : ∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf))) + (hEntryInv : ∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → + (InterpreterState.mk newVars state.memory).DefinesDominating (.atStart! b ctx.raw) + (by have := ctx.wellFormed.inBounds; grind)) + (hRun : interpretBlock b values state bIn = some (.ok (exitState, .branch res succ))) : + ∀ newVars', exitState.variables.setArgumentValues? succ res succIn = some newVars' → + (InterpreterState.mk newVars' exitState.memory).DefinesDominating (.atStart! succ ctx.raw) + (by have := ctx.wellFormed.inBounds; grind) := by + intro newVars' hArgs + obtain ⟨newVars, s', hSetArgs, hFrontRun, hTermRun⟩ := + interpretBlock_branch_split bIn frontIn termIn harr hFrontNoCf hRun + obtain ⟨hparent, hnext, hchain, hfirst⟩ := operationList_split_term_facts bIn termIn harr + have hBeforeTerm : s'.DefinesDominating (.before term) termIn := + interpretOpList_DefinesDominating_before ctxDom frontIn termIn hchain + (fun fst _ hhead => by + have hfo : (b.get! ctx.raw).firstOp = some fst := by rw [hfirst]; exact hhead + have hdd := hEntryInv newVars hSetArgs + have heq : InsertPoint.atStart! b ctx.raw = .before fst := by simp [InsertPoint.atStart!, hfo] + simp only [heq] at hdd + exact hdd) + hFrontRun + have hAfterTerm := interpretOp_DefinesDominating ctxDom hBeforeTerm hparent hTermRun + have succMem : succ ∈ term.getSuccessors! ctx.raw := + interpretOp_branch_dest_mem_getSuccessors! hTermRun + have hlast : (b.get! ctx.raw).lastOp = some term := by grind + have hSucc : succ ∈ b.getSuccessors! ctx.raw := by + simp only [BlockPtr.getSuccessors!, hlast]; exact succMem + have hAtEnd : InsertPoint.after term ctx.raw b hparent termIn = InsertPoint.atEnd b := by + grind [InsertPoint.after] + simp only [hAtEnd] at hAfterTerm + exact InterpreterState.DefinesDominating.setArgumentValues?_succ_entry ctxDom bIn + hSucc hAfterTerm hArgs + +/-- **Cross-edge transport of the scoped entry relation.** Given the full scoped state refinement at +the predecessor's exit (`atEnd b`), produce the incoming-edge scoped relation at the successor's +entry (`.blockEntry succ`). This is a pure scope-weakening (`isRefinedByAt.weaken`): a value in +`succ`'s incoming-edge scope dominates `succ`'s entry and is not one of `succ`'s arguments, so by +`value_dominatesIp_successor_entry` it already dominated `b`'s exit, where the exit relation applies. +The same argument holds on the target side, value-for-value (no `σ`-image reasoning needed). -/ +theorem RewrittenAt.transport_succ_entry + (hRW : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (hCtxDom : ctx.Dom) + {b succ : BlockPtr} (bIn : b.InBounds ctx.raw) (succIn : succ.InBounds ctx.raw) + (succIn' : succ.InBounds newCtx.raw) + (hsucc : succ ∈ b.getSuccessors! ctx.raw) (hsucc' : succ ∈ b.getSuccessors! newCtx.raw) + {s : InterpreterState ctx} {s' : InterpreterState newCtx} + (h : s.isRefinedByAt s' hRW.σ (InsertPoint.atEnd b) (InsertPoint.atEnd b) + bIn (hRW.blocksInBounds b bIn)) : + s.isRefinedByAt s' hRW.σ (.blockEntry succ) (.blockEntry succ) + succIn succIn' := + h.weaken + (fun _val hsc => + (WfIRContext.Dom.value_dominatesIp_successor_entry hCtxDom bIn hsucc hsc.1).resolve_right hsc.2) + (fun _val hsc => + (WfIRContext.Dom.value_dominatesIp_successor_entry hRW.newCtxDom + (hRW.blocksInBounds b bIn) hsucc' hsc.1).resolve_right hsc.2) + +/-- A branching block run lands in one of the block's CFG successors. -/ +theorem interpretBlock_branch_mem_getSuccessors! + {ctx : WfIRContext OpCode} {b succ : BlockPtr} (bIn : b.InBounds ctx.raw) + {values res : Array RuntimeValue} {state exitState : InterpreterState ctx} + {front : List OperationPtr} {term : OperationPtr} + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (termIn : term.InBounds ctx.raw) + (harr : (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term]) + (hFrontNoCf : ∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf))) + (hRun : interpretBlock b values state bIn = some (.ok (exitState, .branch res succ))) : + succ ∈ b.getSuccessors! ctx.raw := by + obtain ⟨newVars, s', hSetArgs, hFrontRun, hTermRun⟩ := + interpretBlock_branch_split bIn frontIn termIn harr hFrontNoCf hRun + obtain ⟨hparent, hnext, hchain, hfirst⟩ := operationList_split_term_facts bIn termIn harr + have succMem : succ ∈ term.getSuccessors! ctx.raw := + interpretOp_branch_dest_mem_getSuccessors! hTermRun + have hlast : (b.get! ctx.raw).lastOp = some term := by grind + simp only [BlockPtr.getSuccessors!, hlast]; exact succMem + /-! ## Stage D: `interpretBlockCFG` refinement (the CFG walk) Lifts the per-block refinement (Stage C, `interpretBlock_refinement`) to the whole CFG walk via the @@ -1350,37 +1862,57 @@ theorem RewrittenAt.interpretBlockCFG_refinement (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + (hTgtSplit : ∀ (b : BlockPtr) (bIn' : b.InBounds newCtx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds newCtx.raw) (_termIn : term.InBounds newCtx.raw), + (b.operationList newCtx.raw newCtx.wellFormed bIn').toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState newCtx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) {b : BlockPtr} (bIn : b.InBounds ctx.raw) {values values' : Array RuntimeValue} {state : InterpreterState ctx} {state' : InterpreterState newCtx} - (hState : state.isRefinedBy state' hRW.σ) + (hState : state.isRefinedByAt state' hRW.σ (.blockEntry b) (.blockEntry b) + bIn (hRW.blocksInBounds b bIn)) (hVals : values ⊒ values') (hSrcInv : ∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → ∀ fst (hfst : (b.get! ctx.raw).firstOp = some fst), (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) - (by have := ctx.wellFormed.inBounds; grind)) : + (by have := ctx.wellFormed.inBounds; grind)) + (hTgtInv : ∀ newVars', + state'.variables.setArgumentValues? b values' (hRW.blocksInBounds b bIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).DefinesDominating + (InsertPoint.atStart! b newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr + (hRW.blocksInBounds b bIn))) : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState newCtx × Array RuntimeValue) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) (interpretBlockCFG b values state bIn) (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn)) := by refine interpretBlockCFG.fixpoint_induct (motive := fun g => ∀ (b : BlockPtr) (bIn : b.InBounds ctx.raw) (values values' : Array RuntimeValue) (state : InterpreterState ctx) (state' : InterpreterState newCtx), - state.isRefinedBy state' hRW.σ → values ⊒ values' → + state.isRefinedByAt state' hRW.σ (.blockEntry b) (.blockEntry b) + bIn (hRW.blocksInBounds b bIn) → values ⊒ values' → (∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → ∀ fst (hfst : (b.get! ctx.raw).firstOp = some fst), (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) (by have := ctx.wellFormed.inBounds; grind)) → + (∀ newVars', + state'.variables.setArgumentValues? b values' (hRW.blocksInBounds b bIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).DefinesDominating + (InsertPoint.atStart! b newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr + (hRW.blocksInBounds b bIn))) → Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState newCtx × Array RuntimeValue) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) (g b values state bIn) (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) - ?admissible ?step b bIn values values' state state' hState hVals hSrcInv + ?admissible ?step b bIn values values' state state' hState hVals hSrcInv hTgtInv case admissible => -- Peel the (dependent) leading `∀ b` together with the `g b` application with -- `admissible_pi_apply`, the remaining (non-dependent) binders with `admissible_pi`, the @@ -1390,15 +1922,22 @@ theorem RewrittenAt.interpretBlockCFG_refinement b.InBounds ctx.raw → Interp (InterpreterState ctx × Array RuntimeValue)) => ∀ (bIn : b.InBounds ctx.raw) (values values' : Array RuntimeValue) (state : InterpreterState ctx) (state' : InterpreterState newCtx), - state.isRefinedBy state' hRW.σ → values ⊒ values' → + state.isRefinedByAt state' hRW.σ (.blockEntry b) (.blockEntry b) + bIn (hRW.blocksInBounds b bIn) → values ⊒ values' → (∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → ∀ fst (hfst : (b.get! ctx.raw).firstOp = some fst), (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) (by have := ctx.wellFormed.inBounds; grind)) → + (∀ newVars', + state'.variables.setArgumentValues? b values' (hRW.blocksInBounds b bIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).DefinesDominating + (InsertPoint.atStart! b newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr + (hRW.blocksInBounds b bIn))) → Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState newCtx × Array RuntimeValue) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) (gb values state bIn) (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) intro b @@ -1410,13 +1949,14 @@ theorem RewrittenAt.interpretBlockCFG_refinement apply Lean.Order.admissible_pi; intro hState apply Lean.Order.admissible_pi; intro hVals apply Lean.Order.admissible_pi; intro hSrcInv + apply Lean.Order.admissible_pi; intro hTgtInv apply Lean.Order.admissible_apply (P := fun (_v : Array RuntimeValue) (gv : InterpreterState ctx → b.InBounds ctx.raw → Interp (InterpreterState ctx × Array RuntimeValue)) => Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState newCtx × Array RuntimeValue) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) (gv state bIn) (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) (x := values) apply Lean.Order.admissible_apply @@ -1425,7 +1965,7 @@ theorem RewrittenAt.interpretBlockCFG_refinement Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState newCtx × Array RuntimeValue) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) (gs bIn) (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) (x := state) apply Lean.Order.admissible_apply @@ -1433,13 +1973,14 @@ theorem RewrittenAt.interpretBlockCFG_refinement Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState newCtx × Array RuntimeValue) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) gh (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) (x := bIn) exact Lean.Order.admissible_flatOrder _ trivial case step => - intro g IH b bIn values values' state state' hState hVals hSrcInv - have hBlk := hRW.interpretBlock_refinement newCtxVerif hCtxDom bIn hState hVals hSrcInv hOpSim + intro g IH b bIn values values' state state' hState hVals hSrcInv hTgtInv + have hBlk := hRW.interpretBlock_refinement newCtxVerif hCtxDom bIn hState hVals hSrcInv hTgtInv + (hSrcSplit b bIn) hOpSim rw [interpretBlockCFG] rcases hsrc : interpretBlock b values state bIn with _ | (⟨s, act⟩ | _) · -- Source block run fails: the CFG step is `none`, refinement is trivial. @@ -1455,7 +1996,7 @@ theorem RewrittenAt.interpretBlockCFG_refinement cases act' <;> simp_all [ControlFlowAction.isRefinedBy] subst hact' simp only [hsrc, htgt, Interp.isRefinedBy] - exact ⟨hsRef, hr⟩ + exact ⟨hsRef.1, hr⟩ | branch r succ => -- A `branch`: the target action branches to the *same* successor with refined values. obtain ⟨r', hact', hr⟩ : ∃ r', act' = .branch r' succ ∧ r ⊒ r' := by @@ -1463,12 +2004,23 @@ theorem RewrittenAt.interpretBlockCFG_refinement subst hact' by_cases hsuccIn : succ.InBounds ctx.raw · -- Successor in bounds: both walks recurse into `succ`; close with the IH, threading the - -- source-entry SSA invariant across the CFG edge. + -- source SSA invariant, the target dominance invariant, and the scoped entry relation + -- across the CFG edge. + have bIn' := hRW.blocksInBounds b bIn obtain ⟨front, term, frontIn, termIn, harr, hFrontNoCf⟩ := hSrcSplit b bIn + obtain ⟨frontT, termT, frontInT, termInT, harrT, hFrontNoCfT⟩ := hTgtSplit b bIn' have hSrcInvSucc := interpretBlock_branch_equationLemmaAt_succ hCtxDom bIn hsuccIn frontIn termIn harr hFrontNoCf hSrcInv hsrc + have hsucc : succ ∈ b.getSuccessors! ctx.raw := + interpretBlock_branch_mem_getSuccessors! bIn frontIn termIn harr hFrontNoCf hsrc + have hsucc' : succ ∈ b.getSuccessors! newCtx.raw := + interpretBlock_branch_mem_getSuccessors! bIn' frontInT termInT harrT hFrontNoCfT htgt + have hStateSucc := hRW.transport_succ_entry hCtxDom bIn hsuccIn + (hRW.blocksInBounds succ hsuccIn) hsucc hsucc' hsRef + have hTgtInvSucc := interpretBlock_branch_definesDominating_succ_atStart hRW.newCtxDom + bIn' (hRW.blocksInBounds succ hsuccIn) frontInT termInT harrT hFrontNoCfT hTgtInv htgt simp only [hsrc, htgt, dif_pos hsuccIn, dif_pos (hRW.blocksInBounds succ hsuccIn)] - exact IH succ hsuccIn r r' s s' hsRef hr hSrcInvSucc + exact IH succ hsuccIn r r' s s' hStateSucc hr hSrcInvSucc hTgtInvSucc · -- Successor out of bounds in the source: the source CFG step is `none`, refinement trivial. simp only [hsrc, dif_neg hsuccIn, Interp.isRefinedBy_none_target] · -- Source block run is UB, which is refined by any target outcome. @@ -1496,18 +2048,28 @@ theorem InterpreterState.empty_isRefinedBy {ctx ctx' : WfIRContext OpCode} intro val valIn sourceVar hget simp [VariableState.getVar?, VariableState.empty] at hget +/-- The fresh, empty interpreter state satisfies the scoped relation at any pair of refinement +points: it defines no variables, so the constraint is vacuous (and the memories coincide). -/ +theorem InterpreterState.empty_isRefinedByAt {ctx ctx' : WfIRContext OpCode} + (μ : ValueMapping ctx ctx') (mem : MemoryState) (s s' : RefinementPoint) + (sIn : s.InBounds ctx.raw) (s'In : s'.InBounds ctx'.raw) : + (InterpreterState.mk (VariableState.empty ctx) mem).isRefinedByAt + (InterpreterState.mk (VariableState.empty ctx') mem) μ s s' sIn s'In := by + refine ⟨rfl, ?_⟩ + intro val valIn _ _ sv tv hget _ + simp [VariableState.getVar?, VariableState.empty] at hget + /-- Lift a `σ`-refinement of two region runs to a `FunctionResult` refinement of the corresponding function runs: `interpretFunction` post-processes `interpretRegion` by keeping only the final memory and the returned values, and `InterpreterState.isRefinedBy` already entails equal memories, so the refinement is preserved by that projection. -/ theorem Interp.isRefinedBy_functionResult_of_region {ctx ctx' : WfIRContext OpCode} - {μ : ValueMapping ctx ctx'} {a : Interp (InterpreterState ctx × Array RuntimeValue)} {b : Interp (InterpreterState ctx' × Array RuntimeValue)} (h : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState ctx' × Array RuntimeValue) => - r₁.1.isRefinedBy r₂.1 μ ∧ r₁.2 ⊒ r₂.2) a b) : + r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) a b) : Interp.isRefinedBy FunctionResult.isRefinedBy (a >>= fun x => pure (x.1.memory, x.2)) (b >>= fun x => pure (x.1.memory, x.2)) := by @@ -1516,20 +2078,19 @@ theorem Interp.isRefinedBy_functionResult_of_region {ctx ctx' : WfIRContext OpCo · simp only [Interp.isRefinedBy_ok_target_iff] at h obtain ⟨⟨sf', sres'⟩, htgt, hsRef, hresRef⟩ := h subst htgt - exact ⟨hsRef.1, hresRef⟩ + exact ⟨hsRef, hresRef⟩ · exact Interp.isRefinedBy_ub_target /-- Lift a `σ`-refinement of two region runs to an array refinement of the corresponding module runs: `interpretModule` post-processes `interpretRegion` by keeping only the returned values, so the value-array refinement carried by the region refinement is exactly what survives. -/ theorem Interp.isRefinedBy_moduleResult_of_region {ctx ctx' : WfIRContext OpCode} - {μ : ValueMapping ctx ctx'} {a : Interp (InterpreterState ctx × Array RuntimeValue)} {b : Interp (InterpreterState ctx' × Array RuntimeValue)} (h : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState ctx' × Array RuntimeValue) => - r₁.1.isRefinedBy r₂.1 μ ∧ r₁.2 ⊒ r₂.2) a b) : + r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) a b) : Interp.isRefinedBy RuntimeValue.arrayIsRefinedBy (a >>= fun x => pure x.2) (b >>= fun x => pure x.2) := by @@ -1560,22 +2121,38 @@ theorem RewrittenAt.interpretRegion_refinement (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + (hTgtSplit : ∀ (b : BlockPtr) (bIn' : b.InBounds newCtx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds newCtx.raw) (_termIn : term.InBounds newCtx.raw), + (b.operationList newCtx.raw newCtx.wellFormed bIn').toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState newCtx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) {r r' : RegionPtr} (rIn : r.InBounds ctx.raw) (rIn' : r'.InBounds newCtx.raw) (hrr : r' = r) {values values' : Array RuntimeValue} {state : InterpreterState ctx} {state' : InterpreterState newCtx} - (hState : state.isRefinedBy state' hRW.σ) + (hState : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw), + state.isRefinedByAt state' hRW.σ (.blockEntry entryBlock) (.blockEntry entryBlock) + entryIn (hRW.blocksInBounds entryBlock entryIn)) (hVals : values ⊒ values') (hSrcInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) (newVars : VariableState ctx), state.variables.setArgumentValues? entryBlock values entryIn = some newVars → ∀ fst (hfst : (entryBlock.get! ctx.raw).firstOp = some fst), (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) - (by have := ctx.wellFormed.inBounds; grind)) : + (by have := ctx.wellFormed.inBounds; grind)) + (hTgtInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) + (newVars' : VariableState newCtx), + state'.variables.setArgumentValues? entryBlock values' + (hRW.blocksInBounds entryBlock entryIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).DefinesDominating + (InsertPoint.atStart! entryBlock newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed + (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) : Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState newCtx × Array RuntimeValue) => - r₁.1.isRefinedBy r₂.1 hRW.σ ∧ r₁.2 ⊒ r₂.2) + r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) (interpretRegion r values state rIn) (interpretRegion r' values' state' rIn') := by subst hrr @@ -1607,8 +2184,10 @@ theorem RewrittenAt.interpretRegion_refinement rw [hfb, hentry] at h1 simpa using h1.symm subst entryBlock' - exact hRW.interpretBlockCFG_refinement newCtxVerif hCtxDom hOpSim hSrcSplit entryIn - hState hVals (fun newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) + exact hRW.interpretBlockCFG_refinement newCtxVerif hCtxDom hOpSim hSrcSplit hTgtSplit entryIn + (hState entryBlock entryIn) hVals + (fun newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) + (fun newVars' h => hTgtInv entryBlock entryIn newVars' h) /-- **Stage E — `interpretFunction` refinement (monotonicity).** Interpreting a function operation @@ -1629,6 +2208,12 @@ theorem RewrittenAt.interpretFunction_refinement (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + (hTgtSplit : ∀ (b : BlockPtr) (bIn' : b.InBounds newCtx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds newCtx.raw) (_termIn : term.InBounds newCtx.raw), + (b.operationList newCtx.raw newCtx.wellFormed bIn').toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState newCtx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) {funcOp : OperationPtr} (funcOpIn : funcOp.InBounds ctx.raw) (funcOpIn' : funcOp.InBounds newCtx.raw) {values values' : Array RuntimeValue} {mem : MemoryState} @@ -1638,7 +2223,15 @@ theorem RewrittenAt.interpretFunction_refinement (VariableState.empty ctx).setArgumentValues? entryBlock values entryIn = some newVars → ∀ fst (hfst : (entryBlock.get! ctx.raw).firstOp = some fst), (InterpreterState.mk newVars mem).EquationLemmaAt (.before fst) - (by have := ctx.wellFormed.inBounds; grind)) : + (by have := ctx.wellFormed.inBounds; grind)) + (hTgtInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) + (newVars' : VariableState newCtx), + (VariableState.empty newCtx).setArgumentValues? entryBlock values' + (hRW.blocksInBounds entryBlock entryIn) = some newVars' → + (InterpreterState.mk newVars' mem).DefinesDominating + (InsertPoint.atStart! entryBlock newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed + (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) : Interp.isRefinedBy FunctionResult.isRefinedBy (interpretFunction funcOp values mem funcOpIn) (interpretFunction funcOp values' mem funcOpIn') := by @@ -1667,10 +2260,14 @@ theorem RewrittenAt.interpretFunction_refinement exact OperationPtr.getRegions!_inBounds newCtx.wellFormed.inBounds funcOpIn' (by rw [OperationPtr.getNumRegions!_eq_getNumRegions funcOpIn']; exact hi') -- The single region is preserved, so its interpretation refines (Stage E region lemma). - have hregRef := hRW.interpretRegion_refinement newCtxVerif hCtxDom hOpSim hSrcSplit rIn rIn' hReg - (state := ⟨.empty ctx, mem⟩) (state' := ⟨.empty newCtx, mem⟩) - (InterpreterState.empty_isRefinedBy hRW.σ mem) hVals + have hregRef := hRW.interpretRegion_refinement newCtxVerif hCtxDom hOpSim hSrcSplit hTgtSplit + rIn rIn' hReg (state := ⟨.empty ctx, mem⟩) (state' := ⟨.empty newCtx, mem⟩) + (fun entryBlock entryIn => InterpreterState.empty_isRefinedByAt hRW.σ mem + (.blockEntry entryBlock) (.blockEntry entryBlock) + entryIn (hRW.blocksInBounds entryBlock entryIn)) + hVals (fun entryBlock entryIn newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) + (fun entryBlock entryIn newVars' h => hTgtInv entryBlock entryIn newVars' h) -- The function result keeps only the final memory and returned values of the region run. show Interp.isRefinedBy FunctionResult.isRefinedBy ((interpretRegion (funcOp.getRegion ctx.raw 0 funcOpIn hi) values ⟨.empty ctx, mem⟩ rIn) @@ -1701,6 +2298,12 @@ theorem RewrittenAt.interpretModule_refinement (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + (hTgtSplit : ∀ (b : BlockPtr) (bIn' : b.InBounds newCtx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds newCtx.raw) (_termIn : term.InBounds newCtx.raw), + (b.operationList newCtx.raw newCtx.wellFormed bIn').toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState newCtx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) {moduleOp : OperationPtr} (moduleOpIn : moduleOp.InBounds ctx.raw) (moduleOpIn' : moduleOp.InBounds newCtx.raw) (hSrcInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) @@ -1708,7 +2311,15 @@ theorem RewrittenAt.interpretModule_refinement (VariableState.empty ctx).setArgumentValues? entryBlock #[] entryIn = some newVars → ∀ fst (hfst : (entryBlock.get! ctx.raw).firstOp = some fst), (InterpreterState.mk newVars MemoryState.empty).EquationLemmaAt (.before fst) - (by have := ctx.wellFormed.inBounds; grind)) : + (by have := ctx.wellFormed.inBounds; grind)) + (hTgtInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) + (newVars' : VariableState newCtx), + (VariableState.empty newCtx).setArgumentValues? entryBlock #[] + (hRW.blocksInBounds entryBlock entryIn) = some newVars' → + (InterpreterState.mk newVars' MemoryState.empty).DefinesDominating + (InsertPoint.atStart! entryBlock newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed + (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) : Interp.isRefinedBy RuntimeValue.arrayIsRefinedBy (interpretModule ctx moduleOp moduleOpIn) (interpretModule newCtx moduleOp moduleOpIn') := by @@ -1737,11 +2348,14 @@ theorem RewrittenAt.interpretModule_refinement exact OperationPtr.getRegions!_inBounds newCtx.wellFormed.inBounds moduleOpIn' (by rw [OperationPtr.getNumRegions!_eq_getNumRegions moduleOpIn']; exact hi') -- The single region is preserved, so its interpretation refines (Stage E region lemma). - have hregRef := hRW.interpretRegion_refinement newCtxVerif hCtxDom hOpSim hSrcSplit rIn rIn' hReg - (state := InterpreterState.empty ctx) (state' := InterpreterState.empty newCtx) - (InterpreterState.empty_isRefinedBy hRW.σ MemoryState.empty) + have hregRef := hRW.interpretRegion_refinement newCtxVerif hCtxDom hOpSim hSrcSplit hTgtSplit + rIn rIn' hReg (state := InterpreterState.empty ctx) (state' := InterpreterState.empty newCtx) + (fun entryBlock entryIn => InterpreterState.empty_isRefinedByAt hRW.σ + MemoryState.empty (.blockEntry entryBlock) (.blockEntry entryBlock) + entryIn (hRW.blocksInBounds entryBlock entryIn)) (RuntimeValue.arrayIsRefinedBy_refl #[]) (fun entryBlock entryIn newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) + (fun entryBlock entryIn newVars' h => hTgtInv entryBlock entryIn newVars' h) -- The module result keeps only the returned values of the region run. show Interp.isRefinedBy RuntimeValue.arrayIsRefinedBy ((interpretRegion (moduleOp.getRegion ctx.raw 0 moduleOpIn hi) #[] (InterpreterState.empty ctx) rIn) From 622174dee27540b1c65bbf0bebefa8718071d55b Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Sun, 28 Jun 2026 22:03:27 +0200 Subject: [PATCH 14/31] Done refactoring with isRefinedByAt --- Veir/Dominance.lean | 39 +++ Veir/Interpreter/Refinement/Basic.lean | 24 -- Veir/Interpreter/Refinement/Lemmas.lean | 12 - Veir/Interpreter/Refinement/Monotonicity.lean | 295 +----------------- Veir/PatternRewriter/Soundness.lean | 127 ++++---- 5 files changed, 111 insertions(+), 386 deletions(-) diff --git a/Veir/Dominance.lean b/Veir/Dominance.lean index 54fd1a10d..4bd5b93fb 100644 --- a/Veir/Dominance.lean +++ b/Veir/Dominance.lean @@ -203,3 +203,42 @@ axiom WfIRContext.Dom.opResult_not_dominatesIp_atStart! (opInBlock : op ∈ block.operationList ctx.raw ctx.wellFormed blockIn) {r : ValuePtr} (rResult : r ∈ op.getResults! ctx.raw) : ¬ r.dominatesIp (InsertPoint.atStart! block ctx.raw) ctx + +/-! +## Block-level dominance + +Dominance between blocks (the entry of `b₁` dominates every point of `b₂`). Used to discharge the +cross-block antisymmetry argument in the pattern-rewriter soundness proof: a value forwarded by a +rewrite cannot become an argument of a *different* block. +-/ + +variable {b₁ b₂ block bl : BlockPtr} + +/-- The dominance relation between two blocks: `b₁` dominates `b₂` when `b₁`'s entry dominates every +program point of `b₂`. A block dominates itself. -/ +axiom BlockPtr.dominates (b₁ b₂ : BlockPtr) (ctx : WfIRContext OpInfo) : Prop + +/-- Block dominance is antisymmetric: two blocks that dominate each other are equal. -/ +axiom BlockPtr.dominates_antisymm : + b₁.dominates b₂ ctx → b₂.dominates b₁ ctx → b₁ = b₂ + +/-- If a result `r` of an operation `op` living in `block` dominates the entry of a block `bl`, then +`block` dominates `bl` (the definition site of `r` is in `block`, and `r` reaches all of `bl`). -/ +axiom WfIRContext.Dom.block_dominates_of_opResult_dominatesIp_atStart! + (ctxDom : ctx.Dom) {op : OperationPtr} (opIn : op.InBounds ctx.raw) + (blockIn : block.InBounds ctx.raw) (blIn : bl.InBounds ctx.raw) + (opInBlock : op ∈ block.operationList ctx.raw ctx.wellFormed blockIn) + {r : ValuePtr} (rResult : r ∈ op.getResults! ctx.raw) + (rDom : r.dominatesIp (InsertPoint.atStart! bl ctx.raw) ctx) : + block.dominates bl ctx + +/-- If an argument `w` of a block `bl` dominates a program point inside `block` (the end of a list of +`block`'s operations, started from `block`'s entry), then `bl` dominates `block`: `w` is in scope +from `bl`'s entry, so `bl`'s entry dominates that point, hence all of `block`. -/ +axiom WfIRContext.Dom.block_dominates_of_arg_dominatesIp_afterLast + (ctxDom : ctx.Dom) (blIn : bl.InBounds ctx.raw) (blockIn : block.InBounds ctx.raw) + {w : ValuePtr} (wArg : w ∈ bl.getArguments! ctx.raw) + {ops : List OperationPtr} + (hops : ∀ o ∈ ops, o ∈ block.operationList ctx.raw ctx.wellFormed blockIn) + (wDom : w.dominatesIp (InsertPoint.afterLast ops ctx.raw (.atStart! block ctx.raw)) ctx) : + bl.dominates block ctx diff --git a/Veir/Interpreter/Refinement/Basic.lean b/Veir/Interpreter/Refinement/Basic.lean index c836036fa..cfc83ac58 100644 --- a/Veir/Interpreter/Refinement/Basic.lean +++ b/Veir/Interpreter/Refinement/Basic.lean @@ -176,30 +176,6 @@ structure ValueMapping.PreservesOperation {ctx ctx' : WfIRContext OpInfo} results : op'.getResults! ctx'.raw = mapping.applyToArray (op.getResults! ctx.raw) (by grind) reflect : mapping.ReflectsResults op op' -/-- -A variable state `state` is refined by `state'` through the value renaming `mapping`: every -variable defined in `state` is, after renaming through `mapping`, also defined in `state'` with a -value that refines the source value. --/ -def VariableState.isRefinedBy {ctx ctx' : WfIRContext OpInfo} - (state : VariableState ctx) (state' : VariableState ctx') - (mapping : ValueMapping ctx ctx') : Prop := - ∀ (val : ValuePtr) (valIn : val.InBounds ctx.raw), - ∀ sourceVar, state.getVar? val = some sourceVar → - ∃ targetVar, state'.getVar? (mapping ⟨val, valIn⟩) = some targetVar ∧ - sourceVar ⊒ targetVar - -/-- -An interpreter state `state` is refined by `state'` through the value mapping -`mapping`: they have the same memory, and the variable state of `state` is refined by the variable -state of `state'` through `mapping`. --/ -def InterpreterState.isRefinedBy {ctx ctx' : WfIRContext OpInfo} - (state : InterpreterState ctx) (state' : InterpreterState ctx') - (mapping : ValueMapping ctx ctx') : Prop := - state.memory = state'.memory ∧ - state.variables.isRefinedBy state'.variables mapping - /-- A *refinement point* selects which values a scoped refinement relation constrains. It is the position parameter of `isRefinedByAt`, richer than a bare `InsertPoint`: diff --git a/Veir/Interpreter/Refinement/Lemmas.lean b/Veir/Interpreter/Refinement/Lemmas.lean index 1541c01bf..3dd455825 100644 --- a/Veir/Interpreter/Refinement/Lemmas.lean +++ b/Veir/Interpreter/Refinement/Lemmas.lean @@ -24,18 +24,6 @@ theorem Interp.isRefinedBy_refl_of_ne_none {α : Type} {R : α → α → Prop} (hR : ∀ a, R a a) (x : Interp α) (neNone : x ≠ none) : Interp.isRefinedBy R x x := by rcases x with _ | (x | _) <;> grind [Interp.isRefinedBy] -@[simp, grind .] -theorem VariableState.isRefinedBy_refl - {ctx : WfIRContext OpInfo} {state : VariableState ctx} : - state.isRefinedBy state id := by - grind [VariableState.isRefinedBy] - -@[simp, grind .] -theorem InterpreterState.isRefinedBy_refl - {ctx : WfIRContext OpInfo} {state : InterpreterState ctx} : - state.isRefinedBy state id := by - grind [InterpreterState.isRefinedBy, VariableState.isRefinedBy] - /-- `isRefinedByAt` is reflexive under the identity mapping at any program point. -/ @[simp, grind .] theorem VariableState.isRefinedByAt_refl diff --git a/Veir/Interpreter/Refinement/Monotonicity.lean b/Veir/Interpreter/Refinement/Monotonicity.lean index b77fb76bf..34892f7f3 100644 --- a/Veir/Interpreter/Refinement/Monotonicity.lean +++ b/Veir/Interpreter/Refinement/Monotonicity.lean @@ -20,133 +20,6 @@ namespace Veir variable {OpInfo : Type} [HasOpInfo OpInfo] variable {ctx ctx' : WfIRContext OpInfo} -/-- `VariableState.getOperandValues` is monotone with respect to the state refinement relation: -refined variable states produce refined operand arrays. -/ -theorem VariableState.getOperandValues_isRefinedBy - {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} {mapping : ValueMapping ctx ctx'} - (hRef : srcVars.isRefinedBy tgtVars mapping) (opIn : op.InBounds ctx.raw) - (hOperands : op'.getOperands! ctx'.raw = mapping.applyToArray (op.getOperands! ctx.raw)) - (hSrc : srcVars.getOperandValues op = some srcVal) : - ∃ tgtVal, tgtVars.getOperandValues op' = some tgtVal ∧ srcVal ⊒ tgtVal := by - simp only [VariableState.isRefinedBy] at hRef - have ⟨hsize, hSrc⟩ := VariableState.getOperandValues_eq_some_iff.mp hSrc - have hSrc₂ := Array.mapM_option_isSome (f := tgtVars.getVar?) (l := op'.getOperands! ctx'.raw) - have ⟨r, hr⟩ := hSrc₂ (by grind [ValueMapping.applyToArray]) - simp only [getOperandValues, hr, Option.some.injEq, exists_eq_left'] - simp only [RuntimeValue.arrayIsRefinedBy] - constructor - · grind - · intro i hi - grind [Array.mapM_option_eq_some_implies hr i (by grind), ValueMapping.applyToArray] - -/-- `setResultValues?` preserves the state refinement. If the source/target variable states are -related by `mapping`, the freshly-computed result values refine (`resValues ⊒ resValues'`), `op` -and `op'` have the same results related by `mapping` (`hResults` and `hReflect`), then the target -`setResultValues?` also succeeds and the states after binding the results are again related by -`mapping`. -/ -theorem VariableState.setResultValues?_isRefinedBy - {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} - (hRef : srcVars.isRefinedBy tgtVars mapping) {newSrcVars : VariableState ctx} - {srcVals tgtVals : Array RuntimeValue} (hVals : srcVals ⊒ tgtVals) - (hResults : op'.getResults! ctx'.raw = mapping.applyToArray (op.getResults! ctx.raw)) - (hReflect : mapping.ReflectsResults op op') - (hSrc : srcVars.setResultValues? op srcVals opIn = some newSrcVars) - (tgtValsConforms : RuntimeValue.ArrayConforms tgtVals (op'.getResultTypes! ctx'.raw)) - (opIn' : op'.InBounds ctx'.raw) : - ∃ newTgtVars, tgtVars.setResultValues? op' tgtVals opIn' = some newTgtVars ∧ - newSrcVars.isRefinedBy newTgtVars mapping := by - /- Conformance of the (refined) target values implies target success. -/ - have ⟨newTgtVars, hTgt⟩ := - (VariableState.setResultValues?_isSome_iff_conforms - (varState := tgtVars) (inBounds := opIn')).mp tgtValsConforms - simp only [hTgt, Option.some.injEq, exists_eq_left'] - /- Reason per element in the source and result value arrays. -/ - intro val valIn sv hsv - /- Do a case analysis on whether or not the value is one of `op` results. - If it is not, the value is not a result of `op'`, and the refinement follows from the - original `VariableState` refinement. - If it is, then because of `hReflect`, the value is mapped to a result of `op'`, and the - refinement follows the `RuntimeValue` array refinement. -/ - cases OperationPtr.getResults!_not_mem_or_eq_getResult ctx.raw val op - next hNotMem => grind [VariableState.isRefinedBy] - next hMem => - have hfix := ValueMapping.applyToArray_getResults!_ext opIn hResults.symm - grind [RuntimeValue.arrayIsRefinedBy] - -/-- `setArgumentValues?` preserves the state refinement. If the source/target variable states are -related by `mapping`, the block argument values refine pointwise (`values ⊒ values'`), the -renaming `mapping` doesn't change the block arguments (`hArgs` and `hReflectArgs`), and the target -values conform to the target argument types (`tgtConforms`), then the target `setArgumentValues?` -also succeeds and the states after binding the block arguments are again related by `mapping`. -/ -theorem VariableState.setArgumentValues?_isRefinedBy {ctx ctx' : WfIRContext OpCode} - {srcVars : VariableState ctx} {tgtVars : VariableState ctx'} {mapping : ValueMapping ctx ctx'} - {block : BlockPtr} {values values' : Array RuntimeValue} {newSrcVars : VariableState ctx} - (hRef : srcVars.isRefinedBy tgtVars mapping) - (hVals : values ⊒ values') - (blockIn : block.InBounds ctx.raw) (blockIn' : block.InBounds ctx'.raw) - (hArgs : block.getArguments! ctx'.raw = mapping.applyToArray (block.getArguments! ctx.raw)) - (hReflectArgs : ∀ (val : ValuePtr) (valIn : val.InBounds ctx.raw) (arg : ValuePtr), - arg ∈ block.getArguments! ctx'.raw → - (mapping ⟨val, valIn⟩).val = arg → val = arg) - (tgtConforms : ∀ j, j < block.getNumArguments! ctx'.raw → - (values'[j]!).Conforms ((block.getArguments! ctx'.raw)[j]!.getType! ctx'.raw)) - (hSrc : srcVars.setArgumentValues? block values blockIn = some newSrcVars) : - ∃ newTgtVars, tgtVars.setArgumentValues? block values' blockIn' = some newTgtVars ∧ - newSrcVars.isRefinedBy newTgtVars mapping := by - -- `applyToArray` preserves size, so the two blocks have the same number of arguments; the renaming - -- fixes every block argument (`hArgs` is the pointwise "fixes" equation in array form). - have hNumArgs : block.getNumArguments! ctx'.raw = block.getNumArguments! ctx.raw := by - have := congrArg Array.size hArgs - simpa using this - have hFix : ∀ (val : ValuePtr) (valIn : val.InBounds ctx.raw), - val ∈ block.getArguments! ctx.raw → (mapping ⟨val, valIn⟩).val = val := by - intro val valIn hMem - obtain ⟨i, hi, rfl⟩ := BlockPtr.getArguments!.mem_iff_exists_index.mp hMem - exact ValueMapping.applyToArray_getArguments!_ext blockIn hArgs.symm i hi - -- Target success follows from conformance of the (refined) target values. - have tgtConforms' : ∀ j, j < block.getNumArguments! ctx'.raw → - (values'[j]!).Conforms ((block.getArgument j : ValuePtr).getType! ctx'.raw) := by - intro j hj - rw [← BlockPtr.getArguments!.getElem!_eq_getArgument hj] - exact tgtConforms j hj - have ⟨newTgtVars, hTgt⟩ := - (VariableState.setArgumentValues?_isSome_iff_conforms tgtVars - (block := block) (blockInBounds := blockIn')).mp tgtConforms' - refine ⟨newTgtVars, hTgt, ?_⟩ - -- Pointwise refinement of the value arrays at every argument index. Out-of-range indices read the - -- same `default` on both sides (the arrays have equal size by `hVals`), so refinement holds there too. - have hPt : ∀ i, i < block.getNumArguments! ctx.raw → values[i]! ⊒ values'[i]! := by - intro i _hi - obtain ⟨hsize, hpt⟩ := hVals - by_cases h : i < values.size - · exact hpt i h - · rw [getElem!_neg values i h, getElem!_neg values' i (hsize ▸ h)] - exact RuntimeValue.isRefinedBy_refl _ - intro val valIn sv hsv - by_cases hMem : val ∈ block.getArguments! ctx.raw - · -- `val` is a block argument `block.getArgument i`, set to `values[i]!`/`values'[i]!`. - obtain ⟨i, hi, rfl⟩ := BlockPtr.getArguments!.mem_iff_exists_index.mp hMem - have hsrcval := VariableState.getVar?_getArgument_of_setArgumentValues? hi hSrc - rw [hsv] at hsrcval - obtain rfl : sv = values[i]! := (Option.some.injEq ..).mp hsrcval - refine ⟨values'[i]!, ?_, hPt i hi⟩ - rw [hFix (block.getArgument i) valIn hMem] - exact VariableState.getVar?_getArgument_of_setArgumentValues? (hNumArgs ▸ hi) hTgt - · -- `val` is not a block argument: its value is unchanged on both sides; use `hRef`. - rw [VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! hMem hSrc] at hsv - obtain ⟨tv, htv, href⟩ := hRef val valIn sv hsv - refine ⟨tv, ?_, href⟩ - -- `mapping val` is not a block argument either (else `hReflectArgs` forces `val` to be one). - have hσnotMem : (mapping ⟨val, valIn⟩).val ∉ block.getArguments! ctx'.raw := by - intro hm - obtain ⟨i, hi, heq⟩ := BlockPtr.getArguments!.mem_iff_exists_index.mp hm - have harg : (block.getArgument i : ValuePtr) ∈ block.getArguments! ctx'.raw := - BlockPtr.getArguments!.mem_iff_exists_index.mpr ⟨i, hi, rfl⟩ - exact hMem (BlockPtr.getArguments!.mem_iff_exists_index.mpr - ⟨i, hNumArgs ▸ hi, (hReflectArgs val valIn _ harg heq.symm).symm⟩) - rw [VariableState.getVar?_setArgumentValues?_of_notMem_getArguments! hσnotMem hTgt] - exact htv - /-- `setArgumentValues?` preserves the *scoped* state refinement `isRefinedByAt`. The input relation `hRef` holds at the block entry `(atStart! block, atStart! block)`. On the @@ -154,9 +27,9 @@ pre-argument input state the block's own arguments are not yet defined, so `isRe entry constrains them only vacuously; it does constrain the non-argument values already in scope at the entry, which is exactly what the proof reuses for the surviving (non-argument) values. -Hypotheses compared to `setArgumentValues?_isRefinedBy`: -- `hRef` uses `isRefinedByAt` at the **incoming-edge** scope `.blockEntry block` (which excuses - `block`'s own arguments) instead of unscoped `isRefinedBy`. +Key hypotheses: +- `hRef` uses `isRefinedByAt` at the **incoming-edge** scope `.blockEntry block`, which excuses + `block`'s own arguments. - `hImageNotArg`: the mapping does not send a non-argument value that is *in scope at the block entry* onto a block-argument slot. (Justified by dominance: a forwarded block argument dominates the value it replaces, so it cannot also be dominated by it — hence no value dominating the block @@ -548,166 +421,4 @@ theorem interpretTerminatedOpList_monoAt exact ⟨hsRef, hcfRef⟩ · exact Interp.isRefinedBy_ub_target -/-- -`interpretOp` is monotone under a *cross-context* interpreter-state refinement. - -Lift `interpretOp'_monotone` through `getOperandValues` and `setResultValues?`. The source state -lives in `ctx`, the target in `ctx'`, related by `InterpreterState.isRefinedBy` through the value -renaming `mapping` (for the unchanged majority, `mapping` is the identity-on-`ValuePtr` `InBounds`-embedding). - -The conclusion relates the two `interpretOp` results: their interpreter states are again related by -`InterpreterState.isRefinedBy mapping`, and their control flow actions by `ControlFlowAction`-refinement. -Because the state relation constrains only values defined in *both* states, `op`'s freshly-set -results are added on both sides and re-established by `interpretOp'_monotone`, while pre-existing -values stay defined and refined. --/ -theorem interpretOp_monotone - {ctx ctx' : WfIRContext OpCode} - {state : InterpreterState ctx} {state' : InterpreterState ctx'} - {mapping : ValueMapping ctx ctx'} - (opIn : op.InBounds ctx.raw) (opIn' : op'.InBounds ctx'.raw) - (hState : state.isRefinedBy state' mapping) - (hPreserves : mapping.PreservesOperation op op') - (opVerif' : op'.Verified ctx' opIn') : - Interp.isRefinedBy - (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) - (r₂ : InterpreterState ctx' × Option ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 mapping ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) - (interpretOp op state opIn) - (interpretOp op' state' opIn') := by - -- If the source interpretation fails, then the refinement is trivial - by_cases hsource : interpretOp op state opIn = none; simp [hsource, Interp.isRefinedBy] - -- Source/target operands are defined, and memory is equal. - have ⟨operands, hSrcOps⟩ : ∃ operands, state.variables.getOperandValues op = some operands := by - grind [interpretOp] - obtain ⟨operands', hTgtOps, hOpsRef⟩ := - VariableState.getOperandValues_isRefinedBy hState.2 opIn hPreserves.operands hSrcOps - have hMem : state.memory = state'.memory := hState.1 - -- Add the refinement of `interpretOp'` on `op` with `operands` and `operands'` - have hPR1 := interpretOp'_monotone (op.getOpType! ctx.raw) - (op.getProperties! ctx.raw (op.getOpType! ctx.raw)) (op.getResultTypes! ctx.raw) - operands operands' (op.getSuccessors! ctx.raw) state.memory hOpsRef - -- Add the equality between `interpretOp'` on `operands'` - have hInterp'Eq : op'.interpret ctx'.raw operands' state'.memory = - op.interpret ctx.raw operands' state.memory := by - grind [interpretOp'_opType_cast, cases ValueMapping.PreservesOperation] - /- Do a case analysis on the source interpretation -/ - rcases hsrc : interpretOp op state opIn with _ | (⟨state₂, act⟩ | _) - · -- If the source interpretation fails, then the refinement is trivial - simp [Interp.isRefinedBy] - · /- If the source interpretation returns a new state, we need to prove that (1) the target - interpretation also returns a new state, and (2) the new states are in the refinement relation. -/ - simp only [Interp.isRefinedBy_ok_target_iff, Prod.exists] - have ⟨resValues, hinterp', hResValues⟩ := - (interpretOp_ok_iff_of_getOperandValues_eq_some hSrcOps).mp hsrc - simp only [hinterp', Interp.isRefinedBy_ok_target_iff, Prod.exists] at hPR1 - have ⟨resValues', memory'₂, act', hinterp'Tgt, resValuesRef, memoryEq, actRef⟩ := hPR1 - subst memory'₂ - simp only [← hInterp'Eq] at hinterp'Tgt - simp only [interpretOp, hTgtOps, bind, hinterp'Tgt, liftM, monadLift, MonadLift.monadLift] - have := interpretOp'_results_conform (opInBounds := opIn') opVerif' (VariableState.getOperandValues_conforms hTgtOps) hinterp'Tgt - have ⟨v, hv⟩ := (VariableState.setResultValues?_isSome_iff_conforms state'.variables opIn').mp this - simp only [Interp, hv, pure, Option.some.injEq, UBOr.ok.injEq, Prod.mk.injEq] - have stateVarRef : state.variables.isRefinedBy state'.variables mapping := by grind [InterpreterState.isRefinedBy] - grind [InterpreterState.isRefinedBy, VariableState.setResultValues?_isRefinedBy stateVarRef resValuesRef, cases ValueMapping.PreservesOperation] - · /- If the source interpretation returns UB, then the refinement holds trivially. -/ - simp - - -/-! -## Monotonicity of `interpretOpList` and `interpretTerminatedOpList` - -Lifts the per-operation monotonicity lemma `interpretOp_monotone` to lists of operations -(`interpretOpList` / `interpretTerminatedOpList`), under a *cross-context* interpreter-state -refinement over an *identical* list of operations modulus α-renaming -(`ValueMapping.PreservesOperation`) of operands and results. -/ - -/-- `interpretOpList` is monotone under a *cross-context* interpreter-state refinement, over an -*identical* slice of a block operation chain (the same `OperationPtr`s, whose intrinsic data agrees -modulo renaming `mapping`). -/ -theorem interpretOpList_mono - {ctx ctx' : WfIRContext OpCode} (hVerif : ctx'.Verified) - {ops : List OperationPtr} - (opsInBounds : ∀ op, op ∈ ops → op.InBounds ctx.raw) - (opsInBounds' : ∀ op, op ∈ ops → op.InBounds ctx'.raw) - {mapping : ValueMapping ctx ctx'} - {state : InterpreterState ctx} {state' : InterpreterState ctx'} - (hState : state.isRefinedBy state' mapping) - (hPreserves : ∀ op, (h : op ∈ ops) → mapping.PreservesOperation op op) : - Interp.isRefinedBy - (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) - (r₂ : InterpreterState ctx' × Option ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 mapping ∧ ControlFlowAction.optionIsRefinedBy r₁.2 r₂.2) - (interpretOpList ops state) (interpretOpList ops state') := by - induction ops generalizing state state' with - | nil => simpa [Interp] - | cons a l ih => - /- Refinement of the state after interpreting the head operation `a`. -/ - have refinesHead := interpretOp_monotone (opsInBounds a (by grind)) (opsInBounds' a (by grind)) - hState (hPreserves a (by grind)) (by grind) - simp only [interpretOpList_cons] - /- Case analysis on the interpretation of the head operation `a` in the source. -/ - rcases hsrc : interpretOp a state (opsInBounds a (by grind)) with _ | (⟨s, act⟩ | _) - · /- Source operation fails: interpreting the list returns `none`, refinement is trivial. -/ - simp [Interp.isRefinedBy] - · /- Source operation succeeds with new state `s` and action `act`. This means that the target - operation also succeeds with a refined state `s'` and action `act'`. -/ - simp only [hsrc, Interp.isRefinedBy_ok_target_iff] at refinesHead - obtain ⟨⟨s', act'⟩, htgt, hsRef, hactRef⟩ := refinesHead - simp only [htgt] - /- Case analysis on the action. -/ - cases act - case none => - /- No control-flow action: recurse on the tail, advancing the target invariant past `a`. - We use the induction to handle the tail. -/ - have hact' : act' = none := by grind [ControlFlowAction.optionIsRefinedBy] - subst hact' - simp only - apply ih (by grind) (by grind) hsRef (by grind) - case some cf => - simp [ControlFlowAction.optionIsRefinedBy] at hactRef - /- A control-flow action: the list stops here for both the source and the target. -/ - have ⟨cf', hact', hcfRef⟩ : ∃ cf', act' = some cf' ∧ cf.isRefinedBy cf' := by grind - subst hact' - simp [Interp, hsRef, ControlFlowAction.optionIsRefinedBy, hcfRef] - · /- Source operation is UB, which is refined by anything. -/ - simp - -/-- `interpretTerminatedOpList` is monotone under a *cross-context* interpreter-state refinement, -over an *identical* list of operations. The proof is derived from `interpretOpList_monotone`, as -`interpretTerminatedOpList` is a wrapper around `interpretOpList` that checks that the list of -operation has reached a terminator. -/ -theorem interpretTerminatedOpList_mono - {ctx ctx' : WfIRContext OpCode} (ctx'Verif : ctx'.Verified) - {state : InterpreterState ctx} {state' : InterpreterState ctx'} - {mapping : ValueMapping ctx ctx'} - (opsInBounds : ∀ op, op ∈ ops → op.InBounds ctx.raw) - (opsInBounds' : ∀ op, op ∈ ops → op.InBounds ctx'.raw) - (hState : state.isRefinedBy state' mapping) - (hFrame : ∀ op, (h : op ∈ ops) → mapping.PreservesOperation op op) : - Interp.isRefinedBy - (fun (r₁ : InterpreterState ctx × ControlFlowAction) - (r₂ : InterpreterState ctx' × ControlFlowAction) => - r₁.1.isRefinedBy r₂.1 mapping ∧ r₁.2.isRefinedBy r₂.2) - (interpretTerminatedOpList ops state) (interpretTerminatedOpList ops state') := by - have hList := interpretOpList_mono ctx'Verif opsInBounds opsInBounds' hState hFrame - simp only [interpretTerminatedOpList, bind] - rcases hsrc : interpretOpList ops state (by grind) with _ | (⟨s, act⟩ | _) - · simp [Interp.isRefinedBy] - · simp only [hsrc, Interp.isRefinedBy_ok_target_iff] at hList - obtain ⟨⟨s', act'⟩, htgt, hsRef, hactRef⟩ := hList - simp only [htgt] - /- Case analysis on the action returned by `interpretOpList`. If no action is returned at the - source, then the refinement is trivial (as interpretation failed in the input). If an action - is returned, then we derive refinement from the refinement of the action (given by - `interpretOpList_mono`). -/ - cases act with - | none => simp [Interp.isRefinedBy] - | some cf => - have ⟨cf', hact', hcfRef⟩ : ∃ cf', act' = some cf' ∧ cf.isRefinedBy cf' := by - cases act' <;> simp_all [ControlFlowAction.optionIsRefinedBy] - subst hact' - exact ⟨hsRef, hcfRef⟩ - · exact Interp.isRefinedBy_ub_target - end Veir diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 145928ef5..0340865a6 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -29,7 +29,7 @@ The clauses describe the edit as the composition *insert `newOps` before `op`* an identical operation list. 4. **intrinsic-data frame** — every surviving operation satisfies `CrossContextFrame` under `σ` (op-type/properties/result-types/successors agree; operands/results map through `σ`; `op` verified - in `newCtx`) — exactly what `interpretOp_monotone`/`interpretOpList_monotone` consume. + in `newCtx`) — exactly what `interpretOp_monotone_at`/`interpretOpList_monoAt` consume. 5. **structure frame** — blocks stay in bounds (successor transport), and parent operations of surviving operations are preserved (so `IsTopLevelFuncWithName` transports). 6. **result well-formedness** — `newCtx` is dominance-well-formed, and every member of `newOps` is @@ -152,18 +152,29 @@ structure RewrittenAt /-- `σ` fixes every value that is not a result of `op`. `LocalRewritePattern.mapping` is the identity except on `op`'s results (which it redirects to `newValues`), so every other value — in particular every block argument, which is never an `op` result — is left untouched. This is what - discharges the `hFix`/`hReflectArgs` frame hypotheses of `setArgumentValues?_isRefinedBy`. -/ + discharges the `hFix`/`hReflectArgs` frame hypotheses of `setArgumentValues?_isRefinedByAt`. -/ mappingFixesNonResults : ∀ (v : ValuePtr) (vIn : v.InBounds ctx.raw), v ∉ op.getResults! ctx.raw → ((rewriteMapping op newValues mapResultsInBounds mapNonResultsInBounds) ⟨v, vIn⟩).val = v - /-- Every produced value is an operation result, never a block argument (in the concrete driver they - are results of `newOps`). Together with `mappingFixesNonResults`, this lets `σ` reflect a block - argument only from that same block argument. -/ - newValuesAreResults : ∀ v ∈ newValues, ∃ opr : OpResultPtr, v = ValuePtr.opResult opr + /-- Every produced value dominates the post-insertion point in `block` — the `newCtx` analog of + "after `op`", i.e. the end of the inserted `newOps` span (`afterLast newOps (atStart! block)`). This + is the genuine SSA-validity condition on produced values, satisfied both by results of inserted + `newOps` (defined within the span) and by forwarded pre-existing values (e.g. a block argument), + which are in scope throughout the block. It replaces the old `newValuesAreResults`, admitting + block-argument forwarding (`x + 0 → x`, where `x` is a block argument). -/ + newValuesDominate : ∀ v ∈ newValues, + v.dominatesIp (InsertPoint.afterLast newOps.toList newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) newCtx /-- The block-argument list of every block is preserved by the rewrite (it only edits operations). Gives equal argument counts and argument types across the two contexts. -/ blockArgsPreserved : ∀ (bl : BlockPtr), bl.InBounds ctx.raw → (bl.get! newCtx.raw).arguments = (bl.get! ctx.raw).arguments + /-- Block-level dominance is preserved by the rewrite: it only edits the operation list inside + `block` (replacing `[op]` by `newOps`), leaving the CFG (block successors / terminators / region + entries) unchanged, so the dominance relation between any two in-bounds blocks agrees across the two + contexts. Used to bridge the cross-context antisymmetry argument of `mappingImageNotArg`. -/ + blockDominatesPreserved : ∀ (b₁ b₂ : BlockPtr), b₁.InBounds ctx.raw → b₂.InBounds ctx.raw → + (b₁.dominates b₂ newCtx ↔ b₁.dominates b₂ ctx) -- Clause 8: region/block structure frame (the rewrite edits only operation lists). /-- Regions stay in bounds — region-`InBounds` transport (mirrors `blocksInBounds`). -/ regionsInBounds : ∀ (r : RegionPtr), r.InBounds ctx.raw → r.InBounds newCtx.raw @@ -385,7 +396,7 @@ theorem blockArg_notMem_getResults simp [BlockPtr.getArgument, OperationPtr.getResult_def] at heq /-- `σ` fixes block-argument pointers: it maps `bl.getArgument i` to itself. Discharges the `hFix` -hypothesis of `setArgumentValues?_isRefinedBy`. -/ +hypothesis of `setArgumentValues?_isRefinedByAt`. -/ theorem mappingFixesArg (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') {bl : BlockPtr} {i : Nat} (vIn : (bl.getArgument i : ValuePtr).InBounds ctx.raw) : @@ -394,7 +405,7 @@ theorem mappingFixesArg /-- `σ` fixes a block's argument array: applying it to `bl`'s source arguments yields the same arguments in the target context. Discharges the `hArgs` hypothesis of -`setArgumentValues?_isRefinedBy`. -/ +`setArgumentValues?_isRefinedByAt`. -/ theorem argsApplyToArray (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) : @@ -429,43 +440,50 @@ theorem getArguments!_eq bl.getArguments! newCtx.raw = bl.getArguments! ctx.raw := by simp only [BlockPtr.getArguments!, BlockPtr.getNumArguments!, h.blockArgsPreserved bl blIn] -/-- `σ` never maps a value onto one of `bl`'s block arguments unless it already is that block -argument: a value not in `bl`'s arguments is either fixed by `σ` (so stays out of the arguments) or a -result of `op` (so maps into `newValues`, which are operation results, never block arguments). -Discharges the `hImageNotArg` hypothesis of `setArgumentValues?_isRefinedByAt`. -/ +/-- `σ` never maps an in-scope value onto one of `bl`'s block arguments unless it already is that +block argument: a value not in `bl`'s arguments is either fixed by `σ` (so stays out of the +arguments), or a result `r` of `op` and then a cross-block antisymmetry argument applies. If `bl` is +`block` itself, `r` does not dominate `block`'s entry (SSA), contradicting `hdom`. If `bl ≠ block`, +then the image `σ r` (which dominates the post-insertion point inside `block`) being a `bl`-argument +would force `bl` to dominate `block`, while `hdom` forces `block` to dominate `bl`; antisymmetry gives +`bl = block`, a contradiction. Discharges the `hImageNotArg` hypothesis of +`setArgumentValues?_isRefinedByAt`. -/ theorem mappingImageNotArg (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (ctxDom : ctx.Dom) {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) {val : ValuePtr} (valIn : val.InBounds ctx.raw) - (hNotArg : val ∉ bl.getArguments! ctx.raw) : + (hNotArg : val ∉ bl.getArguments! ctx.raw) + (hdom : val.dominatesIp (InsertPoint.atStart! bl ctx.raw) ctx) : (h.σ ⟨val, valIn⟩).val ∉ bl.getArguments! newCtx.raw := by - rw [h.getArguments!_eq blIn] by_cases hMem : val ∈ op.getResults! ctx.raw - · have hmem := h.mapping_getResult_mem_newValues valIn hMem - obtain ⟨opr, hopr⟩ := h.newValuesAreResults _ hmem - rw [hopr] - simp only [BlockPtr.getArguments!.mem_iff_exists_index, not_exists, not_and] - intro i _ heq - simp [BlockPtr.getArgument] at heq - · rw [h.mappingFixesNonResults val valIn hMem] + · -- `val` is a result `r` of `op`, which lives in `block`. + have opInBlock : op ∈ block.operationList ctx.raw ctx.wellFormed blockIn := by + rw [h.srcList]; simp + by_cases hbl : bl = block + · -- `bl = block`: `r` cannot dominate `block`'s own entry (SSA), contradicting `hdom`. + subst hbl + exact absurd hdom (ctxDom.opResult_not_dominatesIp_atStart! opIn blockIn opInBlock hMem) + · -- `bl ≠ block`: cross-block antisymmetry rules out `σ r ∈ bl.args`. + intro hσMem + have hImgMem := h.mapping_getResult_mem_newValues valIn hMem + have hσDom := h.newValuesDominate _ hImgMem + have hops : ∀ o ∈ newOps.toList, + o ∈ block.operationList newCtx.raw newCtx.wellFormed blockIn' := by + intro o ho + rw [Array.mem_toList_iff] at ho + rw [h.tgtList] + exact Array.mem_append.mpr (Or.inl (Array.mem_append.mpr (Or.inr ho))) + have hBlDomBlock : bl.dominates block newCtx := + WfIRContext.Dom.block_dominates_of_arg_dominatesIp_afterLast h.newCtxDom + (h.blocksInBounds bl blIn) blockIn' hσMem hops hσDom + have hBlockDomBl : block.dominates bl newCtx := + (h.blockDominatesPreserved block bl blockIn blIn).mpr + (WfIRContext.Dom.block_dominates_of_opResult_dominatesIp_atStart! ctxDom opIn blockIn blIn + opInBlock hMem hdom) + exact hbl (BlockPtr.dominates_antisymm hBlDomBlock hBlockDomBl) + · rw [h.getArguments!_eq blIn, h.mappingFixesNonResults val valIn hMem] exact hNotArg -/-- `σ` reflects block-argument pointers: the only value it maps onto `bl.getArgument i` is -`bl.getArgument i` itself (results map into `newValues`, which are never block arguments). Discharges -the `hReflectArgs` hypothesis of `setArgumentValues?_isRefinedBy`. -/ -theorem mappingReflectsArg - (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') - {bl : BlockPtr} (val : ValuePtr) (valIn : val.InBounds ctx.raw) (i : Nat) - (heq : (h.σ ⟨val, valIn⟩).val = (bl.getArgument i : ValuePtr)) : - val = (bl.getArgument i : ValuePtr) := by - by_cases hMem : val ∈ op.getResults! ctx.raw - · exfalso - have hmem := h.mapping_getResult_mem_newValues valIn hMem - rw [heq] at hmem - obtain ⟨opr, hopr⟩ := h.newValuesAreResults _ hmem - simp [BlockPtr.getArgument] at hopr - · rw [h.mappingFixesNonResults val valIn hMem] at heq - exact heq - /-- Every new operation is verified in the target context. Derived from `newCtxVerif`: the target context is verified, so every in-bounds operation (in particular every `newOp`) satisfies its local invariants. -/ @@ -502,14 +520,14 @@ the rewrite renaming `σ`. The proof (below) splits the list with `interpretTerminatedOpList_append` and dispatches on the source outcome at the matched operation `op`: -* **`pre` (identical list)** — cross-context monotonicity `interpretOpList_monotone` (PR 3), fed the +* **`pre` (identical list)** — cross-context monotonicity `interpretOpList_monoAt` (PR 3), fed the `CrossContextFrame`s of clause 4; this also threads `EquationLemmaAt`/`DefinesDominating` to `op`. * **`[op]` vs `newOps`** — the local op-step simulation `hOpSim`, which PR 8 discharges from `PreservesSemantics` (bridging its create-only context to the inserted/erased `newCtx` via clause 4). -* **`post` (same pointers, redirected operands)** — `interpretOpList_mono` again, now under `σ`, +* **`post` (same pointers, redirected operands)** — `interpretOpList_monoAt` again, now under `σ`, seeded by the post-`op` state from the previous regime. * **source `.ub` at `op`** — a source `ub` refines any target outcome, so no target-progress argument - is needed: cross-context monotonicity (`interpretOpList_mono`) discharges this regime directly. + is needed: cross-context monotonicity (`interpretOpList_monoAt`) discharges this regime directly. The `hOpSim` hypothesis is the local op→`newOps` simulation, stated so PR 8 can supply it: refined, SSA-valid entry states map a source `interpretOp op` step onto a target `interpretOpList newOps` run, @@ -876,7 +894,7 @@ theorem isRefinedBy_interpretOpList_seqCompose obtain ⟨cf', ha', hcf⟩ : ∃ cf', a' = some cf' ∧ cf.isRefinedBy cf' := by cases a' <;> simp_all [ControlFlowAction.optionIsRefinedBy] subst ha' - simp only [interpretOpList_nil, Interp.isRefinedBy_ok_target_iff] + simp only [Interp.isRefinedBy_ok_target_iff] refine ⟨_, rfl, ?_, by simpa [ControlFlowAction.optionIsRefinedBy] using hcf⟩ -- The result point `afterLast (l₁ ++ []) = afterLast l₁`, where `hsRef` already lands. simpa using hsRef @@ -1480,7 +1498,7 @@ theorem RewrittenAt.interpretBlock_refinement (hRW.numArgsEq bIn ▸ hj)) obtain ⟨newVars', hsa', hpsRefVar⟩ := VariableState.setArgumentValues?_isRefinedByAt bIn bIn' hState.2 hVals (hRW.argsApplyToArray bIn) - (fun val valIn hNotArg _hdom => hRW.mappingImageNotArg bIn valIn hNotArg) + (fun val valIn hNotArg hdom => hRW.mappingImageNotArg hCtxDom bIn valIn hNotArg hdom) tgtConforms hsa have hpsRef : (InterpreterState.mk newVars state.memory).isRefinedByAt ⟨newVars', state'.memory⟩ hRW.σ @@ -2037,17 +2055,6 @@ same memory); the source-entry SSA invariant on it is supplied by the caller (PR driver), exactly as in Stage D. -/ -/-- The fresh, empty interpreter state (no variables, memory `mem`) in the source context is refined -by the fresh empty state in the target context under any renaming `σ`: they define no variables (so -the variable-refinement obligation is vacuous) and share the same memory. -/ -theorem InterpreterState.empty_isRefinedBy {ctx ctx' : WfIRContext OpCode} - (μ : ValueMapping ctx ctx') (mem : MemoryState) : - (InterpreterState.mk (VariableState.empty ctx) mem).isRefinedBy - (InterpreterState.mk (VariableState.empty ctx') mem) μ := by - refine ⟨rfl, ?_⟩ - intro val valIn sourceVar hget - simp [VariableState.getVar?, VariableState.empty] at hget - /-- The fresh, empty interpreter state satisfies the scoped relation at any pair of refinement points: it defines no variables, so the constraint is vacuous (and the memories coincide). -/ theorem InterpreterState.empty_isRefinedByAt {ctx ctx' : WfIRContext OpCode} @@ -2061,7 +2068,7 @@ theorem InterpreterState.empty_isRefinedByAt {ctx ctx' : WfIRContext OpCode} /-- Lift a `σ`-refinement of two region runs to a `FunctionResult` refinement of the corresponding function runs: `interpretFunction` post-processes `interpretRegion` by keeping only the final memory -and the returned values, and `InterpreterState.isRefinedBy` already entails equal memories, so the +and the returned values, and `InterpreterState.isRefinedByAt` already entails equal memories, so the refinement is preserved by that projection. -/ theorem Interp.isRefinedBy_functionResult_of_region {ctx ctx' : WfIRContext OpCode} {a : Interp (InterpreterState ctx × Array RuntimeValue)} @@ -2639,11 +2646,15 @@ theorem RewrittenAt.of_fromLocalRewrite -- `σ` (`rewriteMapping`) is the identity off `op`'s results: it takes the `else` branch. mappingFixesNonResults := fun v vIn hv => by simp only [rewriteMapping, dif_neg hv] - -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS. The four `Return*` props do not force `newValues` to be - -- operation results (a pattern could return a block argument), so this needs a pattern obligation. - newValuesAreResults := by sorry + -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS. Produced values must dominate the post-insertion point in + -- `block` (the SSA-validity condition: results of `newOps` are defined within the span, forwarded + -- values are in scope throughout the block); discharged from a pattern obligation. + newValuesDominate := by sorry -- TODO(PR 9, keystone): operation-list edits leave block-argument lists untouched. blockArgsPreserved := by sorry + -- TODO(PR 9, keystone): op-list edits inside `block` leave the CFG unchanged, so block-level + -- dominance agrees across the two contexts. + blockDominatesPreserved := by sorry -- Regions stay in bounds: into `newCtxPat`, then the folds/erase (erase removes only `op`). regionsInBounds := fun r hr => hSurviveRegion r (hCreated.inBounds_mono (GenericPtr.region r) (by grind)) From 514697879ea197319c594034a3711919ae2451e4 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Mon, 29 Jun 2026 05:07:48 +0200 Subject: [PATCH 15/31] A few less fields --- Veir/PatternRewriter/Soundness.lean | 92 ++++++++++++++++------------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 0340865a6..1ec31e7c5 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -89,13 +89,30 @@ theorem rewriteMapping_applyToArray_getResults {ctx newCtx : WfIRContext OpCode} grind simp [hidx, getElem!_pos, h2] +/-- The in-bounds witness for the redirect branch of `rewriteMapping`: each result index of `op` +selects a value of `newValues` that is in bounds of the target context. Derived from `newValuesSize` +(the index is within `newValues`) and `newValuesInBounds` (every member is in bounds). This is the +witness `rewriteMapping` expects; previously it was carried as a `RewrittenAt` field. -/ +theorem rewriteMapping_resultsInBounds {ctx newCtx : WfIRContext OpCode} + (op : OperationPtr) (newValues : Array ValuePtr) + (newValuesSize : newValues.size = op.getNumResults! ctx.raw) + (newValuesInBounds : ∀ v ∈ newValues, v.InBounds newCtx.raw) : + ∀ (index : Nat), index < (op.getResults! ctx.raw).size → + newValues[index]!.InBounds newCtx.raw := by + intro index hidx + have hsize : index < newValues.size := by + rw [newValuesSize, ← OperationPtr.getResults!.size_eq_getNumResults!]; exact hidx + rw [getElem!_pos newValues index hsize] + exact newValuesInBounds _ (Array.getElem_mem hsize) + /-- `RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn'` asserts that `newCtx` is obtained from `ctx` by the single local rewrite that replaces `op` with `newOps` (producing `newValues`). The renaming identifying surviving values across the two contexts is *not* a parameter: it is the function `RewrittenAt.σ` of the instance, defined as `rewriteMapping op newValues -h.mapResultsInBounds h.mapNonResultsInBounds` (the in-bounds witnesses `mapResultsInBounds`/ -`mapNonResultsInBounds` are fields below). `block` is the block containing `op`, `pre`/`post` the +h.mapResultsInBounds h.mapNonResultsInBounds` (the redirect-branch witness `RewrittenAt.mapResultsInBounds` +is derived from `newValuesSize`/`newValuesInBounds`; the identity-branch witness `mapNonResultsInBounds` +is a field below). `block` is the block containing `op`, `pre`/`post` the operation lists before/after `op` in `block`, and `blockIn`/`blockIn'` the in-bounds witnesses for `block` in the source/target contexts. See the module docstring for the clauses. -/ @@ -118,11 +135,6 @@ structure RewrittenAt newValuesInBounds : ∀ v ∈ newValues, v.InBounds newCtx.raw /-- `newOps` are exactly the operations fresh to the target context. -/ newOpsFresh : ∀ newOp, newOp ∈ newOps ↔ (newOp.InBounds newCtx.raw ∧ ¬ newOp.InBounds ctx.raw) - /-- In-bounds witness for the redirect branch of `σ` (`rewriteMapping`): each produced value is in - bounds of the target context. (Derivable from `newValuesSize`/`newValuesInBounds`, but kept as a - field so `σ := RewrittenAt.σ` is total without re-deriving it.) -/ - mapResultsInBounds : ∀ (index : Nat), index < (op.getResults! ctx.raw).size → - newValues[index]!.InBounds newCtx.raw /-- In-bounds witness for the identity branch of `σ` (`rewriteMapping`): every value that is not a result of `op` survives into the target context. -/ mapNonResultsInBounds : ∀ (v : ValuePtr), v.InBounds ctx.raw → @@ -130,32 +142,27 @@ structure RewrittenAt -- Clause 2: `op` erased, others survive. /-- The matched operation is erased. -/ opErased : ¬ op.InBounds newCtx.raw - /-- Every operation other than `op` survives into the target context. -/ survives : ∀ (o : OperationPtr), o.InBounds ctx.raw → o ≠ op → o.InBounds newCtx.raw -- Clause 4: intrinsic-data frame for survivors. /-- Every surviving operation carries identical intrinsic data, modulo the renaming `σ`. -/ frame : ∀ (o : OperationPtr) (oIn : o.InBounds ctx.raw) (oIn' : o.InBounds newCtx.raw), o ≠ op → - (rewriteMapping op newValues mapResultsInBounds mapNonResultsInBounds).PreservesOperation + (rewriteMapping op newValues + (rewriteMapping_resultsInBounds op newValues newValuesSize newValuesInBounds) + mapNonResultsInBounds).PreservesOperation o o oIn oIn' -- Clause 5: structure frame. /-- Blocks stay in bounds — successor-`InBounds` transport. -/ blocksInBounds : ∀ (b : BlockPtr), b.InBounds ctx.raw → b.InBounds newCtx.raw - /-- Parent operations of surviving operations are preserved. -/ - parentOps : ∀ (o : OperationPtr), o.InBounds ctx.raw → o ≠ op → - o.getParentOp! newCtx.raw = o.getParentOp! ctx.raw + blockArgsPreserved : ∀ (bl : BlockPtr), bl.InBounds ctx.raw → + (bl.get! newCtx.raw).arguments = (bl.get! ctx.raw).arguments + blockDominatesPreserved : ∀ (b₁ b₂ : BlockPtr), b₁.InBounds ctx.raw → b₂.InBounds ctx.raw → + (b₁.dominates b₂ newCtx ↔ b₁.dominates b₂ ctx) -- Clause 6: result well-formedness. /-- The target context is dominance-well-formed. -/ newCtxDom : newCtx.Dom newCtxVerif : newCtx.Verified -- Clause 7: value-renaming frame for block arguments (PR 9 / `LocalRewritePattern.mapping`). - /-- `σ` fixes every value that is not a result of `op`. `LocalRewritePattern.mapping` is the - identity except on `op`'s results (which it redirects to `newValues`), so every other value — in - particular every block argument, which is never an `op` result — is left untouched. This is what - discharges the `hFix`/`hReflectArgs` frame hypotheses of `setArgumentValues?_isRefinedByAt`. -/ - mappingFixesNonResults : ∀ (v : ValuePtr) (vIn : v.InBounds ctx.raw), - v ∉ op.getResults! ctx.raw → - ((rewriteMapping op newValues mapResultsInBounds mapNonResultsInBounds) ⟨v, vIn⟩).val = v /-- Every produced value dominates the post-insertion point in `block` — the `newCtx` analog of "after `op`", i.e. the end of the inserted `newOps` span (`afterLast newOps (atStart! block)`). This is the genuine SSA-validity condition on produced values, satisfied both by results of inserted @@ -165,19 +172,7 @@ structure RewrittenAt newValuesDominate : ∀ v ∈ newValues, v.dominatesIp (InsertPoint.afterLast newOps.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) newCtx - /-- The block-argument list of every block is preserved by the rewrite (it only edits operations). - Gives equal argument counts and argument types across the two contexts. -/ - blockArgsPreserved : ∀ (bl : BlockPtr), bl.InBounds ctx.raw → - (bl.get! newCtx.raw).arguments = (bl.get! ctx.raw).arguments - /-- Block-level dominance is preserved by the rewrite: it only edits the operation list inside - `block` (replacing `[op]` by `newOps`), leaving the CFG (block successors / terminators / region - entries) unchanged, so the dominance relation between any two in-bounds blocks agrees across the two - contexts. Used to bridge the cross-context antisymmetry argument of `mappingImageNotArg`. -/ - blockDominatesPreserved : ∀ (b₁ b₂ : BlockPtr), b₁.InBounds ctx.raw → b₂.InBounds ctx.raw → - (b₁.dominates b₂ newCtx ↔ b₁.dominates b₂ ctx) -- Clause 8: region/block structure frame (the rewrite edits only operation lists). - /-- Regions stay in bounds — region-`InBounds` transport (mirrors `blocksInBounds`). -/ - regionsInBounds : ∀ (r : RegionPtr), r.InBounds ctx.raw → r.InBounds newCtx.raw /-- The region list of every surviving operation is preserved: the rewrite only edits the operation list of `block`, never region structure. Gives equal region counts and equal region pointers across the two contexts (used to relate `interpretFunction`/`interpretRegion`). -/ @@ -205,6 +200,16 @@ variable {ctx newCtx : WfIRContext OpCode} {op : OperationPtr} namespace RewrittenAt +/-- In-bounds witness for the redirect branch of `σ` (`rewriteMapping`): each produced value is in +bounds of the target context. Derived from `newValuesSize`/`newValuesInBounds` (it used to be a field). +Exposed under the `RewrittenAt` namespace so the `rewriteMapping` callsites can keep writing +`h.mapResultsInBounds`. -/ +theorem mapResultsInBounds + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : + ∀ (index : Nat), index < (op.getResults! ctx.raw).size → + newValues[index]!.InBounds newCtx.raw := + rewriteMapping_resultsInBounds op newValues h.newValuesSize h.newValuesInBounds + /-- The fixed renaming performed by the rewrite: `rewriteMapping` applied to the in-bounds witnesses carried by the `RewrittenAt` instance. This is *not* a parameter of `RewrittenAt`; it is a function of the instance (the lemmas below refer to it as `h.σ`). -/ @@ -212,6 +217,20 @@ abbrev σ (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post bl ValueMapping ctx newCtx := rewriteMapping op newValues h.mapResultsInBounds h.mapNonResultsInBounds +/-- `σ` fixes every value that is not a result of `op`. `LocalRewritePattern.mapping` is the identity +except on `op`'s results (which it redirects to `newValues`), so every other value — in particular +every block argument, which is never an `op` result — is left untouched: `rewriteMapping` takes its +`else` branch. This used to be a field; it is purely a fact about `rewriteMapping`, independent of the +other `RewrittenAt` data. It discharges the `hFix`/`hReflectArgs` frame hypotheses of +`setArgumentValues?_isRefinedByAt`. -/ +theorem mappingFixesNonResults + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (v : ValuePtr) (vIn : v.InBounds ctx.raw) (hv : v ∉ op.getResults! ctx.raw) : + ((rewriteMapping op newValues + (rewriteMapping_resultsInBounds op newValues h.newValuesSize h.newValuesInBounds) + h.mapNonResultsInBounds) ⟨v, vIn⟩).val = v := by + simp only [rewriteMapping, dif_neg hv] + /-- `op` lives in `block`: derived from `srcList`, since `op` occurs in `block`'s operation list. -/ theorem opParent (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : (op.get! ctx.raw).parent = some block := @@ -2605,9 +2624,6 @@ theorem RewrittenAt.of_fromLocalRewrite · rintro ⟨h1, h2⟩ have hne : newOp ≠ op := by rintro rfl; exact h2 opInBounds exact hfresh.mpr ⟨(hOpBnd newOp hne).mp h1, h2⟩ - -- TODO(PR 9): as `newValuesInBounds` (indexed form via `ReturnValues` size). NEEDS EXTRA - -- HYPOTHESIS: `newValues` must not reference `op`'s results (so they survive `eraseOp`). - mapResultsInBounds := by sorry -- A value that is not a result of `op` survives: its owner (if any) is `≠ op`, so it is not one -- of the pointers `eraseOp` removes. mapNonResultsInBounds := by @@ -2634,8 +2650,6 @@ theorem RewrittenAt.of_fromLocalRewrite -- Blocks stay in bounds: into `newCtxPat`, then the folds/erase (erase removes only `op`). blocksInBounds := fun b hb => hSurviveBlock b (hCreated.inBounds_mono (GenericPtr.block b) (by grind)) - -- TODO(PR 9, keystone): parent ops of survivors preserved (op-list edits don't move other ops). - parentOps := by sorry -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS. Dominance/verification are not preserved by arbitrary -- `insertOp`s, so this requires source `rewriter.ctx.Dom` plus a pattern obligation that the rewrite -- produces a dominance-well-formed result (mirroring `PreservesSemantics`'s `ctxDom`). @@ -2643,9 +2657,6 @@ theorem RewrittenAt.of_fromLocalRewrite -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS, as `newCtxDom` (source `rewriter.ctx.Verified` + pattern -- obligation that the result is verified). newCtxVerif := by sorry - -- `σ` (`rewriteMapping`) is the identity off `op`'s results: it takes the `else` branch. - mappingFixesNonResults := fun v vIn hv => by - simp only [rewriteMapping, dif_neg hv] -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS. Produced values must dominate the post-insertion point in -- `block` (the SSA-validity condition: results of `newOps` are defined within the span, forwarded -- values are in scope throughout the block); discharged from a pattern obligation. @@ -2655,9 +2666,6 @@ theorem RewrittenAt.of_fromLocalRewrite -- TODO(PR 9, keystone): op-list edits inside `block` leave the CFG unchanged, so block-level -- dominance agrees across the two contexts. blockDominatesPreserved := by sorry - -- Regions stay in bounds: into `newCtxPat`, then the folds/erase (erase removes only `op`). - regionsInBounds := fun r hr => - hSurviveRegion r (hCreated.inBounds_mono (GenericPtr.region r) (by grind)) -- TODO(PR 9, keystone): op-list edits leave survivors' region lists untouched. opRegionsPreserved := by sorry -- TODO(PR 9, keystone): op-list edits leave region entry blocks untouched. From 5073f775326ba70a97caa16ae771333a5bd93547 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Mon, 29 Jun 2026 05:59:40 +0200 Subject: [PATCH 16/31] Remove two sorries from lifting lemma --- Veir/PatternRewriter/Semantics.lean | 16 + Veir/PatternRewriter/Soundness.lean | 845 +++++++++++++++++++++++++++- 2 files changed, 848 insertions(+), 13 deletions(-) diff --git a/Veir/PatternRewriter/Semantics.lean b/Veir/PatternRewriter/Semantics.lean index 176f9637f..090de246d 100644 --- a/Veir/PatternRewriter/Semantics.lean +++ b/Veir/PatternRewriter/Semantics.lean @@ -88,6 +88,22 @@ def LocalRewritePattern.ReturnValuesInBounds (pattern : LocalRewritePattern OpCo ∀ ctx op newCtx newOps newValues, pattern ctx op = some (newCtx, some (newOps, newValues)) → ∀ v ∈ newValues, v.InBounds newCtx.raw +/-- +No value returned by the pattern is a result of a *pre-existing* (source-context) operation: a +returned value is either a result of one of the freshly created `newOps`, or a pre-existing non-result +value (e.g. a block argument). It may never be a result of an operation already in `ctx`. + +This rules out three problems with the driver's "redirect `op`'s results to `newValues`, then erase +`op`" pipeline: (a) a `newValue` equal to a result of `op` would dangle once `op` is erased; (b) it +would make the sequential redirect fold diverge from the parallel value renaming `σ`; and (c) a +`newValue` equal to a result of *any* surviving operation `o` would let `σ` map `op`'s result onto +`o`'s result, breaking the `ReflectsResults o o` frame clause. It admits block-argument forwarding +(`x + 0 → x` with `x` a block argument), which is the forwarding case the framework supports. +-/ +def LocalRewritePattern.ReturnValuesNotSourceResults (pattern : LocalRewritePattern OpCode) : Prop := + ∀ ctx op newCtx newOps newValues, pattern ctx op = some (newCtx, some (newOps, newValues)) → + ∀ v ∈ newValues, ∀ orp : OpResultPtr, v = ValuePtr.opResult orp → ¬ orp.op.InBounds ctx.raw + /-- Indexed access on the returned values is in bounds of the new context. Discharges the second `sorry` in `LocalRewritePattern.Mapping`. diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 1ec31e7c5..e79fc09b6 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -2486,6 +2486,627 @@ theorem PatternRewriter.eraseOp_ctx_eq {b b' : PatternRewriter OpCode} {op : Ope subst h rfl +/-! ### Keystone helpers: how the driver's insert/replace/erase folds reshape block op-lists. + +These discharge the `tgtList`/`otherBlocks` fields of `RewrittenAt.of_fromLocalRewrite`. The insert +fold rewrites `block`'s list `pre ++ [op] ++ post` into `pre ++ newOps ++ [op] ++ post`, the replace +fold leaves every list untouched, and the final `eraseOp op` drops `op`, giving `pre ++ newOps ++ post`. +Every other block's list is untouched by all three stages. -/ + +/-- A `List.insertIdx` at the boundary index splices the new element just before the pivot. -/ +private theorem List.insertIdx_mid {α} (pre l₂ : List α) (op a : α) : + (pre ++ [op] ++ l₂).insertIdx pre.length a = pre ++ [a] ++ [op] ++ l₂ := by + induction pre with + | nil => simp [List.insertIdx] + | cons hd tl ih => + simp only [List.cons_append, List.length_cons, List.insertIdx_succ_cons] + simp only [List.append_assoc, List.cons_append, List.nil_append] at ih ⊢ + rw [ih] + +/-- Array version of `List.insertIdx_mid`. -/ +private theorem Array.insertIdx_mid {α} (pre post : Array α) (op a : α) + (hle : pre.size ≤ (pre ++ #[op] ++ post).size) : + (pre ++ #[op] ++ post).insertIdx pre.size a hle = pre ++ #[a] ++ #[op] ++ post := by + apply Array.toList_inj.mp + simp only [Array.toList_insertIdx, Array.toList_append, List.append_assoc] + have := List.insertIdx_mid pre.toList post.toList op a + simp only [List.append_assoc] at this + simpa using this + +/-- The index of the pivot in `pre ++ [op] ++ post` is `pre.size` when `op ∉ pre`. -/ +private theorem Array.idxOf_mid {α} [BEq α] [LawfulBEq α] (pre post : Array α) (op : α) + (h : op ∉ pre) : (pre ++ #[op] ++ post).idxOf op = pre.size := by + rw [show pre ++ #[op] ++ post = pre ++ (#[op] ++ post) by simp] + rw [Array.idxOf_append, Array.idxOf_append]; simp [h] + +/-- Erasing the unique pivot from `pre ++ mid ++ [op] ++ post` removes exactly that occurrence. -/ +private theorem Array.erase_mid {α} [BEq α] [LawfulBEq α] (pre mid post : Array α) (op : α) + (h1 : op ∉ pre) (h2 : op ∉ mid) : + (pre ++ mid ++ #[op] ++ post).erase op = pre ++ mid ++ post := by + apply Array.toList_inj.mp + have hm : op ∉ (pre ++ mid) := by simp only [Array.mem_append]; exact fun h => h.elim h1 h2 + simp only [Array.toList_erase, Array.toList_append, Array.append_assoc] + rw [show pre.toList ++ (mid.toList ++ ([op] ++ post.toList)) + = (pre.toList ++ mid.toList) ++ ([op] ++ post.toList) by simp] + rw [List.erase_append_right _ (by simpa using hm)] + simp [List.erase_cons_head] + +/-- `operationList` only depends on the underlying context, so equal contexts give equal lists. -/ +theorem BlockPtr.operationList_congr {c₁ c₂ : WfIRContext OpInfo} (h : c₁ = c₂) {block : BlockPtr} + (b1 : block.InBounds c₁.raw) (b2 : block.InBounds c₂.raw) : + block.operationList c₁.raw c₁.wellFormed b1 = block.operationList c₂.raw c₂.wellFormed b2 := by + subst h; rfl + +/-- `WfRewriter.createOp` with no insertion point leaves every block's operation list unchanged. -/ +theorem BlockPtr.operationList_WfRewriter_createOp_none {ctx newCtx : WfIRContext OpInfo} + {opType : OpInfo} {resultTypes operands blockOperands regions properties} + {h₁ h₂ h₃ h₄} {newOp : OperationPtr} {block : BlockPtr} + (h : WfRewriter.createOp ctx opType resultTypes operands blockOperands regions properties + none h₁ h₂ h₃ h₄ = some (newCtx, newOp)) + (blockIn : block.InBounds ctx.raw) (blockIn' : block.InBounds newCtx.raw) : + block.operationList newCtx.raw newCtx.wellFormed blockIn' = + block.operationList ctx.raw ctx.wellFormed blockIn := by + simp only [WfRewriter.createOp] at h + split at h + · exact absurd h (by simp) + · rename_i c op' hc + simp only [Option.pure_def, Option.some.injEq, Prod.mk.injEq] at h + obtain ⟨rfl, -⟩ := h + simpa using BlockPtr.operationList_rewriter_createOp hc ctx.wellFormed + +/-- A `WithCreatedOps` chain (the pattern only creates detached operations) leaves every block's +operation list unchanged. -/ +theorem WfIRContext.WithCreatedOps.operationList_eq {ctx₁ ctx₂ : WfIRContext OpInfo} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {block : BlockPtr} + (blockIn₁ : block.InBounds ctx₁.raw) : + ∀ (blockIn₂ : block.InBounds ctx₂.raw), + block.operationList ctx₂.raw ctx₂.wellFormed blockIn₂ = + block.operationList ctx₁.raw ctx₁.wellFormed blockIn₁ := by + induction h with + | Nil ctx => intro blockIn₂; rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + intro blockIn₃ + obtain ⟨opType, resultTypes, operands, successors, regions, properties, k₁, k₂, k₃, k₄, hcreate⟩ := hex + have hb₂ : block.InBounds ctx₂.raw := by + have := hwco.inBounds_mono (GenericPtr.block block) (by grind); grind + rw [BlockPtr.operationList_WfRewriter_createOp_none hcreate hb₂ blockIn₃] + exact ih blockIn₁ hb₂ + +/-- The block operation list after a `WfRewriter.insertOp`: the new op is spliced into the insertion +point's block, every other block is untouched. -/ +theorem BlockPtr.operationList_WfRewriter_insertOp {ctx ctx' : WfIRContext OpInfo} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {block : BlockPtr} + (h : WfRewriter.insertOp ctx newOp ip h1 h2 = some ctx') + (blockIn : block.InBounds ctx.raw) (blockIn' : block.InBounds ctx'.raw) : + block.operationList ctx'.raw ctx'.wellFormed blockIn' = + if hb : ip.block! ctx.raw = some block then + (block.operationList ctx.raw ctx.wellFormed blockIn).insertIdx + (ip.idxIn ctx.raw block) newOp (by apply InsertPoint.idxIn.le_size_operationList) + else block.operationList ctx.raw ctx.wellFormed blockIn := by + simp only [WfRewriter.insertOp] at h + split at h + · exact absurd h (by simp) + · rename_i c hc + simp only [Option.pure_def, Option.some.injEq] at h + obtain rfl := h + exact BlockPtr.operationList_rewriter_insertOp hc ctx.wellFormed + +/-- `PatternRewriter.insertOp` lift of `operationList_WfRewriter_insertOp`. -/ +theorem PatternRewriter.insertOp_operationList {b b' : PatternRewriter OpInfo} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {block : BlockPtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') + (blockIn : block.InBounds b.ctx.raw) (blockIn' : block.InBounds b'.ctx.raw) : + block.operationList b'.ctx.raw b'.ctx.wellFormed blockIn' = + if hb : ip.block! b.ctx.raw = some block then + (block.operationList b.ctx.raw b.ctx.wellFormed blockIn).insertIdx + (ip.idxIn b.ctx.raw block) newOp (by apply InsertPoint.idxIn.le_size_operationList) + else block.operationList b.ctx.raw b.ctx.wellFormed blockIn := by + unfold PatternRewriter.insertOp at h + split at h + · exact absurd h (by simp) + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact BlockPtr.operationList_WfRewriter_insertOp hwf blockIn blockIn' + +/-- `PatternRewriter.insertOp` preserves the parent of every operation other than the inserted one. -/ +theorem PatternRewriter.insertOp_op_parent {b b' : PatternRewriter OpInfo} + {newOp op : OperationPtr} {ip : InsertPoint} {h1 h2} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') (hne : op ≠ newOp) : + (op.get! b'.ctx.raw).parent = (op.get! b.ctx.raw).parent := by + unfold PatternRewriter.insertOp at h + split at h + · exact absurd h (by simp) + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + have := OperationPtr.parent!_wfRewriter_insertOp (operation := op) hwf + simpa [hne] using this + +/-- `PatternRewriter.insertOp` preserves all `InBounds` facts. -/ +theorem PatternRewriter.insertOp_genericPtr_inBounds {b b' : PatternRewriter OpInfo} + {newOp : OperationPtr} {ptr : GenericPtr} {ip : InsertPoint} {h1 h2} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + ptr.InBounds b'.ctx.raw ↔ ptr.InBounds b.ctx.raw := by + unfold PatternRewriter.insertOp at h + split at h + · exact absurd h (by simp) + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact WfRewriter.insertOp_inBounds_iff hwf + +/-- `WfRewriter.replaceValue` only redirects operands, leaving every block's operation list intact. -/ +theorem BlockPtr.operationList_WfRewriter_replaceValue {ctx : WfIRContext OpInfo} + {oldValue newValue : ValuePtr} {ne : oldValue ≠ newValue} + {oldIn : oldValue.InBounds ctx.raw} {newIn : newValue.InBounds ctx.raw} + {block : BlockPtr} (blockIn : block.InBounds ctx.raw) + (blockIn' : block.InBounds (WfRewriter.replaceValue ctx oldValue newValue ne oldIn newIn).raw) : + block.operationList (WfRewriter.replaceValue ctx oldValue newValue ne oldIn newIn).raw + (WfRewriter.replaceValue ctx oldValue newValue ne oldIn newIn).wellFormed blockIn' = + block.operationList ctx.raw ctx.wellFormed blockIn := by + have hchain : BlockPtr.OpChain block + (WfRewriter.replaceValue ctx oldValue newValue ne oldIn newIn).raw + (block.operationList ctx.raw ctx.wellFormed blockIn) := by + apply BlockPtr.OpChain_unchanged + (BlockPtr.operationListWF ctx.raw block blockIn ctx.wellFormed) blockIn' + · grind + · grind + · intro opPtr hop hpar; refine ⟨?_, ?_, ?_, ?_⟩ <;> grind + · intro opPtr hop hpar; refine ⟨?_, ?_⟩ <;> grind + grind + +/-- `PatternRewriter.replaceValue` lift of `operationList_WfRewriter_replaceValue`. -/ +theorem PatternRewriter.replaceValue_operationList {b : PatternRewriter OpInfo} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {block : BlockPtr} + (blockIn : block.InBounds b.ctx.raw) + (blockIn' : block.InBounds (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw) : + block.operationList (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw + (b.replaceValue oldVal newVal ne oldIn newIn).ctx.wellFormed blockIn' = + block.operationList b.ctx.raw b.ctx.wellFormed blockIn := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + revert blockIn' + rw [hctx] + intro blockIn' + exact BlockPtr.operationList_WfRewriter_replaceValue blockIn _ + +/-- `PatternRewriter.replaceValue` preserves all `InBounds` facts. -/ +theorem PatternRewriter.replaceValue_genericPtr_inBounds {b : PatternRewriter OpInfo} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {ptr : GenericPtr} : + ptr.InBounds (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw ↔ ptr.InBounds b.ctx.raw := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx]; exact WfRewriter.replaceValue_inBounds + +/-- `PatternRewriter.replaceValue` preserves block in-boundedness (the `BlockPtr`-headed form, so it +unifies against goals where the replace proofs are opaque). -/ +theorem PatternRewriter.replaceValue_blockPtr_inBounds {b : PatternRewriter OpInfo} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {block : BlockPtr} : + block.InBounds (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw ↔ block.InBounds b.ctx.raw := by + have := PatternRewriter.replaceValue_genericPtr_inBounds (b := b) (oldVal := oldVal) + (newVal := newVal) (ne := ne) (oldIn := oldIn) (newIn := newIn) (ptr := GenericPtr.block block) + grind + +/-- The block operation list after a `WfRewriter.eraseOp`: the erased op is dropped from its parent +block, every other block is untouched. -/ +theorem BlockPtr.operationList_WfRewriter_eraseOp {ctx : WfIRContext OpInfo} {op : OperationPtr} + {hr hu ho} {block : BlockPtr} + (blockIn : block.InBounds ctx.raw) + (blockIn' : block.InBounds (WfRewriter.eraseOp ctx op hr hu ho).raw) : + block.operationList (WfRewriter.eraseOp ctx op hr hu ho).raw + (WfRewriter.eraseOp ctx op hr hu ho).wellFormed blockIn' + = if (op.get! ctx.raw).parent = block then + (block.operationList ctx.raw ctx.wellFormed blockIn).erase op + else block.operationList ctx.raw ctx.wellFormed blockIn := by + simp only [WfRewriter.eraseOp] + exact BlockPtr.operationList_rewriter_eraseOp ctx.wellFormed + +/-- Folding `insertOp · (before op)` over a list of fresh ops splices them, in order, just before +`op` inside `op`'s block, leaving `op`'s membership/parent intact. -/ +theorem PatternRewriter.foldlM_insertOp_before_opList + {op : OperationPtr} {block : BlockPtr} + {f : PatternRewriter OpInfo → OperationPtr → Option (PatternRewriter OpInfo)} + (hf : ∀ b a b', f b a = some b' → + ∃ (k1 : a.InBounds b.ctx.raw) (k2 : (InsertPoint.before op).InBounds b.ctx.raw), + PatternRewriter.insertOp b a (InsertPoint.before op) k1 k2 = some b') : + ∀ {l : List OperationPtr} {init s : PatternRewriter OpInfo} {pre post : Array OperationPtr}, + op.InBounds init.ctx.raw → + List.foldlM f init l = some s → + (op.get! init.ctx.raw).parent = some block → + (∀ (hb : block.InBounds init.ctx.raw), + block.operationList init.ctx.raw init.ctx.wellFormed hb = pre ++ #[op] ++ post) → + op ∉ pre → op ∉ post → op ∉ l → + op.InBounds s.ctx.raw ∧ + (op.get! s.ctx.raw).parent = some block ∧ + (∀ (hb : block.InBounds s.ctx.raw), + block.operationList s.ctx.raw s.ctx.wellFormed hb = pre ++ l.toArray ++ #[op] ++ post) := by + intro l + induction l with + | nil => + intro init s pre post hinit hfold hpar hlist _ _ _ + simp only [List.foldlM_nil, Option.pure_def, Option.some.injEq] at hfold + subst hfold + exact ⟨hinit, hpar, by intro hb; simpa using hlist hb⟩ + | cons a t ih => + intro init s pre post hinit hfold hpar hlist hpre hpost hnotmem + rw [List.foldlM_cons] at hfold + obtain ⟨b, hfa, htail⟩ := Option.bind_eq_some_iff.mp hfold + obtain ⟨k1, k2, hins⟩ := hf init a b hfa + have hblockInit : block.InBounds init.ctx.raw := by + have := init.ctx.wellFormed.inBounds; grind + have hane : op ≠ a := by intro h; subst h; exact hnotmem (by simp) + have hopB : op.InBounds b.ctx.raw := by + have := PatternRewriter.insertOp_genericPtr_inBounds (ptr := GenericPtr.operation op) hins + grind + have hparB : (op.get! b.ctx.raw).parent = some block := by + rw [PatternRewriter.insertOp_op_parent hins hane]; exact hpar + have hipblock : (InsertPoint.before op).block! init.ctx.raw = some block := by + rw [InsertPoint.block!_before_eq]; exact hpar + have hlistB : ∀ (hb : block.InBounds b.ctx.raw), + block.operationList b.ctx.raw b.ctx.wellFormed hb = (pre ++ #[a]) ++ #[op] ++ post := by + intro hb + rw [PatternRewriter.insertOp_operationList hins hblockInit hb, dif_pos hipblock] + simp only [InsertPoint.idxIn_before_eq, hlist hblockInit, Array.idxOf_mid pre post op hpre] + exact Array.insertIdx_mid pre post op a _ + have hpre' : op ∉ pre ++ #[a] := by + simp only [Array.mem_append, Array.mem_singleton] + exact fun h => h.elim hpre (fun h => hane h) + have hnotmemt : op ∉ t := fun h => hnotmem (List.mem_cons_of_mem a h) + obtain ⟨hs, hsp, hsl⟩ := ih hopB htail hparB hlistB hpre' hpost hnotmemt + refine ⟨hs, hsp, ?_⟩ + intro hb + rw [hsl hb, show (a :: t).toArray = #[a] ++ t.toArray from List.toArray_cons a t] + simp only [Array.append_assoc] + +/-- A fold that preserves `c`'s operation list (and `c`'s in-boundedness) at every step preserves it +overall. -/ +theorem PatternRewriter.foldlM_preserves_opList {α} {c : BlockPtr} + {f : PatternRewriter OpInfo → α → Option (PatternRewriter OpInfo)} + (hstep : ∀ b a b', f b a = some b' → + (c.InBounds b.ctx.raw → c.InBounds b'.ctx.raw) ∧ + (∀ (hc : c.InBounds b.ctx.raw) (hc' : c.InBounds b'.ctx.raw), + c.operationList b'.ctx.raw b'.ctx.wellFormed hc' + = c.operationList b.ctx.raw b.ctx.wellFormed hc)) : + ∀ {l : List α} {init s : PatternRewriter OpInfo}, + List.foldlM f init l = some s → + ∀ (hc : c.InBounds init.ctx.raw) (hc' : c.InBounds s.ctx.raw), + c.operationList s.ctx.raw s.ctx.wellFormed hc' + = c.operationList init.ctx.raw init.ctx.wellFormed hc := by + intro l + induction l with + | nil => + intro init s hfold hc hc' + simp only [List.foldlM_nil, Option.pure_def, Option.some.injEq] at hfold + subst hfold; rfl + | cons a t ih => + intro init s hfold hc hc' + rw [List.foldlM_cons] at hfold + obtain ⟨b, hfa, htail⟩ := Option.bind_eq_some_iff.mp hfold + obtain ⟨hinb, hop⟩ := hstep init a b hfa + have hcb : c.InBounds b.ctx.raw := hinb hc + rw [ih htail hcb hc', hop hc hcb] + +/-- Folding `insertOp · (before op)` leaves every block other than `op`'s parent untouched. -/ +theorem PatternRewriter.foldlM_insertOp_before_other + {op : OperationPtr} {block c : BlockPtr} (hcb : c ≠ block) + {f : PatternRewriter OpInfo → OperationPtr → Option (PatternRewriter OpInfo)} + (hf : ∀ b a b', f b a = some b' → + ∃ (k1 : a.InBounds b.ctx.raw) (k2 : (InsertPoint.before op).InBounds b.ctx.raw), + PatternRewriter.insertOp b a (InsertPoint.before op) k1 k2 = some b') : + ∀ {l : List OperationPtr} {init s : PatternRewriter OpInfo}, + op.InBounds init.ctx.raw → + (op.get! init.ctx.raw).parent = some block → + List.foldlM f init l = some s → + op ∉ l → + ∀ (hc : c.InBounds init.ctx.raw) (hc' : c.InBounds s.ctx.raw), + c.operationList s.ctx.raw s.ctx.wellFormed hc' + = c.operationList init.ctx.raw init.ctx.wellFormed hc := by + intro l + induction l with + | nil => + intro init s hinit hpar hfold _ hc hc' + simp only [List.foldlM_nil, Option.pure_def, Option.some.injEq] at hfold + subst hfold; rfl + | cons a t ih => + intro init s hinit hpar hfold hnotmem hc hc' + rw [List.foldlM_cons] at hfold + obtain ⟨b, hfa, htail⟩ := Option.bind_eq_some_iff.mp hfold + obtain ⟨k1, k2, hins⟩ := hf init a b hfa + have hane : op ≠ a := by intro h; subst h; exact hnotmem (by simp) + have hcInB : c.InBounds b.ctx.raw := by + have := PatternRewriter.insertOp_genericPtr_inBounds (ptr := GenericPtr.block c) hins + grind + have hopB : op.InBounds b.ctx.raw := by + have := PatternRewriter.insertOp_genericPtr_inBounds (ptr := GenericPtr.operation op) hins + grind + have hparB : (op.get! b.ctx.raw).parent = some block := by + rw [PatternRewriter.insertOp_op_parent hins hane]; exact hpar + have hstep : c.operationList b.ctx.raw b.ctx.wellFormed hcInB + = c.operationList init.ctx.raw init.ctx.wellFormed hc := by + rw [PatternRewriter.insertOp_operationList hins hc hcInB, dif_neg ?_] + rw [InsertPoint.block!_before_eq, hpar] + simp only [Option.some.injEq] + exact fun h => hcb h.symm + rw [ih hopB hparB htail (fun h => hnotmem (List.mem_cons_of_mem a h)) hcInB hc', hstep] + +/-! ### Keystone helpers: how the driver's pipeline frames a *survivor's* intrinsic data. + +These discharge the `frame` field of `RewrittenAt.of_fromLocalRewrite`. For an operation `o ≠ op` the +driver leaves its op type, properties, result count, successors and result types untouched at every +stage (created ops, insert fold, replace fold, erase); only its operands are rewritten, and exactly by +the result→`newValues` redirect, which equals the value renaming `σ`. -/ + +/-- `createEmptyOp` leaves a pre-existing operation's properties (at every op code) untouched: it only +`set`s the fresh `newOp`'s record. The shipped `getProperties!_createEmptyOp` is code-specific. -/ +theorem OperationPtr.getProperties!_createEmptyOp_ne {ctx ctx' : IRContext OpCode} + {opType : OpCode} {properties : HasOpInfo.propertiesOf opType} {newOp operation : OperationPtr} + {oc : OpCode} + (h : Rewriter.createEmptyOp ctx opType properties = some (ctx', newOp)) + (hne : operation ≠ newOp) : + operation.getProperties! ctx' oc = operation.getProperties! ctx oc := by + simp only [Rewriter.createEmptyOp, OperationPtr.allocEmpty] at h + grind [OperationPtr.getProperties!, OperationPtr.set, OperationPtr.get!] + +/-- A `WfRewriter.createOp` leaves a pre-existing operation's properties (at every op code) untouched: +only the fresh `newOp` gets properties, and the init steps touch only results/regions/operands. The +code-specific `getProperties!_WfRewriter_createOp` covers only the created op's own type. -/ +theorem OperationPtr.getProperties!_WfRewriter_createOp_ne {ctx ctx' : WfIRContext OpCode} + {opType : OpCode} {resultTypes operands blockOperands regions properties h₁ h₂ h₃ h₄} + {newOp operation : OperationPtr} {oc : OpCode} + (h : WfRewriter.createOp ctx opType resultTypes operands blockOperands regions properties + none h₁ h₂ h₃ h₄ = some (ctx', newOp)) + (hne : operation ≠ newOp) : + operation.getProperties! ctx'.raw oc = operation.getProperties! ctx.raw oc := by + simp only [WfRewriter.createOp] at h + grind [Rewriter.createOp, OperationPtr.getProperties!_createEmptyOp_ne, + OperationPtr.getProperties!_initOpRegions] + +/-- Intrinsic operation data the rewrite driver leaves untouched for a *surviving* operation `o`: its +op type, properties (at every op code), result count, successors and result types. Operands are +deliberately excluded — the redirect fold rewrites them. Packaged as a single `Prop` so the driver's +folds can thread it through `Array.foldlM_option_invariant` in one shot. -/ +def OperationPtr.SameIntrinsic (o : OperationPtr) (c c' : IRContext OpCode) : Prop := + o.getOpType! c' = o.getOpType! c ∧ + (∀ ot, o.getProperties! c' ot = o.getProperties! c ot) ∧ + o.getNumResults! c' = o.getNumResults! c ∧ + o.getSuccessors! c' = o.getSuccessors! c ∧ + o.getResultTypes! c' = o.getResultTypes! c + +@[refl] +theorem OperationPtr.SameIntrinsic.rfl {o : OperationPtr} {c : IRContext OpCode} : + o.SameIntrinsic c c := ⟨_root_.rfl, fun _ => _root_.rfl, _root_.rfl, _root_.rfl, _root_.rfl⟩ + +theorem OperationPtr.SameIntrinsic.symm {o : OperationPtr} {c c' : IRContext OpCode} + (h : o.SameIntrinsic c c') : o.SameIntrinsic c' c := + ⟨h.1.symm, fun ot => (h.2.1 ot).symm, h.2.2.1.symm, h.2.2.2.1.symm, h.2.2.2.2.symm⟩ + +theorem OperationPtr.SameIntrinsic.trans {o : OperationPtr} {c c' c'' : IRContext OpCode} + (h : o.SameIntrinsic c c') (h' : o.SameIntrinsic c' c'') : o.SameIntrinsic c c'' := + ⟨h'.1.trans h.1, fun ot => (h'.2.1 ot).trans (h.2.1 ot), h'.2.2.1.trans h.2.2.1, + h'.2.2.2.1.trans h.2.2.2.1, h'.2.2.2.2.trans h.2.2.2.2⟩ + +/-- `PatternRewriter.insertOp` frames a survivor's intrinsic data (it only links a fresh op). -/ +theorem PatternRewriter.insertOp_sameIntrinsic {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {o : OperationPtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + o.SameIntrinsic b.ctx.raw b'.ctx.raw := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact ⟨OperationPtr.getOpType!_wfRewriter_insertOp hwf, + fun _ => OperationPtr.getProperties!_wfRewriter_insertOp hwf, + OperationPtr.getNumResults!_wfRewriter_insertOp hwf, + OperationPtr.getSuccessors!_wfRewriter_insertOp hwf, + OperationPtr.getResultTypes!_wfRewriter_insertOp hwf⟩ + +/-- `PatternRewriter.insertOp` frames a survivor's operands. -/ +theorem PatternRewriter.insertOp_getOperands {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {o : OperationPtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + o.getOperands! b'.ctx.raw = o.getOperands! b.ctx.raw := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact OperationPtr.getOperands!_wfRewriter_insertOp hwf + +/-- `PatternRewriter.replaceValue` frames every operation's intrinsic data (it only redirects +operands). -/ +theorem PatternRewriter.replaceValue_sameIntrinsic {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {o : OperationPtr} : + o.SameIntrinsic b.ctx.raw (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx] + exact ⟨OperationPtr.getOpType!_WfRewriter_replaceValue, + fun _ => OperationPtr.getProperties!_WfRewriter_replaceValue, + OperationPtr.getNumResults!_WfRewriter_replaceValue, + OperationPtr.getSuccessors!_WfRewriter_replaceValue, + OperationPtr.getResultTypes!_WfRewriter_replaceValue⟩ + +/-- `PatternRewriter.replaceValue` rewrites a survivor's operands by the single-value redirect. -/ +theorem PatternRewriter.replaceValue_getOperands {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {o : OperationPtr} (hin : o.InBounds b.ctx.raw) : + o.getOperands! (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw = + (o.getOperands! b.ctx.raw).map (fun v => if v = oldVal then newVal else v) := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx] + exact OperationPtr.getOperands!_WfRewriter_replaceValue hin + +/-- A `WithCreatedOps` chain frames a survivor's intrinsic data (it only creates fresh ops). -/ +theorem WfIRContext.WithCreatedOps.sameIntrinsic {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {o : OperationPtr} (oIn : o.InBounds ctx₁.raw) : + o.SameIntrinsic ctx₁.raw ctx₂.raw := by + induction h with + | Nil => rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + obtain ⟨opType, rt, ops, succ, regs, props, k₁, k₂, k₃, k₄, hcreate⟩ := hex + have ho2 : o.InBounds ctx₂.raw := by + have := hwco.inBounds_mono (GenericPtr.operation o) (by grind); grind + have hstep : o.SameIntrinsic ctx₂.raw ctx₃.raw := by + refine ⟨by grind, fun ot => ?_, by grind, by grind, by grind⟩ + exact OperationPtr.getProperties!_WfRewriter_createOp_ne hcreate (by grind) + exact (ih oIn).trans hstep + +/-- A `WithCreatedOps` chain frames a survivor's operands (it only creates fresh ops). -/ +theorem WfIRContext.WithCreatedOps.getOperands_eq {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {o : OperationPtr} (oIn : o.InBounds ctx₁.raw) : + o.getOperands! ctx₂.raw = o.getOperands! ctx₁.raw := by + induction h with + | Nil => rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + obtain ⟨opType, rt, ops, succ, regs, props, k₁, k₂, k₃, k₄, hcreate⟩ := hex + have ho2 : o.InBounds ctx₂.raw := by + have := hwco.inBounds_mono (GenericPtr.operation o) (by grind); grind + rw [OperationPtr.getOperands!_WfRewriter_createOp hcreate, if_neg (by grind)] + exact ih oIn + +/-- Fuse a left-fold of array `map`s into one `map` of left-folds. -/ +theorem List.foldl_arrayMap_fusion {α β : Type} (l : List β) (g : β → α → α) (arr : Array α) : + l.foldl (fun a b => a.map (fun x => g b x)) arr + = arr.map (fun x => l.foldl (fun acc b => g b acc) x) := by + induction l generalizing arr with + | nil => simp + | cons b t ih => simp only [List.foldl_cons, ih, Array.map_map, Function.comp_def] + +/-- The result→`newValues` redirect fold, applied to a value that is *not* one of `op`'s results, is +the identity: no step's `oldVal` (an `op` result) ever matches. -/ +theorem fold_replaceResult_eq_self (op : OperationPtr) : + ∀ (l : List (ValuePtr × Nat)) (v : ValuePtr), + (∀ q ∈ l, v ≠ (op.getResult q.2 : ValuePtr)) → + l.foldl (fun acc q => if acc = (op.getResult q.2 : ValuePtr) then q.1 else acc) v = v := by + intro l + induction l with + | nil => intro v _; rfl + | cons q t ih => + intro v h + rw [List.foldl_cons, if_neg (h q (by simp))] + exact ih v (fun q' hq' => h q' (by simp [hq'])) + +/-- The redirect fold over `newValues.zipIdx`, applied to `op`'s `(start+k)`-th result, lands on +`newValues[k]`: every earlier step targets a different result so leaves it; the matching step sends it +to `newValues[k]`; later steps cannot touch `newValues[k]`, which is not an `op` result. -/ +theorem fold_replaceResult_zipIdx_hit (op : OperationPtr) : + ∀ (vs : List ValuePtr) (start k : Nat) (hk : k < vs.length), + (∀ x ∈ vs, ∀ m, x ≠ (op.getResult m : ValuePtr)) → + (vs.zipIdx start).foldl + (fun acc q => if acc = (op.getResult q.2 : ValuePtr) then q.1 else acc) + (op.getResult (start + k) : ValuePtr) = vs[k] := by + intro vs + induction vs with + | nil => intro start k hk _; simp at hk + | cons a t ih => + intro start k hk hrepl + rw [List.zipIdx_cons, List.foldl_cons] + match k with + | 0 => + simp only [Nat.add_zero, List.getElem_cons_zero] + exact fold_replaceResult_eq_self op (t.zipIdx (start + 1)) a + (fun q hq => hrepl a (by simp) q.2) + | k' + 1 => + have hne : (op.getResult (start + (k' + 1)) : ValuePtr) ≠ (op.getResult start : ValuePtr) := by + simp only [OperationPtr.getResult, ne_eq, ValuePtr.opResult.injEq, + OpResultPtr.mk.injEq, true_and] + omega + rw [if_neg hne, show start + (k' + 1) = (start + 1) + k' by omega, + List.getElem_cons_succ] + exact ih (start + 1) k' (by simpa using hk) (fun x hx m => hrepl x (by simp [hx]) m) + +/-- The driver's redirect fold (`foldlM` of `replaceValue (op.getResult i) newValues[i]` over +`newValues.zipIdx`) rewrites a survivor's operand array by mapping each operand through the +single-result redirect, composed left-to-right. -/ +theorem PatternRewriter.foldlM_replaceValue_getOperands {op o : OperationPtr} + {f : PatternRewriter OpCode → (ValuePtr × Nat) → Option (PatternRewriter OpCode)} + (hf : ∀ b q b', f b q = some b' → + ∃ ne oldIn newIn, b.replaceValue (op.getResult q.2 : ValuePtr) q.1 ne oldIn newIn = b') : + ∀ {l : List (ValuePtr × Nat)} {init s : PatternRewriter OpCode}, + List.foldlM f init l = some s → o.InBounds init.ctx.raw → + o.getOperands! s.ctx.raw = + l.foldl (fun arr q => arr.map (fun v => if v = (op.getResult q.2 : ValuePtr) then q.1 else v)) + (o.getOperands! init.ctx.raw) := by + intro l + induction l with + | nil => + intro init s hfold _ + simp only [List.foldlM_nil, Option.pure_def, Option.some.injEq] at hfold + subst hfold; rfl + | cons q t ih => + intro init s hfold hin + rw [List.foldlM_cons] at hfold + obtain ⟨b, hfb, htail⟩ := Option.bind_eq_some_iff.mp hfold + obtain ⟨ne, oldIn, newIn, hb⟩ := hf init q b hfb + have hinb : o.InBounds b.ctx.raw := by + rw [← hb] + have h := PatternRewriter.replaceValue_genericPtr_inBounds (b := init) + (oldVal := (op.getResult q.2 : ValuePtr)) (newVal := q.1) (ne := ne) (oldIn := oldIn) + (newIn := newIn) (ptr := GenericPtr.operation o) + grind + have hstep : o.getOperands! b.ctx.raw + = (o.getOperands! init.ctx.raw).map + (fun v => if v = (op.getResult q.2 : ValuePtr) then q.1 else v) := by + rw [← hb]; exact PatternRewriter.replaceValue_getOperands hin + rw [List.foldl_cons, ih htail hinb, hstep] + +/-- The driver's redirect fold over `newValues.zipIdx` realizes the value renaming `σ` pointwise: a +value that is one of `op`'s results `i` goes to `newValues[i]`, anything else is fixed. Requires that +no `newValue` is itself an `op` result (`hNoAlias`) so the sequential fold cannot chain redirects. -/ +theorem fold_replaceResult_zipIdx_eq_sigma {ctx : WfIRContext OpCode} + (op : OperationPtr) (newValues : Array ValuePtr) + (hsize : newValues.size = op.getNumResults! ctx.raw) + (hNoAlias : ∀ x ∈ newValues, ∀ m, x ≠ (op.getResult m : ValuePtr)) + (v : ValuePtr) : + (newValues.zipIdx.toList).foldl + (fun acc q => if acc = (op.getResult q.2 : ValuePtr) then q.1 else acc) v + = if v ∈ op.getResults! ctx.raw + then newValues[(op.getResults! ctx.raw).idxOf v]! else v := by + rw [Array.toList_zipIdx] + by_cases hv : v ∈ op.getResults! ctx.raw + · rw [if_pos hv] + obtain ⟨j, hj, hvj⟩ := OperationPtr.getResults!.mem_iff_exists_index.mp hv + have hidx : (op.getResults! ctx.raw).idxOf v = j := by + have h1 : (op.getResult ((op.getResults! ctx.raw).idxOf v) : ValuePtr) = v := + OperationPtr.getResult_eq_of_idxOf_getResults! hv rfl + have := h1.trans hvj.symm + simp only [OperationPtr.getResult, ValuePtr.opResult.injEq, OpResultPtr.mk.injEq, + true_and] at this + exact this + have hjsize : j < newValues.toList.length := by + rw [Array.length_toList]; omega + have key := fold_replaceResult_zipIdx_hit op newValues.toList 0 j hjsize + (by simpa [Array.mem_toList_iff] using hNoAlias) + rw [Nat.zero_add] at key + rw [hidx, show v = (op.getResult j : ValuePtr) from hvj.symm, key] + rw [Array.getElem_toList] + exact (getElem!_pos newValues j (by omega)).symm + · rw [if_neg hv] + apply fold_replaceResult_eq_self + intro q hq hcontra + apply hv + rw [hcontra] + refine OperationPtr.getResults!.mem_getResult ?_ + have hlt := List.snd_lt_of_mem_zipIdx (by simpa using hq) + rw [Array.length_toList] at hlt; omega + +/-- `rewriteMapping`'s `applyToArray` is, pointwise, the underlying value renaming: a value among +`op`'s results is redirected to the matching `newValue`, everything else is fixed. -/ +theorem rewriteMapping_applyToArray_eq_map {ctx newCtx : WfIRContext OpCode} + (op : OperationPtr) (newValues : Array ValuePtr) {mR mN} (arr : Array ValuePtr) + (hin : ∀ v ∈ arr, v.InBounds ctx.raw) : + (rewriteMapping (ctx := ctx) (newCtx := newCtx) op newValues mR mN).applyToArray arr hin + = arr.map (fun v => if v ∈ op.getResults! ctx.raw + then newValues[(op.getResults! ctx.raw).idxOf v]! else v) := by + apply Array.ext + · simp [ValueMapping.applyToArray] + · intro i h1 h2 + simp only [ValueMapping.applyToArray, Array.getElem_map, Array.getElem_attach, rewriteMapping] + split <;> grind + set_option warn.sorry false in /-- **PR 9 — bridge from the concrete driver.** When `fromLocalRewrite` runs the rewrite branch for a @@ -2504,6 +3125,7 @@ theorem RewrittenAt.of_fromLocalRewrite (hReturnCtxChanges : pattern.ReturnCtxChanges) (hReturnValuesInBounds : pattern.ReturnValuesInBounds) (hReturnValues : pattern.ReturnValues) + (hReturnValuesNotSourceResults : pattern.ReturnValuesNotSourceResults) {rewriter rewriter' : PatternRewriter OpCode} {op : OperationPtr} (opInBounds : op.InBounds rewriter.ctx.raw) {block : BlockPtr} (hOpParent : (op.get! rewriter.ctx.raw).parent = some block) @@ -2595,22 +3217,110 @@ theorem RewrittenAt.of_fromLocalRewrite have hb := (hbnd (GenericPtr.value v)).mpr hv rw [PatternRewriter.eraseOp_ctx_eq herase] grind [WfRewriter.eraseOp] + -- === Keystone block-list facts (shared by the `tgtList`/`otherBlocks` fields). === + -- `op` is in bounds of the pattern's output and not among the freshly created `newOps`. + have hopNewCtxPat : op.InBounds newCtxPat.raw := by + have := hCreated.inBounds_mono (GenericPtr.operation op) (by grind); grind + have hopNotNewOps : op ∉ newOps := fun hmem => + ((hReturnOps rewriter.ctx op newCtxPat newOps newValues hpat op).mp hmem).2 opInBounds + -- `op` occurs once in `block`'s source list, so it is in neither `pre` nor `post`. + have hoppre : op ∉ pre ∧ op ∉ post := by + have hnodup := BlockPtr.OpChain_array_toList_Nodup + (BlockPtr.operationListWF rewriter.ctx.raw block blockIn rewriter.ctx.wellFormed) + rw [hsrcList] at hnodup + simp only [Array.toList_append, List.nodup_append, List.mem_append] at hnodup + exact ⟨fun h => hnodup.1.2.2 op (by simpa using h) op (by simp) rfl, + fun h => hnodup.2.2 op (Or.inr (by simp)) op (by simpa using h) rfl⟩ + -- `block`'s list in the pattern output is still `pre ++ [op] ++ post` (only ops were created). + have hlistInit : ∀ (hb : block.InBounds newCtxPat.raw), + block.operationList newCtxPat.raw newCtxPat.wellFormed hb = pre ++ #[op] ++ post := by + intro hb; rw [hCreated.operationList_eq blockIn hb, hsrcList] + have hparInit : (op.get! newCtxPat.raw).parent = some block := + (BlockPtr.operationList.mem hopNewCtxPat).mpr + (by rw [hlistInit hblockNewCtxPat]; simp [Array.mem_append]) + -- The two driver folds as `List.foldlM`s. + have hfold1L := hfold1; rw [← Array.foldlM_toList] at hfold1L + have hfold2L := hfold2; rw [← Array.foldlM_toList] at hfold2L + -- Insert fold: `block`'s list becomes `pre ++ newOps ++ [op] ++ post`; `op` keeps its parent. + obtain ⟨hopS1, hparS1, hlistS1⟩ := + PatternRewriter.foldlM_insertOp_before_opList + (hf := fun b a b' hfa => ⟨_, _, hfa⟩) + hopNewCtxPat hfold1L hparInit hlistInit hoppre.1 hoppre.2 (by simpa using hopNotNewOps) + have hblockS1 : block.InBounds s₁.ctx.raw := by have := s₁.ctx.wellFormed.inBounds; grind + have hblockS2 : block.InBounds s₂.ctx.raw := by + have := hbnd (GenericPtr.block block); grind + -- Replace fold leaves `block`'s list untouched (`hstep` is inlined so `f` matches the driver's). + have hblockListS2 : block.operationList s₂.ctx.raw s₂.ctx.wellFormed hblockS2 + = pre ++ newOps ++ #[op] ++ post := by + rw [PatternRewriter.foldlM_preserves_opList (c := block) + (hstep := by + intro b a b' hfa + simp only [Option.some.injEq] at hfa; subst hfa + exact ⟨fun hcin => PatternRewriter.replaceValue_blockPtr_inBounds.mpr hcin, + fun hc hc' => PatternRewriter.replaceValue_operationList hc hc'⟩) + hfold2L hblockS1 hblockS2, hlistS1 hblockS1] + have hopS2 : op.InBounds s₂.ctx.raw := by have := hbnd (GenericPtr.operation op); grind + have hopParentS2 : (op.get! s₂.ctx.raw).parent = some block := + (BlockPtr.operationList.mem hopS2).mpr (by rw [hblockListS2]; simp [Array.mem_append]) refine ⟨pre, post, blockIn, blockIn', ?_⟩ exact { -- Block-list shape: discharged for the source by the split lemma. srcList := hsrcList - -- TODO(PR 9, keystone): `operationList_rewriter_insertOp` (newOps before op) + - -- `operationList_rewriter_eraseOp` (op removed) on the fold decomposition. - tgtList := by sorry - -- TODO(PR 9, keystone): insert/erase only touch `block`'s list (other blocks unchanged). - otherBlocks := by sorry + -- Target list: the insert fold turns `pre ++ [op] ++ post` into `pre ++ newOps ++ [op] ++ post` + -- (`hblockListS2`), then `eraseOp op` drops the unique `op`, leaving `pre ++ newOps ++ post`. + tgtList := by + rw [BlockPtr.operationList_congr (PatternRewriter.eraseOp_ctx_eq herase) blockIn' + (PatternRewriter.eraseOp_ctx_eq herase ▸ blockIn'), + BlockPtr.operationList_WfRewriter_eraseOp (block := block) hblockS2, + if_pos hopParentS2, hblockListS2] + exact Array.erase_mid pre newOps post op hoppre.1 hopNotNewOps + -- Other blocks: untouched by the created ops (`WithCreatedOps`), the insert fold (inserts target + -- `block ≠ c`), the replace fold, and the final `eraseOp` (drops `op`, whose parent is `block ≠ c`). + otherBlocks := by + intro c cIn cIn' hcne + -- `c` is in bounds throughout the rewrite. + have hcNewCtxPat : c.InBounds newCtxPat.raw := by + have := hCreated.inBounds_mono (GenericPtr.block c) (by grind); grind + have hcS1 : c.InBounds s₁.ctx.raw := by + have h1 := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => (GenericPtr.block c).InBounds b.ctx.raw) + (fun b a b' h => PatternRewriter.insertOp_ctx_inBounds h) hfold1 + grind + have hcS2 : c.InBounds s₂.ctx.raw := by have := hbnd (GenericPtr.block c); grind + have hcond : (op.get! s₂.ctx.raw).parent ≠ (c : Option BlockPtr) := by + rw [hopParentS2] + intro h + have hbc : block = c := by simpa using h + exact hcne hbc.symm + -- `eraseOp op` leaves `c`'s list alone, since `op`'s parent is `block ≠ c`. + rw [BlockPtr.operationList_congr (PatternRewriter.eraseOp_ctx_eq herase) cIn' + (PatternRewriter.eraseOp_ctx_eq herase ▸ cIn'), + BlockPtr.operationList_WfRewriter_eraseOp (block := c) hcS2, if_neg hcond] + -- Replace fold leaves `c`'s list alone. + rw [PatternRewriter.foldlM_preserves_opList (c := c) + (hstep := by + intro b a b' hfa + simp only [Option.some.injEq] at hfa; subst hfa + exact ⟨fun hcin => PatternRewriter.replaceValue_blockPtr_inBounds.mpr hcin, + fun h1 h2 => PatternRewriter.replaceValue_operationList h1 h2⟩) + hfold2L hcS1 hcS2] + -- Insert fold leaves `c`'s list alone (inserts target `block ≠ c`). + rw [PatternRewriter.foldlM_insertOp_before_other (c := c) (block := block) hcne + (hf := fun b a b' hfa => ⟨_, _, hfa⟩) + hopNewCtxPat hparInit hfold1L (by simpa using hopNotNewOps) hcNewCtxPat hcS1] + -- Created ops leave `c`'s list alone. + exact (hCreated.operationList_eq cIn hcNewCtxPat).symm -- Number of produced values: directly from the pattern's `ReturnValues` obligation. newValuesSize := hReturnValues rewriter.ctx op opInBounds newCtxPat newOps newValues hpat - -- TODO(PR 9): `ReturnValuesInBounds` gives in-bounds of `newCtxPat`; transport via `hbnd`, then - -- `eraseOp`. NEEDS EXTRA HYPOTHESIS: a `newValue` that is one of `op`'s own results would be erased, - -- so this requires the pattern to guarantee `newValues` do not reference `op`'s results (cf. - -- `hSurviveVal`, which is then directly applicable). - newValuesInBounds := by sorry + -- Every produced value is in bounds of `newCtxPat` (`ReturnValuesInBounds`) and is not a result of + -- `op` (`ReturnValuesNotSourceResults`, since `op` is in bounds of the source), so it survives the + -- final `eraseOp op` (`hSurviveVal`). + newValuesInBounds := by + intro v hv + apply hSurviveVal v (hReturnValuesInBounds rewriter.ctx op newCtxPat newOps newValues hpat v hv) + intro orp hvorp heq + exact hReturnValuesNotSourceResults rewriter.ctx op newCtxPat newOps newValues hpat v hv orp hvorp + (heq ▸ opInBounds) -- `ReturnOps` characterizes `newOps` as fresh to `newCtxPat`; a `newOp ≠ op` has the same bounds -- in `newCtxPat` and `rewriter'.ctx` (`hOpBnd`), so the freshness transports. newOpsFresh := by @@ -2644,9 +3354,118 @@ theorem RewrittenAt.of_fromLocalRewrite -- Every operation `≠ op` survives: into `newCtxPat` (pattern only creates), then the folds/erase. survives := fun o hoIn hne => hSurviveOp o hne (hCreated.inBounds_mono (GenericPtr.operation o) (by grind)) - -- TODO(PR 9, keystone): `CrossContextFrame` under `σ`; `*_insertOp`/`*_detachOp` GetSet lemmas - -- give unchanged type/props/results/successors, `replaceValue` redirects operands = `σ.applyToArray`. - frame := by sorry + -- `CrossContextFrame` under `σ`: created-ops/insert-fold/erase frame `o`'s intrinsic data + -- (`SameIntrinsic`), the replace fold redirects its operands exactly as `σ` does, and `o`'s own + -- results survive untouched. `reflect` uses that no `newValue` is a source-context result. + frame := by + intro o oIn oIn' hne + have hNoAlias : ∀ x ∈ newValues, ∀ m, x ≠ (op.getResult m : ValuePtr) := by + intro x hx m heq + exact hReturnValuesNotSourceResults rewriter.ctx op newCtxPat newOps newValues hpat x hx + (op.getResult m) heq (by simpa using opInBounds) + have hsize : newValues.size = op.getNumResults! rewriter.ctx.raw := + hReturnValues rewriter.ctx op opInBounds newCtxPat newOps newValues hpat + -- `o` survives every stage in bounds. + have hoNewCtxPat : o.InBounds newCtxPat.raw := + hCreated.inBounds_mono (GenericPtr.operation o) (by grind) + have hoS1 : o.InBounds s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => (GenericPtr.operation o).InBounds b.ctx.raw) + (fun b a b' hh => PatternRewriter.insertOp_ctx_inBounds hh) hfold1 + grind + have hoErase := PatternRewriter.eraseOp_ctx_eq herase ▸ oIn' + -- (1) Intrinsic data is framed across the whole pipeline. + have hcre : o.SameIntrinsic rewriter.ctx.raw newCtxPat.raw := hCreated.sameIntrinsic oIn + have hins : o.SameIntrinsic newCtxPat.raw s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => o.SameIntrinsic newCtxPat.raw b.ctx.raw) + (fun b a b' hh => + ⟨fun hb => hb.trans (PatternRewriter.insertOp_sameIntrinsic hh).symm, + fun hb => hb.trans (PatternRewriter.insertOp_sameIntrinsic hh)⟩) hfold1 + exact h.mpr OperationPtr.SameIntrinsic.rfl + have hrep : o.SameIntrinsic s₁.ctx.raw s₂.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => o.SameIntrinsic s₁.ctx.raw b.ctx.raw) + (fun b a b' hh => by + have hst : o.SameIntrinsic b.ctx.raw b'.ctx.raw := by + simp only [Option.some.injEq] at hh; subst hh + exact PatternRewriter.replaceValue_sameIntrinsic + exact ⟨fun hb => hb.trans hst.symm, fun hb => hb.trans hst⟩) hfold2 + exact h.mpr OperationPtr.SameIntrinsic.rfl + have hers : o.SameIntrinsic s₂.ctx.raw rewriter'.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact ⟨OperationPtr.getOpType!_wfRewriter_eraseOp hoErase, + fun _ => OperationPtr.getProperties!_wfRewriter_eraseOp hoErase, + OperationPtr.getNumResults!_wfRewriter_eraseOp hoErase, + OperationPtr.getSuccessors!_wfRewriter_eraseOp hoErase, + OperationPtr.getResultTypes!_wfRewriter_eraseOp hoErase⟩ + have hsame : o.SameIntrinsic rewriter.ctx.raw rewriter'.ctx.raw := + hcre.trans (hins.trans (hrep.trans hers)) + -- (2) Operands are rewritten by the result→`newValues` redirect, which equals `σ`. + have hopsErase : o.getOperands! rewriter'.ctx.raw = o.getOperands! s₂.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact OperationPtr.getOperands!_wfRewriter_eraseOp hoErase + have hopsRepl : o.getOperands! s₂.ctx.raw + = (newValues.zipIdx.toList).foldl + (fun arr q => arr.map (fun v => if v = (op.getResult q.2 : ValuePtr) then q.1 else v)) + (o.getOperands! s₁.ctx.raw) := + PatternRewriter.foldlM_replaceValue_getOperands + (hf := fun b q b' hfa => ⟨_, _, _, by simp only [Option.some.injEq] at hfa; exact hfa⟩) + hfold2L hoS1 + have hopsIns : o.getOperands! s₁.ctx.raw = o.getOperands! newCtxPat.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + o.getOperands! b.ctx.raw = o.getOperands! newCtxPat.raw) + (fun b a b' hh => by + have := PatternRewriter.insertOp_getOperands (o := o) hh + constructor <;> intro hb <;> grind) hfold1 + exact h.mpr rfl + have hopsCre : o.getOperands! newCtxPat.raw = o.getOperands! rewriter.ctx.raw := + hCreated.getOperands_eq oIn + -- Assemble `PreservesOperation` (fields: opType, props, resultTypes, successors, operands, + -- results, reflect). + refine ⟨hsame.1, ?_, hsame.2.2.2.2, hsame.2.2.2.1, ?_, ?_, ?_⟩ + · -- props + rw [hsame.2.1] + refine eq_of_heq (HEq.trans ?_ (eqRec_heq _ _).symm) + rw [hsame.1] + · -- operands + rw [hopsErase, hopsRepl, hopsIns, hopsCre, List.foldl_arrayMap_fusion, + rewriteMapping_applyToArray_eq_map] + congr 1 + funext v + exact fold_replaceResult_zipIdx_eq_sigma op newValues hsize hNoAlias v + · -- results: `o`'s results are unchanged and fixed by `σ` (none is a result of `op`). + have hres : o.getResults! rewriter'.ctx.raw = o.getResults! rewriter.ctx.raw := by + simp only [OperationPtr.getResults!]; rw [hsame.2.2.1] + rw [hres, rewriteMapping_applyToArray_eq_map] + apply Array.ext + · simp + · intro i h1 _ + simp only [Array.getElem_map] + have hidx : i < o.getNumResults! rewriter.ctx.raw := by + simpa [OperationPtr.getResults!.size_eq_getNumResults!] using h1 + have hnotmem : (o.getResults! rewriter.ctx.raw)[i] ∉ op.getResults! rewriter.ctx.raw := by + rw [OperationPtr.getResults!.getElem_eq_getResult hidx] + intro hmem + obtain ⟨k, _, hkeq⟩ := OperationPtr.getResults!.mem_iff_exists_index.mp hmem + simp only [OperationPtr.getResult, ValuePtr.opResult.injEq, OpResultPtr.mk.injEq] at hkeq + exact hne hkeq.1.symm + rw [if_neg hnotmem] + · -- reflect + intro val valIn i hval + by_cases hvr : val ∈ op.getResults! rewriter.ctx.raw + · exfalso + simp only [rewriteMapping, dif_pos hvr] at hval + have hk : (op.getResults! rewriter.ctx.raw).idxOf val < newValues.size := by + have hlt : (op.getResults! rewriter.ctx.raw).idxOf val + < (op.getResults! rewriter.ctx.raw).size := Array.idxOf_lt_length_of_mem hvr + simp only [OperationPtr.getResults!.size_eq_getNumResults!] at hlt; omega + have hmem : newValues[(op.getResults! rewriter.ctx.raw).idxOf val]! ∈ newValues := by + rw [getElem!_pos newValues _ hk]; exact Array.getElem_mem hk + exact hReturnValuesNotSourceResults rewriter.ctx op newCtxPat newOps newValues hpat _ hmem + (o.getResult i) hval (by simpa using oIn) + · simpa only [rewriteMapping, dif_neg hvr] using hval -- Blocks stay in bounds: into `newCtxPat`, then the folds/erase (erase removes only `op`). blocksInBounds := fun b hb => hSurviveBlock b (hCreated.inBounds_mono (GenericPtr.block b) (by grind)) From 9db3068709b1272adbcb5e04f025bd8336f78198 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 02:23:37 +0200 Subject: [PATCH 17/31] remove bad constraint --- Veir/Dominance.lean | 15 +++++ Veir/Interpreter/Refinement/Basic.lean | 15 ++++- Veir/Interpreter/Refinement/Lemmas.lean | 7 ++- Veir/Interpreter/Refinement/Monotonicity.lean | 6 +- Veir/PatternRewriter/Semantics.lean | 40 +++++++++---- Veir/PatternRewriter/Soundness.lean | 57 ++++++++++++------- 6 files changed, 100 insertions(+), 40 deletions(-) diff --git a/Veir/Dominance.lean b/Veir/Dominance.lean index 4bd5b93fb..527dc30b3 100644 --- a/Veir/Dominance.lean +++ b/Veir/Dominance.lean @@ -204,6 +204,21 @@ axiom WfIRContext.Dom.opResult_not_dominatesIp_atStart! {r : ValuePtr} (rResult : r ∈ op.getResults! ctx.raw) : ¬ r.dominatesIp (InsertPoint.atStart! block ctx.raw) ctx +/-- SSA antisymmetry of the definition order, used to justify op-result *forwarding* in the pattern +rewriter. Two distinct operations cannot each be defined before the other: if a result of `op₁` +dominates the point before `op₂`, then a result of `op₂` cannot dominate the point before `op₁` +(that would mean `op₁` strictly dominates `op₂` and `op₂` strictly dominates `op₁`). This is what +rules out the only would-be `ReflectsResults o o` collision when a rewrite redirects `op`'s result +onto a result of a surviving operation `o`: `op`'s own result cannot dominate `.before o` while +`o`'s forwarded result dominates `.before op`. -/ +axiom WfIRContext.Dom.not_opResult_dominatesIp_before_cycle + (ctxDom : ctx.Dom) {op₁ op₂ : OperationPtr} (hne : op₁ ≠ op₂) + {r₁ : ValuePtr} (r₁Res : r₁ ∈ op₁.getResults! ctx.raw) + (r₁Dom : r₁.dominatesIp (InsertPoint.before op₂) ctx) + {r₂ : ValuePtr} (r₂Res : r₂ ∈ op₂.getResults! ctx.raw) + (r₂Dom : r₂.dominatesIp (InsertPoint.before op₁) ctx) : + False + /-! ## Block-level dominance diff --git a/Veir/Interpreter/Refinement/Basic.lean b/Veir/Interpreter/Refinement/Basic.lean index cfc83ac58..3302174a2 100644 --- a/Veir/Interpreter/Refinement/Basic.lean +++ b/Veir/Interpreter/Refinement/Basic.lean @@ -150,13 +150,22 @@ def ValueMapping.applyToArray {ctx ctx' : WfIRContext OpInfo} (mapping : ValueMa vals.attach.map (fun ⟨v, hv⟩ => (mapping ⟨v, valsIn v hv⟩).val) /-- -`mapping` *reflects* `op'`'s result pointers back to `op`'s if the only value it sends onto `op'`'s -`i`-th result pointer is `op`'s `i`-th result pointer. Paired with the "fixes" equation +`mapping` *reflects* `op'`'s result pointers back to `op`'s if the only **in-scope** value it sends +onto `op'`'s `i`-th result pointer is `op`'s `i`-th result pointer. Paired with the "fixes" equation `mapping.applyToArray (op.getResults! ..) = op'.getResults! ..`, this says `mapping` matches the two -operations' results index-by-index without mapping any other value onto them. -/ +operations' results index-by-index without mapping any other in-scope value onto them. + +The reflection is required only for `val` that **dominate the program point before `op`** — i.e. the +values actually live at `op`'s step. This is exactly the set of values the sole consumer +(`setResultValues?_isRefinedByAt`, via `not_mem_getResults`) ever queries, and the scoping is what +makes op-result *forwarding* sound: a rewrite that redirects `op`'s result onto a result of a +surviving operation `o` (`o` defined before `op`) does *not* break `ReflectsResults o o`, because the +only would-be witness — `op`'s own result mapping onto `o`'s — fails the dominance guard (`op`'s +result cannot dominate `.before o` when `o` is defined before `op`; SSA antisymmetry). -/ def ValueMapping.ReflectsResults {ctx ctx' : WfIRContext OpInfo} (mapping : ValueMapping ctx ctx') (op op' : OperationPtr) : Prop := ∀ (val : ValuePtr) (valIn : val.InBounds ctx.raw) (i : Nat), + val.dominatesIp (InsertPoint.before op) ctx → (mapping ⟨val, valIn⟩).val = op'.getResult i → val = op.getResult i /-- An operation `op` in `ctx` is *preserved* and renamed to an operation `op'` in `ctx'` by the diff --git a/Veir/Interpreter/Refinement/Lemmas.lean b/Veir/Interpreter/Refinement/Lemmas.lean index 3dd455825..e87fee083 100644 --- a/Veir/Interpreter/Refinement/Lemmas.lean +++ b/Veir/Interpreter/Refinement/Lemmas.lean @@ -147,19 +147,20 @@ theorem ValueMapping.applyToArray_getArguments!_ext Array.getElem_attach] at hArgs grind -/-- If a value mapping reflects results from `op` to `op'`, then values that are not in -`op` results are not mapped to values in `op'` results. -/ +/-- If a value mapping reflects results from `op` to `op'`, then in-scope values (dominating the point +before `op`) that are not in `op` results are not mapped to values in `op'` results. -/ @[grind .] theorem ValueMapping.ReflectsResults.not_mem_getResults {ctx ctx' : WfIRContext OpInfo} {mapping : ValueMapping ctx ctx'} {op op' : OperationPtr} {val : ValuePtr} (valIn : val.InBounds ctx.raw) (hReflect : mapping.ReflectsResults op op') + (hValDom : val.dominatesIp (InsertPoint.before op) ctx) (hNotMem : val ∉ op.getResults! ctx.raw) : (mapping ⟨val, valIn⟩).val ∉ op'.getResults! ctx'.raw := by intro hmem simp only [OperationPtr.getResults!.mem_iff_exists_index] at hmem have ⟨index, hindex, heq⟩ := hmem - grind [OperationPtr.getResults!.mem_iff_exists_index, hReflect val valIn index heq.symm] + grind [OperationPtr.getResults!.mem_iff_exists_index, hReflect val valIn index hValDom heq.symm] /-! ## Conformance under refinement -/ diff --git a/Veir/Interpreter/Refinement/Monotonicity.lean b/Veir/Interpreter/Refinement/Monotonicity.lean index 34892f7f3..f42c95113 100644 --- a/Veir/Interpreter/Refinement/Monotonicity.lean +++ b/Veir/Interpreter/Refinement/Monotonicity.lean @@ -199,11 +199,11 @@ theorem VariableState.setResultValues?_isRefinedByAt cases OperationPtr.getResults!_not_mem_or_eq_getResult ctx.raw val op with | inl hNotMem => -- `val` is not a result of `op`: unchanged by `setResultValues?` on both sides. - rw [VariableState.getVar?_setResultValues?_of_notMem_getResults! hNotMem hSrc] at hsv - have hσNotMem := (hReflect.not_mem_getResults valIn hNotMem) - rw [VariableState.getVar?_setResultValues?_of_notMem_getResults! hσNotMem hTgt] at htv have hValDomBefore : val.dominatesIp (.before op) ctx := hValDomAfter.resolve_right hNotMem + rw [VariableState.getVar?_setResultValues?_of_notMem_getResults! hNotMem hSrc] at hsv + have hσNotMem := (hReflect.not_mem_getResults valIn hValDomBefore hNotMem) + rw [VariableState.getVar?_setResultValues?_of_notMem_getResults! hσNotMem hTgt] at htv have hσValDomBefore : (mapping ⟨val, valIn⟩).val.dominatesIp (.before op') ctx' := hσValDomAfter.resolve_right hσNotMem exact hRef val valIn hValDomBefore hσValDomBefore sv tv hsv htv diff --git a/Veir/PatternRewriter/Semantics.lean b/Veir/PatternRewriter/Semantics.lean index 090de246d..4b7942b50 100644 --- a/Veir/PatternRewriter/Semantics.lean +++ b/Veir/PatternRewriter/Semantics.lean @@ -89,20 +89,36 @@ def LocalRewritePattern.ReturnValuesInBounds (pattern : LocalRewritePattern OpCo ∀ v ∈ newValues, v.InBounds newCtx.raw /-- -No value returned by the pattern is a result of a *pre-existing* (source-context) operation: a -returned value is either a result of one of the freshly created `newOps`, or a pre-existing non-result -value (e.g. a block argument). It may never be a result of an operation already in `ctx`. - -This rules out three problems with the driver's "redirect `op`'s results to `newValues`, then erase -`op`" pipeline: (a) a `newValue` equal to a result of `op` would dangle once `op` is erased; (b) it -would make the sequential redirect fold diverge from the parallel value renaming `σ`; and (c) a -`newValue` equal to a result of *any* surviving operation `o` would let `σ` map `op`'s result onto -`o`'s result, breaking the `ReflectsResults o o` frame clause. It admits block-argument forwarding -(`x + 0 → x` with `x` a block argument), which is the forwarding case the framework supports. +No value returned by the pattern is one of `op`'s *own* result pointers. This rules out two problems +with the driver's "redirect `op`'s results to `newValues`, then erase `op`" pipeline: (a) a `newValue` +equal to a result of `op` would dangle once `op` is erased; (b) it would make the sequential redirect +fold chain instead of matching the parallel value renaming `σ`. + +This replaces the old `ReturnValuesNotSourceResults`, which *also* forbade results of surviving +(pre-existing) operations. That extra restriction is unnecessary: a returned value may now be a result +of an operation already in `ctx`, provided it is in scope at `op` (`ReturnValuesDominate`). This is +what makes general forwarding `x + 0 → x` sound — `x` may be a block argument *or* a result of an +operation defined before `op`. -/ +def LocalRewritePattern.ReturnValuesNotOwnResults (pattern : LocalRewritePattern OpCode) : Prop := + ∀ ctx op newCtx newOps newValues, pattern ctx op = some (newCtx, some (newOps, newValues)) → + ∀ v ∈ newValues, ∀ m, v ≠ (op.getResult m : ValuePtr) + +/-- +Every produced value that already exists in the source context (a *forwarded* pre-existing value) +dominates the program point before `op`: it is in scope at `op`'s use site. Produced values that are +fresh (results of the inserted `newOps`, not in bounds of `ctx`) are excluded by the `v.InBounds` +guard — they are inserted before `op` and dominate it by construction. + +This is the SSA-validity condition for forwarding. Together with source dominance-wellformedness it is +exactly what discharges the (dominance-scoped) `ReflectsResults o o` frame clause for a surviving +operation `o` whose result is forwarded: `op`'s own result cannot dominate the point before `o` while +`o`'s forwarded result dominates the point before `op` (SSA antisymmetry, +`WfIRContext.Dom.not_opResult_dominatesIp_before_cycle`). It admits any in-scope value — a block +argument or a result of an operation defined before `op` (`x + 0 → x`). -/ -def LocalRewritePattern.ReturnValuesNotSourceResults (pattern : LocalRewritePattern OpCode) : Prop := +def LocalRewritePattern.ReturnValuesDominate (pattern : LocalRewritePattern OpCode) : Prop := ∀ ctx op newCtx newOps newValues, pattern ctx op = some (newCtx, some (newOps, newValues)) → - ∀ v ∈ newValues, ∀ orp : OpResultPtr, v = ValuePtr.opResult orp → ¬ orp.op.InBounds ctx.raw + ∀ v ∈ newValues, v.InBounds ctx.raw → v.dominatesIp (InsertPoint.before op) ctx /-- Indexed access on the returned values is in bounds of the new context. diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index e79fc09b6..b699e3f97 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -166,9 +166,11 @@ structure RewrittenAt /-- Every produced value dominates the post-insertion point in `block` — the `newCtx` analog of "after `op`", i.e. the end of the inserted `newOps` span (`afterLast newOps (atStart! block)`). This is the genuine SSA-validity condition on produced values, satisfied both by results of inserted - `newOps` (defined within the span) and by forwarded pre-existing values (e.g. a block argument), - which are in scope throughout the block. It replaces the old `newValuesAreResults`, admitting - block-argument forwarding (`x + 0 → x`, where `x` is a block argument). -/ + `newOps` (defined within the span) and by forwarded pre-existing values in scope at `op`. It replaces + the old `newValuesAreResults`, admitting general forwarding (`x + 0 → x`): `x` may be a block + argument *or* a result of an operation defined before `op` — the latter is what the dominance-scoped + `ReflectsResults` and `ReturnValuesDominate` together make sound (a forwarded surviving-op result + cannot collide with `op`'s own result by SSA antisymmetry). -/ newValuesDominate : ∀ v ∈ newValues, v.dominatesIp (InsertPoint.afterLast newOps.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) newCtx @@ -3125,8 +3127,10 @@ theorem RewrittenAt.of_fromLocalRewrite (hReturnCtxChanges : pattern.ReturnCtxChanges) (hReturnValuesInBounds : pattern.ReturnValuesInBounds) (hReturnValues : pattern.ReturnValues) - (hReturnValuesNotSourceResults : pattern.ReturnValuesNotSourceResults) + (hReturnValuesNotOwnResults : pattern.ReturnValuesNotOwnResults) + (hReturnValuesDominate : pattern.ReturnValuesDominate) {rewriter rewriter' : PatternRewriter OpCode} + (hSrcDom : rewriter.ctx.Dom) {op : OperationPtr} (opInBounds : op.InBounds rewriter.ctx.raw) {block : BlockPtr} (hOpParent : (op.get! rewriter.ctx.raw).parent = some block) (hOpRegions : op.getNumRegions! rewriter.ctx.raw = 0) @@ -3312,15 +3316,16 @@ theorem RewrittenAt.of_fromLocalRewrite exact (hCreated.operationList_eq cIn hcNewCtxPat).symm -- Number of produced values: directly from the pattern's `ReturnValues` obligation. newValuesSize := hReturnValues rewriter.ctx op opInBounds newCtxPat newOps newValues hpat - -- Every produced value is in bounds of `newCtxPat` (`ReturnValuesInBounds`) and is not a result of - -- `op` (`ReturnValuesNotSourceResults`, since `op` is in bounds of the source), so it survives the - -- final `eraseOp op` (`hSurviveVal`). + -- Every produced value is in bounds of `newCtxPat` (`ReturnValuesInBounds`) and is not one of + -- `op`'s own results (`ReturnValuesNotOwnResults`), so it survives the final `eraseOp op` + -- (`hSurviveVal`, which only needs the value's owner to differ from `op`). newValuesInBounds := by intro v hv apply hSurviveVal v (hReturnValuesInBounds rewriter.ctx op newCtxPat newOps newValues hpat v hv) intro orp hvorp heq - exact hReturnValuesNotSourceResults rewriter.ctx op newCtxPat newOps newValues hpat v hv orp hvorp - (heq ▸ opInBounds) + apply hReturnValuesNotOwnResults rewriter.ctx op newCtxPat newOps newValues hpat v hv orp.index + obtain ⟨o', i'⟩ := orp + grind [OperationPtr.getResult] -- `ReturnOps` characterizes `newOps` as fresh to `newCtxPat`; a `newOp ≠ op` has the same bounds -- in `newCtxPat` and `rewriter'.ctx` (`hOpBnd`), so the freshness transports. newOpsFresh := by @@ -3359,10 +3364,8 @@ theorem RewrittenAt.of_fromLocalRewrite -- results survive untouched. `reflect` uses that no `newValue` is a source-context result. frame := by intro o oIn oIn' hne - have hNoAlias : ∀ x ∈ newValues, ∀ m, x ≠ (op.getResult m : ValuePtr) := by - intro x hx m heq - exact hReturnValuesNotSourceResults rewriter.ctx op newCtxPat newOps newValues hpat x hx - (op.getResult m) heq (by simpa using opInBounds) + have hNoAlias : ∀ x ∈ newValues, ∀ m, x ≠ (op.getResult m : ValuePtr) := + hReturnValuesNotOwnResults rewriter.ctx op newCtxPat newOps newValues hpat have hsize : newValues.size = op.getNumResults! rewriter.ctx.raw := hReturnValues rewriter.ctx op opInBounds newCtxPat newOps newValues hpat -- `o` survives every stage in bounds. @@ -3452,8 +3455,12 @@ theorem RewrittenAt.of_fromLocalRewrite simp only [OperationPtr.getResult, ValuePtr.opResult.injEq, OpResultPtr.mk.injEq] at hkeq exact hne hkeq.1.symm rw [if_neg hnotmem] - · -- reflect - intro val valIn i hval + · -- reflect: SSA dominance rules out the only would-be collision. The reflection is required + -- only for `val` in scope at `o` (`hValDom`). If `val` is a result of `op` redirected by `σ` + -- onto `o`'s `i`-th result, then `op`'s result dominates `.before o` while `o`'s forwarded + -- result (a source value, in scope at `op` by `ReturnValuesDominate`) dominates `.before op` + -- — impossible for `o ≠ op` (`not_opResult_dominatesIp_before_cycle`). + intro val valIn i hValDom hval by_cases hvr : val ∈ op.getResults! rewriter.ctx.raw · exfalso simp only [rewriteMapping, dif_pos hvr] at hval @@ -3461,10 +3468,22 @@ theorem RewrittenAt.of_fromLocalRewrite have hlt : (op.getResults! rewriter.ctx.raw).idxOf val < (op.getResults! rewriter.ctx.raw).size := Array.idxOf_lt_length_of_mem hvr simp only [OperationPtr.getResults!.size_eq_getNumResults!] at hlt; omega - have hmem : newValues[(op.getResults! rewriter.ctx.raw).idxOf val]! ∈ newValues := by - rw [getElem!_pos newValues _ hk]; exact Array.getElem_mem hk - exact hReturnValuesNotSourceResults rewriter.ctx op newCtxPat newOps newValues hpat _ hmem - (o.getResult i) hval (by simpa using oIn) + -- The forwarded value `σ val = (o.getResult i : ValuePtr)` is one of `newValues`. + have hmem : (ValuePtr.opResult (o.getResult i)) ∈ newValues := by + rw [← hval, getElem!_pos newValues _ hk]; exact Array.getElem_mem hk + -- It is a *source* result of `o` (its index is framed by `hcre`), hence in scope at `op` + -- (`ReturnValuesDominate`). + have hvInPat := hReturnValuesInBounds rewriter.ctx op newCtxPat newOps newValues hpat _ hmem + have hiSrc : i < o.getNumResults! rewriter.ctx.raw := by + have hi := OpResultPtr.inBounds_OperationPtr_getNumResults! (o.getResult i) newCtxPat.raw + (by simpa using hvInPat) + simpa [hcre.2.2.1] using hi + have hvRes : (ValuePtr.opResult (o.getResult i)) ∈ o.getResults! rewriter.ctx.raw := + OperationPtr.getResults!.mem_getResult hiSrc + have hvInSrc : (ValuePtr.opResult (o.getResult i)).InBounds rewriter.ctx.raw := by + simpa using OpResultPtr.inBounds_of (result := o.getResult i) oIn (by simpa using hiSrc) + have hvDom := hReturnValuesDominate rewriter.ctx op newCtxPat newOps newValues hpat _ hmem hvInSrc + exact hSrcDom.not_opResult_dominatesIp_before_cycle hne.symm hvr hValDom hvRes hvDom · simpa only [rewriteMapping, dif_neg hvr] using hval -- Blocks stay in bounds: into `newCtxPat`, then the folds/erase (erase removes only `op`). blocksInBounds := fun b hb => From 70d0b6296ef1b3402422724d99377323ac6a0ccd Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 02:55:36 +0200 Subject: [PATCH 18/31] One less sorry --- Veir/PatternRewriter/Soundness.lean | 196 +++++++++++++++++++++++++--- 1 file changed, 181 insertions(+), 15 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index b699e3f97..e5b69303f 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -154,8 +154,19 @@ structure RewrittenAt -- Clause 5: structure frame. /-- Blocks stay in bounds — successor-`InBounds` transport. -/ blocksInBounds : ∀ (b : BlockPtr), b.InBounds ctx.raw → b.InBounds newCtx.raw - blockArgsPreserved : ∀ (bl : BlockPtr), bl.InBounds ctx.raw → - (bl.get! newCtx.raw).arguments = (bl.get! ctx.raw).arguments + /-- The number of arguments of every in-bounds block is preserved: op-list edits never add or + remove block arguments. -/ + blockNumArgsPreserved : ∀ (bl : BlockPtr), bl.InBounds ctx.raw → + bl.getNumArguments! newCtx.raw = bl.getNumArguments! ctx.raw + /-- Every block argument's type is preserved. Note the full `Block.arguments` record is *not* + preserved: each `BlockArgument` carries the head (`firstUse`) of its def-use chain, which the + rewrite mutates (erasing `op` detaches its operands; redirecting `op`'s result-uses onto a forwarded + `newValue` that is itself a block argument grows that argument's chain). The SSA-relevant data — the + argument count (`blockNumArgsPreserved`) and per-argument type — is what survives and is all the + block-argument frame consequences below need. -/ + blockArgTypesPreserved : ∀ (bl : BlockPtr), bl.InBounds ctx.raw → + ∀ i, i < bl.getNumArguments! ctx.raw → + (bl.getArgument i : ValuePtr).getType! newCtx.raw = (bl.getArgument i : ValuePtr).getType! ctx.raw blockDominatesPreserved : ∀ (b₁ b₂ : BlockPtr), b₁.InBounds ctx.raw → b₂.InBounds ctx.raw → (b₁.dominates b₂ newCtx ↔ b₁.dominates b₂ ctx) -- Clause 6: result well-formedness. @@ -397,15 +408,14 @@ theorem postParentEq' (h : RewrittenAt ctx op newOps newValues newCtx opIn block /-- The number of arguments of any in-bounds block is preserved by the rewrite. -/ theorem numArgsEq (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) : - bl.getNumArguments! newCtx.raw = bl.getNumArguments! ctx.raw := by - simp only [BlockPtr.getNumArguments!, h.blockArgsPreserved bl blIn] + bl.getNumArguments! newCtx.raw = bl.getNumArguments! ctx.raw := + h.blockNumArgsPreserved bl blIn -/-- The type of any block argument is preserved by the rewrite. -/ +/-- The type of any (in-range) block argument is preserved by the rewrite. -/ theorem argType_eq (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') - {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) (i : Nat) : - (bl.getArgument i : ValuePtr).getType! newCtx.raw = (bl.getArgument i : ValuePtr).getType! ctx.raw := by - simp only [ValuePtr.getType!_blockArgument, BlockArgumentPtr.get!, BlockPtr.getArgument_block, - BlockPtr.getArgument_index, h.blockArgsPreserved bl blIn] + {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) (i : Nat) (hi : i < bl.getNumArguments! ctx.raw) : + (bl.getArgument i : ValuePtr).getType! newCtx.raw = (bl.getArgument i : ValuePtr).getType! ctx.raw := + h.blockArgTypesPreserved bl blIn i hi /-- A block argument is never a result of `op` (distinct `ValuePtr` constructors). -/ theorem blockArg_notMem_getResults @@ -453,13 +463,14 @@ theorem mapping_getResult_mem_newValues h.mapNonResultsInBounds h.newValuesSize] at hx exact hx -/-- The block-argument array of `bl` is identical across the two contexts (the rewrite only edits -operation lists, never block arguments). -/ +/-- The block-argument *pointer* array of `bl` is identical across the two contexts: `getArguments!` +is `getArgument` mapped over `range (getNumArguments! ·)`, so it depends only on the argument count, +which the rewrite preserves (`blockNumArgsPreserved`). -/ theorem getArguments!_eq (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') {bl : BlockPtr} (blIn : bl.InBounds ctx.raw) : bl.getArguments! newCtx.raw = bl.getArguments! ctx.raw := by - simp only [BlockPtr.getArguments!, BlockPtr.getNumArguments!, h.blockArgsPreserved bl blIn] + simp only [BlockPtr.getArguments!, h.blockNumArgsPreserved bl blIn] /-- `σ` never maps an in-scope value onto one of `bl`'s block arguments unless it already is that block argument: a value not in `bl`'s arguments is either fixed by `σ` (so stays out of the @@ -1513,7 +1524,7 @@ theorem RewrittenAt.interpretBlock_refinement · exact hpt j h · rw [getElem!_neg values j h, getElem!_neg values' j (hsize ▸ h)] exact RuntimeValue.isRefinedBy_refl _ - rw [hRW.argType_eq bIn] + rw [hRW.argType_eq bIn j (hRW.numArgsEq bIn ▸ hj)] exact RuntimeValue.Conforms_of_isRefinedBy hPt ((VariableState.setArgumentValues?_isSome_iff_conforms state.variables).mpr ⟨newVars, hsa⟩ j (hRW.numArgsEq bIn ▸ hj)) @@ -2969,6 +2980,79 @@ theorem WfIRContext.WithCreatedOps.getOperands_eq {ctx₁ ctx₂ : WfIRContext O rw [OperationPtr.getOperands!_WfRewriter_createOp hcreate, if_neg (by grind)] exact ih oIn +/-! ### Block-argument count/type frame across the rewrite stages. + +The rewrite never adds, removes, or retypes block arguments (it only edits operation lists and +def-use chains). The lemmas below lift the per-primitive `getNumArguments!`/`getType!` frame facts to +the `PatternRewriter` insert/replace folds and to `WithCreatedOps`; they discharge the +`blockNumArgsPreserved`/`blockArgTypesPreserved` fields of `RewrittenAt.of_fromLocalRewrite`. -/ + +/-- `PatternRewriter.insertOp` leaves every block's argument count unchanged. -/ +theorem PatternRewriter.insertOp_getNumArguments {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {bl : BlockPtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + bl.getNumArguments! b'.ctx.raw = bl.getNumArguments! b.ctx.raw := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact BlockPtr.getNumArguments!_wfRewriter_insertOp hwf + +/-- `PatternRewriter.insertOp` leaves every value's type unchanged. -/ +theorem PatternRewriter.insertOp_getType {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {v : ValuePtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + v.getType! b'.ctx.raw = v.getType! b.ctx.raw := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact ValuePtr.getType!_wfRewriter_insertOp hwf + +/-- `PatternRewriter.replaceValue` leaves every block's argument count unchanged. -/ +theorem PatternRewriter.replaceValue_getNumArguments {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {bl : BlockPtr} : + bl.getNumArguments! (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw = + bl.getNumArguments! b.ctx.raw := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx]; exact BlockPtr.getNumArguments!_WfRewriter_replaceValue + +/-- `PatternRewriter.replaceValue` leaves every value's type unchanged. -/ +theorem PatternRewriter.replaceValue_getType {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {v : ValuePtr} : + v.getType! (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw = + v.getType! b.ctx.raw := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx]; exact ValuePtr.getType!_WfRewriter_replaceValue + +/-- A `WithCreatedOps` chain leaves every block's argument count unchanged (it only creates fresh +ops). -/ +theorem WfIRContext.WithCreatedOps.getNumArguments_eq {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) (bl : BlockPtr) : + bl.getNumArguments! ctx₂.raw = bl.getNumArguments! ctx₁.raw := by + induction h with + | Nil => rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + obtain ⟨opType, rt, ops, succ, regs, props, k₁, k₂, k₃, k₄, hcreate⟩ := hex + rw [BlockPtr.getNumArguments!_WfRewriter_createOp hcreate]; exact ih + +/-- A `WithCreatedOps` chain leaves every block argument's type unchanged: creating a fresh op only +fixes the types of that op's own (`opResult`) values, never any block argument. -/ +theorem WfIRContext.WithCreatedOps.getType_blockArgument_eq {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) (ba : BlockArgumentPtr) : + (ValuePtr.blockArgument ba).getType! ctx₂.raw = (ValuePtr.blockArgument ba).getType! ctx₁.raw := by + induction h with + | Nil => rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + obtain ⟨opType, rt, ops, succ, regs, props, k₁, k₂, k₃, k₄, hcreate⟩ := hex + rw [ValuePtr.getType!_WfRewriter_createOp hcreate]; exact ih + /-- Fuse a left-fold of array `map`s into one `map` of left-folds. -/ theorem List.foldl_arrayMap_fusion {α β : Type} (l : List β) (g : β → α → α) (arr : Array α) : l.foldl (fun a b => a.map (fun x => g b x)) arr @@ -3266,6 +3350,84 @@ theorem RewrittenAt.of_fromLocalRewrite have hopS2 : op.InBounds s₂.ctx.raw := by have := hbnd (GenericPtr.operation op); grind have hopParentS2 : (op.get! s₂.ctx.raw).parent = some block := (BlockPtr.operationList.mem hopS2).mpr (by rw [hblockListS2]; simp [Array.mem_append]) + -- === Block-argument count/type frame (clause 7). The four stages — created ops, insert fold, + -- replace fold, final `eraseOp` — each preserve argument counts and types. Counts are preserved + -- unconditionally; argument types need only the block argument's in-bounds witness for the `eraseOp` + -- stage. === + have hNumArgs : ∀ (bl : BlockPtr), + bl.getNumArguments! rewriter'.ctx.raw = bl.getNumArguments! rewriter.ctx.raw := by + intro bl + have hCre : bl.getNumArguments! newCtxPat.raw = bl.getNumArguments! rewriter.ctx.raw := + hCreated.getNumArguments_eq bl + have hIns : bl.getNumArguments! s₁.ctx.raw = bl.getNumArguments! newCtxPat.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + bl.getNumArguments! b.ctx.raw = bl.getNumArguments! newCtxPat.raw) + (fun b a b' hh => by + have := PatternRewriter.insertOp_getNumArguments (bl := bl) hh + constructor <;> intro hb <;> grind) hfold1 + exact h.mpr rfl + have hRep : bl.getNumArguments! s₂.ctx.raw = bl.getNumArguments! s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + bl.getNumArguments! b.ctx.raw = bl.getNumArguments! s₁.ctx.raw) + (fun b a b' hh => by + have hst : bl.getNumArguments! b'.ctx.raw = bl.getNumArguments! b.ctx.raw := by + simp only [Option.some.injEq] at hh; subst hh + exact PatternRewriter.replaceValue_getNumArguments + constructor <;> intro hb <;> grind) hfold2 + exact h.mpr rfl + have hErase : bl.getNumArguments! rewriter'.ctx.raw = bl.getNumArguments! s₂.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact BlockPtr.getNumArguments!_wfRewriter_eraseOp + rw [hErase, hRep, hIns, hCre] + have hArgTypes : ∀ (bl : BlockPtr), bl.InBounds rewriter.ctx.raw → + ∀ i, i < bl.getNumArguments! rewriter.ctx.raw → + (bl.getArgument i : ValuePtr).getType! rewriter'.ctx.raw = + (bl.getArgument i : ValuePtr).getType! rewriter.ctx.raw := by + intro bl blIn i hi + -- Work with the explicit block-argument value `blockArgument ⟨bl, i⟩` (`getArgument i` is `⟨bl, i⟩`). + have hv : (bl.getArgument i : ValuePtr) = ValuePtr.blockArgument ⟨bl, i⟩ := by + rw [BlockPtr.getArgument_def] + rw [hv] + have hCre : (ValuePtr.blockArgument ⟨bl, i⟩).getType! newCtxPat.raw + = (ValuePtr.blockArgument ⟨bl, i⟩).getType! rewriter.ctx.raw := + hCreated.getType_blockArgument_eq ⟨bl, i⟩ + have hIns : (ValuePtr.blockArgument ⟨bl, i⟩).getType! s₁.ctx.raw + = (ValuePtr.blockArgument ⟨bl, i⟩).getType! newCtxPat.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + (ValuePtr.blockArgument ⟨bl, i⟩).getType! b.ctx.raw + = (ValuePtr.blockArgument ⟨bl, i⟩).getType! newCtxPat.raw) + (fun b a b' hh => by + have := PatternRewriter.insertOp_getType (v := (ValuePtr.blockArgument ⟨bl, i⟩)) hh + constructor <;> intro hb <;> grind) hfold1 + exact h.mpr rfl + have hRep : (ValuePtr.blockArgument ⟨bl, i⟩).getType! s₂.ctx.raw + = (ValuePtr.blockArgument ⟨bl, i⟩).getType! s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + (ValuePtr.blockArgument ⟨bl, i⟩).getType! b.ctx.raw + = (ValuePtr.blockArgument ⟨bl, i⟩).getType! s₁.ctx.raw) + (fun b a b' hh => by + have hst : (ValuePtr.blockArgument ⟨bl, i⟩).getType! b'.ctx.raw + = (ValuePtr.blockArgument ⟨bl, i⟩).getType! b.ctx.raw := by + simp only [Option.some.injEq] at hh; subst hh + exact PatternRewriter.replaceValue_getType + constructor <;> intro hb <;> grind) hfold2 + exact h.mpr rfl + -- `eraseOp` preserves the type of any *in-bounds* value; the `i`-th argument of the surviving + -- block `bl` is in bounds of `rewriter'.ctx` because the count is preserved. + have hblRew' : bl.InBounds rewriter'.ctx.raw := + hSurviveBlock bl (hCreated.inBounds_mono (GenericPtr.block bl) (by grind)) + have hvIn : (ValuePtr.blockArgument ⟨bl, i⟩).InBounds rewriter'.ctx.raw := by + have hlt : i < bl.getNumArguments! rewriter'.ctx.raw := by rw [hNumArgs bl]; exact hi + grind [BlockArgumentPtr.inBounds_def, BlockPtr.getNumArguments!_eq_getNumArguments] + have hErase : (ValuePtr.blockArgument ⟨bl, i⟩).getType! rewriter'.ctx.raw + = (ValuePtr.blockArgument ⟨bl, i⟩).getType! s₂.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact ValuePtr.getType!_wfRewriter_eraseOp (PatternRewriter.eraseOp_ctx_eq herase ▸ hvIn) + rw [hErase, hRep, hIns, hCre] refine ⟨pre, post, blockIn, blockIn', ?_⟩ exact { -- Block-list shape: discharged for the source by the split lemma. @@ -3499,8 +3661,12 @@ theorem RewrittenAt.of_fromLocalRewrite -- `block` (the SSA-validity condition: results of `newOps` are defined within the span, forwarded -- values are in scope throughout the block); discharged from a pattern obligation. newValuesDominate := by sorry - -- TODO(PR 9, keystone): operation-list edits leave block-argument lists untouched. - blockArgsPreserved := by sorry + -- Operation-list edits leave block-argument counts and types untouched (the chain `hNumArgs` / + -- `hArgTypes` established above). The full `arguments` record is not preserved — argument + -- `firstUse` heads move as uses are redirected/erased — but count and type are, which is all the + -- block-argument frame consequences (`numArgsEq`/`argType_eq`/`getArguments!_eq`) need. + blockNumArgsPreserved := fun bl _ => hNumArgs bl + blockArgTypesPreserved := hArgTypes -- TODO(PR 9, keystone): op-list edits inside `block` leave the CFG unchanged, so block-level -- dominance agrees across the two contexts. blockDominatesPreserved := by sorry From 3e1d6312000600ae5ad54a05fc4b72da42ead8cf Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 03:10:23 +0200 Subject: [PATCH 19/31] One less sorry --- Veir/PatternRewriter/Soundness.lean | 148 +++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 2 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index e5b69303f..049e08c7c 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -2926,6 +2926,30 @@ theorem PatternRewriter.insertOp_getOperands {b b' : PatternRewriter OpCode} simp only [Option.some.injEq] at h; subst h exact OperationPtr.getOperands!_wfRewriter_insertOp hwf +/-- `PatternRewriter.insertOp` leaves every operation's region count unchanged. -/ +theorem PatternRewriter.insertOp_getNumRegions {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {o : OperationPtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + o.getNumRegions! b'.ctx.raw = o.getNumRegions! b.ctx.raw := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact OperationPtr.getNumRegions!_wfRewriter_insertOp hwf + +/-- `PatternRewriter.insertOp` leaves every operation's region pointers unchanged. -/ +theorem PatternRewriter.insertOp_getRegion {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {o : OperationPtr} {idx : Nat} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + o.getRegion! b'.ctx.raw idx = o.getRegion! b.ctx.raw idx := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact OperationPtr.getRegion!_wfRewriter_insertOp hwf + /-- `PatternRewriter.replaceValue` frames every operation's intrinsic data (it only redirects operands). -/ theorem PatternRewriter.replaceValue_sameIntrinsic {b : PatternRewriter OpCode} @@ -2952,6 +2976,39 @@ theorem PatternRewriter.replaceValue_getOperands {b : PatternRewriter OpCode} rw [hctx] exact OperationPtr.getOperands!_WfRewriter_replaceValue hin +/-- `PatternRewriter.replaceValue` leaves every operation's region count unchanged. -/ +theorem PatternRewriter.replaceValue_getNumRegions {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {o : OperationPtr} : + o.getNumRegions! (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw = + o.getNumRegions! b.ctx.raw := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx]; exact OperationPtr.getNumRegions!_WfRewriter_replaceValue + +/-- `PatternRewriter.replaceValue` leaves every operation's region pointers unchanged. -/ +theorem PatternRewriter.replaceValue_getRegion {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {o : OperationPtr} {idx : Nat} : + o.getRegion! (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw idx = + o.getRegion! b.ctx.raw idx := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx]; exact OperationPtr.getRegion!_WfRewriter_replaceValue + +/-- An operation's region list is determined by its region count and region pointers, so equal counts +plus equal pointers (at every index) give equal region lists across two contexts. -/ +theorem OperationPtr.regions_eq_of {o : OperationPtr} {ctx ctx' : IRContext OpCode} + (hsize : o.getNumRegions! ctx = o.getNumRegions! ctx') + (helem : ∀ idx, o.getRegion! ctx idx = o.getRegion! ctx' idx) : + (o.get! ctx).regions = (o.get! ctx').regions := by + apply Array.ext + · simpa only [OperationPtr.getNumRegions!] using hsize + · intro i hi hi' + have h := helem i + simp only [OperationPtr.getRegion!] at h + rwa [getElem!_pos _ i hi, getElem!_pos _ i hi'] at h + /-- A `WithCreatedOps` chain frames a survivor's intrinsic data (it only creates fresh ops). -/ theorem WfIRContext.WithCreatedOps.sameIntrinsic {ctx₁ ctx₂ : WfIRContext OpCode} (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {o : OperationPtr} (oIn : o.InBounds ctx₁.raw) : @@ -2980,6 +3037,33 @@ theorem WfIRContext.WithCreatedOps.getOperands_eq {ctx₁ ctx₂ : WfIRContext O rw [OperationPtr.getOperands!_WfRewriter_createOp hcreate, if_neg (by grind)] exact ih oIn +/-- A `WithCreatedOps` chain frames a survivor's region count (it only creates fresh ops). -/ +theorem WfIRContext.WithCreatedOps.getNumRegions_eq {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {o : OperationPtr} (oIn : o.InBounds ctx₁.raw) : + o.getNumRegions! ctx₂.raw = o.getNumRegions! ctx₁.raw := by + induction h with + | Nil => rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + obtain ⟨opType, rt, ops, succ, regs, props, k₁, k₂, k₃, k₄, hcreate⟩ := hex + have ho2 : o.InBounds ctx₂.raw := by + have := hwco.inBounds_mono (GenericPtr.operation o) (by grind); grind + rw [OperationPtr.getNumRegions!_WfRewriter_createOp hcreate, if_neg (by grind)] + exact ih oIn + +/-- A `WithCreatedOps` chain frames a survivor's region pointers (it only creates fresh ops). -/ +theorem WfIRContext.WithCreatedOps.getRegion_eq {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {o : OperationPtr} (oIn : o.InBounds ctx₁.raw) + (idx : Nat) : + o.getRegion! ctx₂.raw idx = o.getRegion! ctx₁.raw idx := by + induction h with + | Nil => rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + obtain ⟨opType, rt, ops, succ, regs, props, k₁, k₂, k₃, k₄, hcreate⟩ := hex + have ho2 : o.InBounds ctx₂.raw := by + have := hwco.inBounds_mono (GenericPtr.operation o) (by grind); grind + rw [OperationPtr.getRegion!_WfRewriter_createOp hcreate, dif_neg (by grind)] + exact ih oIn + /-! ### Block-argument count/type frame across the rewrite stages. The rewrite never adds, removes, or retypes block arguments (it only edits operation lists and @@ -3670,8 +3754,68 @@ theorem RewrittenAt.of_fromLocalRewrite -- TODO(PR 9, keystone): op-list edits inside `block` leave the CFG unchanged, so block-level -- dominance agrees across the two contexts. blockDominatesPreserved := by sorry - -- TODO(PR 9, keystone): op-list edits leave survivors' region lists untouched. - opRegionsPreserved := by sorry + -- Op-list edits (create / insert / replace-value / erase) never touch a survivor's region list: + -- chain the per-stage `getNumRegions!`/`getRegion!` frame facts and reassemble the array. + opRegionsPreserved := by + intro o oIn hne + have hoNewCtxPat : o.InBounds newCtxPat.raw := + hCreated.inBounds_mono (GenericPtr.operation o) (by grind) + have oIn' : o.InBounds rewriter'.ctx.raw := hSurviveOp o hne hoNewCtxPat + have hoErase := PatternRewriter.eraseOp_ctx_eq herase ▸ oIn' + -- (1) Region counts are framed across the whole pipeline. + have hNum : o.getNumRegions! rewriter'.ctx.raw = o.getNumRegions! rewriter.ctx.raw := by + have hcre : o.getNumRegions! newCtxPat.raw = o.getNumRegions! rewriter.ctx.raw := + hCreated.getNumRegions_eq oIn + have hins : o.getNumRegions! s₁.ctx.raw = o.getNumRegions! newCtxPat.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + o.getNumRegions! b.ctx.raw = o.getNumRegions! newCtxPat.raw) + (fun b a b' hh => by + have := PatternRewriter.insertOp_getNumRegions (o := o) hh + constructor <;> intro hb <;> grind) hfold1 + exact h.mpr rfl + have hrep : o.getNumRegions! s₂.ctx.raw = o.getNumRegions! s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + o.getNumRegions! b.ctx.raw = o.getNumRegions! s₁.ctx.raw) + (fun b a b' hh => by + have hst : o.getNumRegions! b'.ctx.raw = o.getNumRegions! b.ctx.raw := by + simp only [Option.some.injEq] at hh; subst hh + exact PatternRewriter.replaceValue_getNumRegions + constructor <;> intro hb <;> grind) hfold2 + exact h.mpr rfl + have hers : o.getNumRegions! rewriter'.ctx.raw = o.getNumRegions! s₂.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact OperationPtr.getNumRegions!_wfRewriter_eraseOp hoErase + exact hers.trans (hrep.trans (hins.trans hcre)) + -- (2) Region pointers are framed across the whole pipeline, index by index. + have hReg : ∀ idx, o.getRegion! rewriter'.ctx.raw idx = o.getRegion! rewriter.ctx.raw idx := by + intro idx + have hcre : o.getRegion! newCtxPat.raw idx = o.getRegion! rewriter.ctx.raw idx := + hCreated.getRegion_eq oIn idx + have hins : o.getRegion! s₁.ctx.raw idx = o.getRegion! newCtxPat.raw idx := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + o.getRegion! b.ctx.raw idx = o.getRegion! newCtxPat.raw idx) + (fun b a b' hh => by + have := PatternRewriter.insertOp_getRegion (o := o) (idx := idx) hh + constructor <;> intro hb <;> grind) hfold1 + exact h.mpr rfl + have hrep : o.getRegion! s₂.ctx.raw idx = o.getRegion! s₁.ctx.raw idx := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + o.getRegion! b.ctx.raw idx = o.getRegion! s₁.ctx.raw idx) + (fun b a b' hh => by + have hst : o.getRegion! b'.ctx.raw idx = o.getRegion! b.ctx.raw idx := by + simp only [Option.some.injEq] at hh; subst hh + exact PatternRewriter.replaceValue_getRegion + constructor <;> intro hb <;> grind) hfold2 + exact h.mpr rfl + have hers : o.getRegion! rewriter'.ctx.raw idx = o.getRegion! s₂.ctx.raw idx := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact OperationPtr.getRegion!_wfRewriter_eraseOp hoErase + exact hers.trans (hrep.trans (hins.trans hcre)) + exact OperationPtr.regions_eq_of hNum hReg -- TODO(PR 9, keystone): op-list edits leave region entry blocks untouched. regionFirstBlockPreserved := by sorry -- `op` is not a function: it has no regions, so in particular not exactly one. From 5af40296574a3b10aca384ddd27752010f2fba61 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 03:15:07 +0200 Subject: [PATCH 20/31] One less sorry --- Veir/PatternRewriter/Semantics.lean | 55 ++++++++++++++++++ Veir/PatternRewriter/Soundness.lean | 90 ++++++++++++++++++++++++----- 2 files changed, 130 insertions(+), 15 deletions(-) diff --git a/Veir/PatternRewriter/Semantics.lean b/Veir/PatternRewriter/Semantics.lean index 4b7942b50..15f7f316d 100644 --- a/Veir/PatternRewriter/Semantics.lean +++ b/Veir/PatternRewriter/Semantics.lean @@ -186,3 +186,58 @@ def LocalRewritePattern.PreservesSemantics ∃ targetValues, newValues.mapM (newState'.variables.getVar? ·) = some targetValues ∧ sourceValues ⊒ targetValues + +/-- +Applying the pattern through the standard driver (`RewritePattern.fromLocalRewrite`) preserves +dominance-wellformedness: rewriting a `Dom` context yields a `Dom` context. This is the structural +counterpart of `PreservesSemantics`'s `ctxDom` hypothesis — where that *assumes* source dominance, this +*propagates* it across the op-list surgery the driver performs (insert `newOps` before `op`, redirect +`op`'s results onto `newValues`, erase `op`). That surgery does not preserve dominance for an arbitrary +pattern, so each concrete pattern must discharge this obligation (typically from `ReturnValuesDominate` +and the SSA structure of its `newOps`). -/ +def LocalRewritePattern.RewritePreservesDom (pattern : LocalRewritePattern OpCode) : Prop := + ∀ (rewriter : PatternRewriter OpCode) (op : OperationPtr) + (opInBounds : op.InBounds rewriter.ctx.raw) (rewriter' : PatternRewriter OpCode), + RewritePattern.fromLocalRewrite pattern rewriter op opInBounds = some rewriter' → + rewriter.ctx.Dom → rewriter'.ctx.Dom + +/-- +Applying the pattern through the standard driver (`RewritePattern.fromLocalRewrite`) preserves +verification: rewriting a `Verified` context yields a `Verified` context. Like `RewritePreservesDom`, +this propagates a source well-formedness invariant across the driver's op-list surgery, and must be +discharged per concrete pattern. -/ +def LocalRewritePattern.RewritePreservesVerified (pattern : LocalRewritePattern OpCode) : Prop := + ∀ (rewriter : PatternRewriter OpCode) (op : OperationPtr) + (opInBounds : op.InBounds rewriter.ctx.raw) (rewriter' : PatternRewriter OpCode), + RewritePattern.fromLocalRewrite pattern rewriter op opInBounds = some rewriter' → + rewriter.ctx.Verified → rewriter'.ctx.Verified + +/-- +The bundle of correctness obligations a `LocalRewritePattern` must satisfy for the soundness +results (notably `RewrittenAt.of_fromLocalRewrite` and the soundness lift built on it) to apply. +Bundling them into a single structure avoids threading every obligation as a separate argument. +Later fields may refer to earlier ones, so `preservesSemantics` reuses the `Return*` fields it +depends on. +-/ +structure LocalRewritePattern.Valid (pattern : LocalRewritePattern OpCode) : Prop where + /-- The pattern returns the input context whenever there are no errors and no match. -/ + returnsCtxNoChanges : pattern.ReturnsCtxNoChanges + /-- On a match, the output context is only modified by creating new operations. -/ + returnCtxChanges : pattern.ReturnCtxChanges + /-- On a match, the returned operations are exactly the newly created ones. -/ + returnOps : pattern.ReturnOps + /-- The pattern returns one value per result of the matched operation. -/ + returnValues : pattern.ReturnValues + /-- All returned values are in bounds of the new context. -/ + returnValuesInBounds : pattern.ReturnValuesInBounds + /-- No returned value is one of `op`'s own result pointers. -/ + returnValuesNotOwnResults : pattern.ReturnValuesNotOwnResults + /-- Every forwarded pre-existing returned value dominates the point before `op`. -/ + returnValuesDominate : pattern.ReturnValuesDominate + /-- Interpreting the matched operation is refined by interpreting the new operations. -/ + preservesSemantics : + pattern.PreservesSemantics returnOps returnCtxChanges returnValuesInBounds returnValues + /-- The driver-applied rewrite preserves dominance-wellformedness. -/ + rewritePreservesDom : pattern.RewritePreservesDom + /-- The driver-applied rewrite preserves verification. -/ + rewritePreservesVerified : pattern.RewritePreservesVerified diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 049e08c7c..ab46ea89c 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -2950,6 +2950,18 @@ theorem PatternRewriter.insertOp_getRegion {b b' : PatternRewriter OpCode} simp only [Option.some.injEq] at h; subst h exact OperationPtr.getRegion!_wfRewriter_insertOp hwf +/-- `PatternRewriter.insertOp` leaves every region's entry block unchanged. -/ +theorem PatternRewriter.insertOp_firstBlock {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {r : RegionPtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + (r.get! b'.ctx.raw).firstBlock = (r.get! b.ctx.raw).firstBlock := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact RegionPtr.firstBlock!_wfRewriter_insertOp hwf + /-- `PatternRewriter.replaceValue` frames every operation's intrinsic data (it only redirects operands). -/ theorem PatternRewriter.replaceValue_sameIntrinsic {b : PatternRewriter OpCode} @@ -2996,6 +3008,16 @@ theorem PatternRewriter.replaceValue_getRegion {b : PatternRewriter OpCode} simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] rw [hctx]; exact OperationPtr.getRegion!_WfRewriter_replaceValue +/-- `PatternRewriter.replaceValue` leaves every region's entry block unchanged. -/ +theorem PatternRewriter.replaceValue_firstBlock {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {r : RegionPtr} : + (r.get! (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw).firstBlock = + (r.get! b.ctx.raw).firstBlock := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx]; exact RegionPtr.firstBlock!_WfRewriter_replaceValue + /-- An operation's region list is determined by its region count and region pointers, so equal counts plus equal pointers (at every index) give equal region lists across two contexts. -/ theorem OperationPtr.regions_eq_of {o : OperationPtr} {ctx ctx' : IRContext OpCode} @@ -3064,6 +3086,17 @@ theorem WfIRContext.WithCreatedOps.getRegion_eq {ctx₁ ctx₂ : WfIRContext OpC rw [OperationPtr.getRegion!_WfRewriter_createOp hcreate, dif_neg (by grind)] exact ih oIn +/-- A `WithCreatedOps` chain frames every region's entry block (it only creates fresh ops). -/ +theorem WfIRContext.WithCreatedOps.firstBlock_eq {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {r : RegionPtr} : + (r.get! ctx₂.raw).firstBlock = (r.get! ctx₁.raw).firstBlock := by + induction h with + | Nil => rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + obtain ⟨opType, rt, ops, succ, regs, props, k₁, k₂, k₃, k₄, hcreate⟩ := hex + rw [RegionPtr.firstBlock!_WfRewriter_createOp hcreate] + exact ih + /-! ### Block-argument count/type frame across the rewrite stages. The rewrite never adds, removes, or retypes block arguments (it only edits operation lists and @@ -3291,14 +3324,10 @@ The remaining structural fields are discharged from the keystone fold decomposit -/ theorem RewrittenAt.of_fromLocalRewrite {pattern : LocalRewritePattern OpCode} - (hReturnOps : pattern.ReturnOps) - (hReturnCtxChanges : pattern.ReturnCtxChanges) - (hReturnValuesInBounds : pattern.ReturnValuesInBounds) - (hReturnValues : pattern.ReturnValues) - (hReturnValuesNotOwnResults : pattern.ReturnValuesNotOwnResults) - (hReturnValuesDominate : pattern.ReturnValuesDominate) + (hValid : pattern.Valid) {rewriter rewriter' : PatternRewriter OpCode} (hSrcDom : rewriter.ctx.Dom) + (hSrcVerif : rewriter.ctx.Verified) {op : OperationPtr} (opInBounds : op.InBounds rewriter.ctx.raw) {block : BlockPtr} (hOpParent : (op.get! rewriter.ctx.raw).parent = some block) (hOpRegions : op.getNumRegions! rewriter.ctx.raw = 0) @@ -3309,6 +3338,9 @@ theorem RewrittenAt.of_fromLocalRewrite (blockIn : block.InBounds rewriter.ctx.raw) (blockIn' : block.InBounds rewriter'.ctx.raw), RewrittenAt rewriter.ctx op newOps newValues rewriter'.ctx opInBounds block pre post blockIn blockIn' := by + obtain ⟨-, hReturnCtxChanges, hReturnOps, hReturnValues, hReturnValuesInBounds, + hReturnValuesNotOwnResults, hReturnValuesDominate, -, hRewritePreservesDom, + hRewritePreservesVerified⟩ := hValid -- `block` is in bounds of the source context: it is the parent of the in-bounds `op`. have blockIn : block.InBounds rewriter.ctx.raw := by have := rewriter.ctx.wellFormed.inBounds; grind @@ -3321,6 +3353,9 @@ theorem RewrittenAt.of_fromLocalRewrite -- `hdriver` reads: insert every `newOp` before `op`, redirect each result to `newValues`, erase -- `op` — the middle operands-collection loop is dead (its result is discarded). Every `RewrittenAt` -- field below is a fact about the resulting `rewriter'.ctx` read off this fold. + -- Keep the un-reduced driver equation for the well-formedness obligations (`newCtxDom`/`newCtxVerif`), + -- which are stated against `RewritePattern.fromLocalRewrite`; `hdriver` itself is reduced below. + have hdriverOrig := hdriver unfold RewritePattern.fromLocalRewrite at hdriver rw [hpat] at hdriver simp only [bind_pure_comp, Array.forIn_yield_eq_foldlM, id_map'] at hdriver @@ -3734,13 +3769,12 @@ theorem RewrittenAt.of_fromLocalRewrite -- Blocks stay in bounds: into `newCtxPat`, then the folds/erase (erase removes only `op`). blocksInBounds := fun b hb => hSurviveBlock b (hCreated.inBounds_mono (GenericPtr.block b) (by grind)) - -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS. Dominance/verification are not preserved by arbitrary - -- `insertOp`s, so this requires source `rewriter.ctx.Dom` plus a pattern obligation that the rewrite - -- produces a dominance-well-formed result (mirroring `PreservesSemantics`'s `ctxDom`). - newCtxDom := by sorry - -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS, as `newCtxDom` (source `rewriter.ctx.Verified` + pattern - -- obligation that the result is verified). - newCtxVerif := by sorry + -- Source dominance-wellformedness is propagated across the rewrite by the pattern obligation + -- `RewritePreservesDom` (the driver-level counterpart of `PreservesSemantics`'s `ctxDom`). + newCtxDom := hRewritePreservesDom rewriter op opInBounds rewriter' hdriverOrig hSrcDom + -- As `newCtxDom`, via the source `rewriter.ctx.Verified` and the `RewritePreservesVerified` + -- pattern obligation. + newCtxVerif := hRewritePreservesVerified rewriter op opInBounds rewriter' hdriverOrig hSrcVerif -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS. Produced values must dominate the post-insertion point in -- `block` (the SSA-validity condition: results of `newOps` are defined within the span, forwarded -- values are in scope throughout the block); discharged from a pattern obligation. @@ -3816,8 +3850,34 @@ theorem RewrittenAt.of_fromLocalRewrite exact OperationPtr.getRegion!_wfRewriter_eraseOp hoErase exact hers.trans (hrep.trans (hins.trans hcre)) exact OperationPtr.regions_eq_of hNum hReg - -- TODO(PR 9, keystone): op-list edits leave region entry blocks untouched. - regionFirstBlockPreserved := by sorry + -- Op-list edits (create / insert / replace-value / erase) never touch a region's entry block: + -- chain the per-stage `firstBlock` frame facts across the pipeline. + regionFirstBlockPreserved := by + intro r _ + have hcre : (r.get! newCtxPat.raw).firstBlock = (r.get! rewriter.ctx.raw).firstBlock := + hCreated.firstBlock_eq + have hins : (r.get! s₁.ctx.raw).firstBlock = (r.get! newCtxPat.raw).firstBlock := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + (r.get! b.ctx.raw).firstBlock = (r.get! newCtxPat.raw).firstBlock) + (fun b a b' hh => by + have := PatternRewriter.insertOp_firstBlock (r := r) hh + constructor <;> intro hb <;> grind) hfold1 + exact h.mpr rfl + have hrep : (r.get! s₂.ctx.raw).firstBlock = (r.get! s₁.ctx.raw).firstBlock := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + (r.get! b.ctx.raw).firstBlock = (r.get! s₁.ctx.raw).firstBlock) + (fun b a b' hh => by + have hst : (r.get! b'.ctx.raw).firstBlock = (r.get! b.ctx.raw).firstBlock := by + simp only [Option.some.injEq] at hh; subst hh + exact PatternRewriter.replaceValue_firstBlock + constructor <;> intro hb <;> grind) hfold2 + exact h.mpr rfl + have hers : (r.get! rewriter'.ctx.raw).firstBlock = (r.get! s₂.ctx.raw).firstBlock := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact RegionPtr.firstBlock!_wfRewriter_eraseOp + exact hers.trans (hrep.trans (hins.trans hcre)) -- `op` is not a function: it has no regions, so in particular not exactly one. opNotFunction := by simp [hOpRegions] } From a5a8d454b068450aad6ad516f18c3443d7abf47e Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 03:33:20 +0200 Subject: [PATCH 21/31] --- Veir/PatternRewriter/Semantics.lean | 25 +++++++++++++++++++++++++ Veir/PatternRewriter/Soundness.lean | 12 +++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Veir/PatternRewriter/Semantics.lean b/Veir/PatternRewriter/Semantics.lean index 15f7f316d..033fc929b 100644 --- a/Veir/PatternRewriter/Semantics.lean +++ b/Veir/PatternRewriter/Semantics.lean @@ -212,6 +212,29 @@ def LocalRewritePattern.RewritePreservesVerified (pattern : LocalRewritePattern RewritePattern.fromLocalRewrite pattern rewriter op opInBounds = some rewriter' → rewriter.ctx.Verified → rewriter'.ctx.Verified +/-- +Applying the pattern through the standard driver (`RewritePattern.fromLocalRewrite`) leaves every +produced value dominating the post-insertion point in the matched operation's block: the end of the +inserted `newOps` span (`afterLast newOps (atStart! block)`) in the rewritten context. This is the +SSA-validity condition on produced values — fresh results of inserted `newOps` are defined within the +span, and forwarded pre-existing values are in scope throughout `block`. + +It is the rewritten-context (`rewriter'.ctx`) counterpart of `ReturnValuesDominate`, which states the +source-context (`rewriter.ctx`) version (each forwarded value dominates `before op`). Like +`RewritePreservesDom`/`RewritePreservesVerified`, it is a driver-level fact each concrete pattern must +discharge — typically from `ReturnValuesDominate` together with the SSA structure of its `newOps`. -/ +def LocalRewritePattern.RewriteNewValuesDominate (pattern : LocalRewritePattern OpCode) : Prop := + ∀ (rewriter : PatternRewriter OpCode) (op : OperationPtr) + (opInBounds : op.InBounds rewriter.ctx.raw) (rewriter' : PatternRewriter OpCode), + RewritePattern.fromLocalRewrite pattern rewriter op opInBounds = some rewriter' → + ∀ (block : BlockPtr) (newCtx : WfIRContext OpCode) + (newOps : Array OperationPtr) (newValues : Array ValuePtr), + (op.get! rewriter.ctx.raw).parent = some block → + pattern rewriter.ctx op = some (newCtx, some (newOps, newValues)) → + ∀ v ∈ newValues, + v.dominatesIp (InsertPoint.afterLast newOps.toList rewriter'.ctx.raw + (InsertPoint.atStart! block rewriter'.ctx.raw)) rewriter'.ctx + /-- The bundle of correctness obligations a `LocalRewritePattern` must satisfy for the soundness results (notably `RewrittenAt.of_fromLocalRewrite` and the soundness lift built on it) to apply. @@ -241,3 +264,5 @@ structure LocalRewritePattern.Valid (pattern : LocalRewritePattern OpCode) : Pro rewritePreservesDom : pattern.RewritePreservesDom /-- The driver-applied rewrite preserves verification. -/ rewritePreservesVerified : pattern.RewritePreservesVerified + /-- Every produced value dominates the post-insertion point in the matched operation's block. -/ + rewriteNewValuesDominate : pattern.RewriteNewValuesDominate diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index ab46ea89c..406f1125f 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -3340,7 +3340,7 @@ theorem RewrittenAt.of_fromLocalRewrite block pre post blockIn blockIn' := by obtain ⟨-, hReturnCtxChanges, hReturnOps, hReturnValues, hReturnValuesInBounds, hReturnValuesNotOwnResults, hReturnValuesDominate, -, hRewritePreservesDom, - hRewritePreservesVerified⟩ := hValid + hRewritePreservesVerified, hRewriteNewValuesDominate⟩ := hValid -- `block` is in bounds of the source context: it is the parent of the in-bounds `op`. have blockIn : block.InBounds rewriter.ctx.raw := by have := rewriter.ctx.wellFormed.inBounds; grind @@ -3775,10 +3775,12 @@ theorem RewrittenAt.of_fromLocalRewrite -- As `newCtxDom`, via the source `rewriter.ctx.Verified` and the `RewritePreservesVerified` -- pattern obligation. newCtxVerif := hRewritePreservesVerified rewriter op opInBounds rewriter' hdriverOrig hSrcVerif - -- TODO(PR 9): NEEDS EXTRA HYPOTHESIS. Produced values must dominate the post-insertion point in - -- `block` (the SSA-validity condition: results of `newOps` are defined within the span, forwarded - -- values are in scope throughout the block); discharged from a pattern obligation. - newValuesDominate := by sorry + -- Produced values dominate the post-insertion point in `block` (the SSA-validity condition: + -- results of `newOps` are defined within the span, forwarded values are in scope throughout the + -- block); discharged from the driver-level `RewriteNewValuesDominate` pattern obligation. + newValuesDominate := + hRewriteNewValuesDominate rewriter op opInBounds rewriter' hdriverOrig block newCtxPat + newOps newValues hOpParent hpat -- Operation-list edits leave block-argument counts and types untouched (the chain `hNumArgs` / -- `hArgTypes` established above). The full `arguments` record is not preserved — argument -- `firstUse` heads move as uses are redirected/erased — but count and type are, which is all the From f45dc66aaa5bb2e5739751f5a748061db16108fd Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 03:43:40 +0200 Subject: [PATCH 22/31] Last sorry --- Veir/PatternRewriter/Semantics.lean | 19 +++++++++++++++++++ Veir/PatternRewriter/Soundness.lean | 12 +++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/Veir/PatternRewriter/Semantics.lean b/Veir/PatternRewriter/Semantics.lean index 033fc929b..d6bc46ed3 100644 --- a/Veir/PatternRewriter/Semantics.lean +++ b/Veir/PatternRewriter/Semantics.lean @@ -235,6 +235,23 @@ def LocalRewritePattern.RewriteNewValuesDominate (pattern : LocalRewritePattern v.dominatesIp (InsertPoint.afterLast newOps.toList rewriter'.ctx.raw (InsertPoint.atStart! block rewriter'.ctx.raw)) rewriter'.ctx +/-- +Applying the pattern through the standard driver (`RewritePattern.fromLocalRewrite`) preserves +block-level dominance: any two in-bounds blocks dominate each other in the rewritten context exactly +when they do in the source context. The driver edits only the operation list of the matched +operation's block (insert `newOps` before `op`, redirect `op`'s results, erase `op`); it never adds +or removes a block, nor alters region structure. That op-list surgery does not preserve the block +CFG for an *arbitrary* pattern — replacing a block's terminator can re-route its successors — so, like +`RewritePreservesDom`/`RewritePreservesVerified`, each concrete pattern must discharge this obligation +(typically because its `newOps` reproduce the matched operation's control-flow behaviour, leaving every +block's successor edges intact). -/ +def LocalRewritePattern.RewritePreservesBlockDominance (pattern : LocalRewritePattern OpCode) : Prop := + ∀ (rewriter : PatternRewriter OpCode) (op : OperationPtr) + (opInBounds : op.InBounds rewriter.ctx.raw) (rewriter' : PatternRewriter OpCode), + RewritePattern.fromLocalRewrite pattern rewriter op opInBounds = some rewriter' → + ∀ (b₁ b₂ : BlockPtr), b₁.InBounds rewriter.ctx.raw → b₂.InBounds rewriter.ctx.raw → + (b₁.dominates b₂ rewriter'.ctx ↔ b₁.dominates b₂ rewriter.ctx) + /-- The bundle of correctness obligations a `LocalRewritePattern` must satisfy for the soundness results (notably `RewrittenAt.of_fromLocalRewrite` and the soundness lift built on it) to apply. @@ -266,3 +283,5 @@ structure LocalRewritePattern.Valid (pattern : LocalRewritePattern OpCode) : Pro rewritePreservesVerified : pattern.RewritePreservesVerified /-- Every produced value dominates the post-insertion point in the matched operation's block. -/ rewriteNewValuesDominate : pattern.RewriteNewValuesDominate + /-- The driver-applied rewrite preserves block-level dominance. -/ + rewritePreservesBlockDominance : pattern.RewritePreservesBlockDominance diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 406f1125f..02e64cc3f 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -3310,7 +3310,6 @@ theorem rewriteMapping_applyToArray_eq_map {ctx newCtx : WfIRContext OpCode} simp only [ValueMapping.applyToArray, Array.getElem_map, Array.getElem_attach, rewriteMapping] split <;> grind -set_option warn.sorry false in /-- **PR 9 — bridge from the concrete driver.** When `fromLocalRewrite` runs the rewrite branch for a matched, in-bounds, region-free `op` that lives inside a block, and the pattern satisfies the four @@ -3340,7 +3339,7 @@ theorem RewrittenAt.of_fromLocalRewrite block pre post blockIn blockIn' := by obtain ⟨-, hReturnCtxChanges, hReturnOps, hReturnValues, hReturnValuesInBounds, hReturnValuesNotOwnResults, hReturnValuesDominate, -, hRewritePreservesDom, - hRewritePreservesVerified, hRewriteNewValuesDominate⟩ := hValid + hRewritePreservesVerified, hRewriteNewValuesDominate, hRewritePreservesBlockDominance⟩ := hValid -- `block` is in bounds of the source context: it is the parent of the in-bounds `op`. have blockIn : block.InBounds rewriter.ctx.raw := by have := rewriter.ctx.wellFormed.inBounds; grind @@ -3787,9 +3786,12 @@ theorem RewrittenAt.of_fromLocalRewrite -- block-argument frame consequences (`numArgsEq`/`argType_eq`/`getArguments!_eq`) need. blockNumArgsPreserved := fun bl _ => hNumArgs bl blockArgTypesPreserved := hArgTypes - -- TODO(PR 9, keystone): op-list edits inside `block` leave the CFG unchanged, so block-level - -- dominance agrees across the two contexts. - blockDominatesPreserved := by sorry + -- Op-list edits inside `block` leave the CFG unchanged, so block-level dominance agrees across + -- the two contexts. As with `newCtxDom`/`newCtxVerif`, this is propagated from the driver-level + -- pattern obligation `RewritePreservesBlockDominance` (block-dominance preservation does not hold + -- for an arbitrary op-list surgery, so it is discharged per concrete pattern). + blockDominatesPreserved := fun b₁ b₂ h₁ h₂ => + hRewritePreservesBlockDominance rewriter op opInBounds rewriter' hdriverOrig b₁ b₂ h₁ h₂ -- Op-list edits (create / insert / replace-value / erase) never touch a survivor's region list: -- chain the per-stage `getNumRegions!`/`getRegion!` frame facts and reassemble the array. opRegionsPreserved := by From 899065403cbf9af3eafba35d7b76d223e5ef8c50 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 04:02:21 +0200 Subject: [PATCH 23/31] Removing sorries from fromLocalRewrite --- Veir/PatternRewriter/Basic.lean | 45 ++++- Veir/PatternRewriter/Soundness.lean | 247 ++++++++++++++++++++++++---- 2 files changed, 250 insertions(+), 42 deletions(-) diff --git a/Veir/PatternRewriter/Basic.lean b/Veir/PatternRewriter/Basic.lean index b74557bb0..91bfd62d2 100644 --- a/Veir/PatternRewriter/Basic.lean +++ b/Veir/PatternRewriter/Basic.lean @@ -176,6 +176,16 @@ def insertOp (rewriter: PatternRewriter OpInfo) (op: OperationPtr) (ip : InsertP worklist := rewriter.worklist.push op, } +/-- Like `insertOp`, but dynamically checks the in-bounds preconditions instead of +requiring them as proofs. Returns `none` if a precondition fails to hold. -/ +def insertOp! (rewriter: PatternRewriter OpInfo) (op: OperationPtr) (ip : InsertPoint) + : Option (PatternRewriter OpInfo) := do + if hop : op.InBounds rewriter.ctx.raw then + if hip : ip.InBounds rewriter.ctx.raw then + rewriter.insertOp op ip hop hip + else none + else none + def eraseOp (rewriter: PatternRewriter OpInfo) (op: OperationPtr) (opRegions : op.getNumRegions! rewriter.ctx.raw = 0 := by grind) (opUses : !op.hasUses! rewriter.ctx.raw := by grind) @@ -188,6 +198,18 @@ def eraseOp (rewriter: PatternRewriter OpInfo) (op: OperationPtr) worklist := rewriter.worklist.remove op, } +/-- Like `eraseOp`, but dynamically checks the preconditions instead of requiring them +as proofs. Returns `none` if a precondition fails to hold. -/ +def eraseOp! (rewriter: PatternRewriter OpInfo) (op: OperationPtr) + : Option (PatternRewriter OpInfo) := do + if hOp : op.InBounds rewriter.ctx.raw then + if hRegions : op.getNumRegions! rewriter.ctx.raw = 0 then + if hUses : (!op.hasUses! rewriter.ctx.raw) = true then + rewriter.eraseOp op hRegions hUses hOp + else none + else none + else none + def replaceOp (rewriter: PatternRewriter OpInfo) (oldOp newOp: OperationPtr) (opNe : oldOp ≠ newOp := by grind) (hpar : (oldOp.get! rewriter.ctx.raw).parent.isSome = true := by grind) @@ -215,6 +237,18 @@ def replaceValue (rewriter: PatternRewriter OpInfo) (oldVal newVal: ValuePtr) let ctx := WfRewriter.replaceValue rewriter.ctx oldVal newVal { rewriter with ctx, hasDoneAction := true} +/-- Like `replaceValue`, but dynamically checks the preconditions instead of requiring +them as proofs. Returns `none` if a precondition fails to hold. -/ +def replaceValue! (rewriter: PatternRewriter OpInfo) (oldVal newVal: ValuePtr) + : Option (PatternRewriter OpInfo) := do + if hne : oldVal ≠ newVal then + if hold : oldVal.InBounds rewriter.ctx.raw then + if hnew : newVal.InBounds rewriter.ctx.raw then + some (rewriter.replaceValue oldVal newVal hne hold hnew) + else none + else none + else none + def createBlock (rewriter: PatternRewriter OpInfo) (argTypes: Array TypeAttr) (insertPoint : Option BlockInsertPoint) @@ -246,9 +280,8 @@ abbrev RewritePattern (OpInfo : Type) [HasOpInfo OpInfo] := abbrev LocalRewritePattern (OpInfo : Type) [HasOpInfo OpInfo] := WfIRContext OpInfo → OperationPtr → Option (WfIRContext OpInfo × Option (Array OperationPtr × Array ValuePtr)) -set_option warn.sorry false in def RewritePattern.fromLocalRewrite (pattern : LocalRewritePattern OpInfo) : RewritePattern OpInfo := - fun rewriter op opInBounds => do + fun rewriter op _opInBounds => do match pattern rewriter.ctx op with -- error while applying pattern | none => none @@ -258,13 +291,13 @@ def RewritePattern.fromLocalRewrite (pattern : LocalRewritePattern OpInfo) : Rew | some (newCtx, some (newOps, newRes)) => let mut rewriter := { rewriter with ctx := newCtx, hasDoneAction := true } for newOp in newOps do - rewriter ← rewriter.insertOp newOp (InsertPoint.before op) (by sorry) (by sorry) + rewriter ← rewriter.insertOp! newOp (InsertPoint.before op) for (res, i) in newRes.zipIdx do - rewriter ← rewriter.replaceValue (op.getResult i) res (by sorry) (by sorry) (by sorry) + rewriter ← rewriter.replaceValue! (op.getResult i) res let mut operands : Array ValuePtr := #[] - for i in 0...op.getNumOperands rewriter.ctx.raw (by sorry) do + for i in 0...op.getNumOperands! rewriter.ctx.raw do operands := operands.push (op.getOperand! rewriter.ctx.raw i) - rewriter ← rewriter.eraseOp op (by sorry) (by sorry) (by sorry) + rewriter ← rewriter.eraseOp! op return rewriter set_option warn.sorry false in diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 02e64cc3f..b61fffd1c 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -3310,6 +3310,185 @@ theorem rewriteMapping_applyToArray_eq_map {ctx newCtx : WfIRContext OpCode} simp only [ValueMapping.applyToArray, Array.getElem_map, Array.getElem_attach, rewriteMapping] split <;> grind +/-! ### Bridges from the `!`-checked driver operations to their proof-carrying counterparts. + +`RewritePattern.fromLocalRewrite` drives the rewrite with the dynamically-checked `insertOp!`, +`replaceValue!`, and `eraseOp!` (each returns `none` if a precondition fails). When such a call +returns `some b'`, it agrees with the proof-carrying `insertOp`/`replaceValue`/`eraseOp` it reduces +to. These bridges expose that fact, letting the keystone fold lemmas below run unchanged against the +non-`!` API. -/ + +/-- When `insertOp!` succeeds it exhibits the proof-carrying `insertOp` call it reduces to. -/ +theorem PatternRewriter.insertOp!_eq_some {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} + (h : b.insertOp! newOp ip = some b') : + ∃ (h1 : newOp.InBounds b.ctx.raw) (h2 : ip.InBounds b.ctx.raw), + PatternRewriter.insertOp b newOp ip h1 h2 = some b' := by + unfold PatternRewriter.insertOp! at h + split at h + · rename_i hop + split at h + · exact ⟨hop, _, h⟩ + · simp at h + · simp at h + +/-- When `replaceValue!` succeeds it is the `replaceValue` of the same arguments. -/ +theorem PatternRewriter.replaceValue!_eq_some {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} + (h : b.replaceValue! oldVal newVal = some b') : + ∃ (ne : oldVal ≠ newVal) (oldIn : oldVal.InBounds b.ctx.raw) (newIn : newVal.InBounds b.ctx.raw), + b.replaceValue oldVal newVal ne oldIn newIn = b' := by + unfold PatternRewriter.replaceValue! at h + split at h + · rename_i hne + split at h + · rename_i hold + split at h + · rename_i hnew + simp only [Option.some.injEq] at h + exact ⟨hne, hold, hnew, h⟩ + · simp at h + · simp at h + · simp at h + +/-- When `eraseOp!` succeeds it exhibits the proof-carrying `eraseOp` call it reduces to. -/ +theorem PatternRewriter.eraseOp!_eq_some {b b' : PatternRewriter OpCode} {op : OperationPtr} + (h : b.eraseOp! op = some b') : + ∃ (r : op.getNumRegions! b.ctx.raw = 0) (u : (!op.hasUses! b.ctx.raw) = true) + (hop : op.InBounds b.ctx.raw), + PatternRewriter.eraseOp b op r u hop = some b' := by + unfold PatternRewriter.eraseOp! at h + split at h + · rename_i hOp + split at h + · rename_i hRegions + split at h + · rename_i hUses + exact ⟨hRegions, hUses, hOp, h⟩ + · simp at h + · simp at h + · simp at h + +/-- `insertOp!` (when it succeeds) preserves all `InBounds` facts. -/ +theorem PatternRewriter.insertOp!_ctx_inBounds {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {ptr : GenericPtr} + (h : b.insertOp! newOp ip = some b') : + ptr.InBounds b'.ctx.raw ↔ ptr.InBounds b.ctx.raw := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_ctx_inBounds h + +/-- `insertOp!` (when it succeeds) frames a survivor's intrinsic data. -/ +theorem PatternRewriter.insertOp!_sameIntrinsic {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {o : OperationPtr} + (h : b.insertOp! newOp ip = some b') : + o.SameIntrinsic b.ctx.raw b'.ctx.raw := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_sameIntrinsic h + +/-- `insertOp!` (when it succeeds) frames a survivor's operands. -/ +theorem PatternRewriter.insertOp!_getOperands {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {o : OperationPtr} + (h : b.insertOp! newOp ip = some b') : + o.getOperands! b'.ctx.raw = o.getOperands! b.ctx.raw := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_getOperands h + +/-- `insertOp!` (when it succeeds) leaves every operation's region count unchanged. -/ +theorem PatternRewriter.insertOp!_getNumRegions {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {o : OperationPtr} + (h : b.insertOp! newOp ip = some b') : + o.getNumRegions! b'.ctx.raw = o.getNumRegions! b.ctx.raw := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_getNumRegions h + +/-- `insertOp!` (when it succeeds) leaves every operation's region pointers unchanged. -/ +theorem PatternRewriter.insertOp!_getRegion {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {o : OperationPtr} {idx : Nat} + (h : b.insertOp! newOp ip = some b') : + o.getRegion! b'.ctx.raw idx = o.getRegion! b.ctx.raw idx := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_getRegion h + +/-- `insertOp!` (when it succeeds) leaves every region's entry block unchanged. -/ +theorem PatternRewriter.insertOp!_firstBlock {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {r : RegionPtr} + (h : b.insertOp! newOp ip = some b') : + (r.get! b'.ctx.raw).firstBlock = (r.get! b.ctx.raw).firstBlock := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_firstBlock h + +/-- `insertOp!` (when it succeeds) leaves every block's argument count unchanged. -/ +theorem PatternRewriter.insertOp!_getNumArguments {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {bl : BlockPtr} + (h : b.insertOp! newOp ip = some b') : + bl.getNumArguments! b'.ctx.raw = bl.getNumArguments! b.ctx.raw := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_getNumArguments h + +/-- `insertOp!` (when it succeeds) leaves every value's type unchanged. -/ +theorem PatternRewriter.insertOp!_getType {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {v : ValuePtr} + (h : b.insertOp! newOp ip = some b') : + v.getType! b'.ctx.raw = v.getType! b.ctx.raw := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_getType h + +/-- `replaceValue!` (when it succeeds) preserves all `InBounds` facts. -/ +theorem PatternRewriter.replaceValue!_ctx_inBounds {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ptr : GenericPtr} + (h : b.replaceValue! oldVal newVal = some b') : + ptr.InBounds b'.ctx.raw ↔ ptr.InBounds b.ctx.raw := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_ctx_inBounds + +/-- `replaceValue!` (when it succeeds) frames a survivor's intrinsic data. -/ +theorem PatternRewriter.replaceValue!_sameIntrinsic {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {o : OperationPtr} + (h : b.replaceValue! oldVal newVal = some b') : + o.SameIntrinsic b.ctx.raw b'.ctx.raw := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_sameIntrinsic + +/-- `replaceValue!` (when it succeeds) leaves every operation's region count unchanged. -/ +theorem PatternRewriter.replaceValue!_getNumRegions {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {o : OperationPtr} + (h : b.replaceValue! oldVal newVal = some b') : + o.getNumRegions! b'.ctx.raw = o.getNumRegions! b.ctx.raw := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_getNumRegions + +/-- `replaceValue!` (when it succeeds) leaves every operation's region pointers unchanged. -/ +theorem PatternRewriter.replaceValue!_getRegion {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {o : OperationPtr} {idx : Nat} + (h : b.replaceValue! oldVal newVal = some b') : + o.getRegion! b'.ctx.raw idx = o.getRegion! b.ctx.raw idx := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_getRegion + +/-- `replaceValue!` (when it succeeds) leaves every region's entry block unchanged. -/ +theorem PatternRewriter.replaceValue!_firstBlock {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {r : RegionPtr} + (h : b.replaceValue! oldVal newVal = some b') : + (r.get! b'.ctx.raw).firstBlock = (r.get! b.ctx.raw).firstBlock := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_firstBlock + +/-- `replaceValue!` (when it succeeds) leaves every block's argument count unchanged. -/ +theorem PatternRewriter.replaceValue!_getNumArguments {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {bl : BlockPtr} + (h : b.replaceValue! oldVal newVal = some b') : + bl.getNumArguments! b'.ctx.raw = bl.getNumArguments! b.ctx.raw := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_getNumArguments + +/-- `replaceValue!` (when it succeeds) leaves every value's type unchanged. -/ +theorem PatternRewriter.replaceValue!_getType {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {v : ValuePtr} + (h : b.replaceValue! oldVal newVal = some b') : + v.getType! b'.ctx.raw = v.getType! b.ctx.raw := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_getType + /-- **PR 9 — bridge from the concrete driver.** When `fromLocalRewrite` runs the rewrite branch for a matched, in-bounds, region-free `op` that lives inside a block, and the pattern satisfies the four @@ -3363,17 +3542,19 @@ theorem RewrittenAt.of_fromLocalRewrite obtain ⟨s₁, hfold1, hdriver⟩ := Option.bind_eq_some_iff.mp hdriver obtain ⟨s₂, hfold2, hdriver⟩ := Option.bind_eq_some_iff.mp hdriver obtain ⟨_arr, _hloop, herase⟩ := Option.bind_eq_some_iff.mp hdriver + -- The driver erases `op` with the dynamically-checked `eraseOp!`; recover the proof-carrying + -- `eraseOp` call it reduces to (shadowing `herase`) so the keystone `eraseOp` lemmas apply. + obtain ⟨_eraseRegions, _eraseUses, _eraseIn, herase⟩ := PatternRewriter.eraseOp!_eq_some herase -- Bounds transport across the insert/replace folds: both preserve every `InBounds` fact, so `s₂.ctx` -- agrees with the pattern's output `newCtxPat` on bounds. have hbnd : ∀ ptr : GenericPtr, ptr.InBounds s₂.ctx.raw ↔ ptr.InBounds newCtxPat.raw := by intro ptr have h1 := Array.foldlM_option_invariant (P := fun b : PatternRewriter OpCode => ptr.InBounds b.ctx.raw) - (fun b a b' h => PatternRewriter.insertOp_ctx_inBounds h) hfold1 + (fun b a b' h => PatternRewriter.insertOp!_ctx_inBounds h) hfold1 have h2 := Array.foldlM_option_invariant (P := fun b : PatternRewriter OpCode => ptr.InBounds b.ctx.raw) - (fun b a b' h => by - rw [Option.some.injEq] at h; subst h; exact PatternRewriter.replaceValue_ctx_inBounds) hfold2 + (fun b a b' h => PatternRewriter.replaceValue!_ctx_inBounds h) hfold2 exact h2.trans h1 -- `block` survives into the pattern's output context (the pattern only creates ops). have hblockNewCtxPat : block.InBounds newCtxPat.raw := @@ -3450,7 +3631,7 @@ theorem RewrittenAt.of_fromLocalRewrite -- Insert fold: `block`'s list becomes `pre ++ newOps ++ [op] ++ post`; `op` keeps its parent. obtain ⟨hopS1, hparS1, hlistS1⟩ := PatternRewriter.foldlM_insertOp_before_opList - (hf := fun b a b' hfa => ⟨_, _, hfa⟩) + (hf := fun b a b' hfa => PatternRewriter.insertOp!_eq_some hfa) hopNewCtxPat hfold1L hparInit hlistInit hoppre.1 hoppre.2 (by simpa using hopNotNewOps) have hblockS1 : block.InBounds s₁.ctx.raw := by have := s₁.ctx.wellFormed.inBounds; grind have hblockS2 : block.InBounds s₂.ctx.raw := by @@ -3461,7 +3642,7 @@ theorem RewrittenAt.of_fromLocalRewrite rw [PatternRewriter.foldlM_preserves_opList (c := block) (hstep := by intro b a b' hfa - simp only [Option.some.injEq] at hfa; subst hfa + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some hfa exact ⟨fun hcin => PatternRewriter.replaceValue_blockPtr_inBounds.mpr hcin, fun hc hc' => PatternRewriter.replaceValue_operationList hc hc'⟩) hfold2L hblockS1 hblockS2, hlistS1 hblockS1] @@ -3482,7 +3663,7 @@ theorem RewrittenAt.of_fromLocalRewrite (P := fun b : PatternRewriter OpCode => bl.getNumArguments! b.ctx.raw = bl.getNumArguments! newCtxPat.raw) (fun b a b' hh => by - have := PatternRewriter.insertOp_getNumArguments (bl := bl) hh + have := PatternRewriter.insertOp!_getNumArguments (bl := bl) hh constructor <;> intro hb <;> grind) hfold1 exact h.mpr rfl have hRep : bl.getNumArguments! s₂.ctx.raw = bl.getNumArguments! s₁.ctx.raw := by @@ -3490,9 +3671,8 @@ theorem RewrittenAt.of_fromLocalRewrite (P := fun b : PatternRewriter OpCode => bl.getNumArguments! b.ctx.raw = bl.getNumArguments! s₁.ctx.raw) (fun b a b' hh => by - have hst : bl.getNumArguments! b'.ctx.raw = bl.getNumArguments! b.ctx.raw := by - simp only [Option.some.injEq] at hh; subst hh - exact PatternRewriter.replaceValue_getNumArguments + have hst : bl.getNumArguments! b'.ctx.raw = bl.getNumArguments! b.ctx.raw := + PatternRewriter.replaceValue!_getNumArguments hh constructor <;> intro hb <;> grind) hfold2 exact h.mpr rfl have hErase : bl.getNumArguments! rewriter'.ctx.raw = bl.getNumArguments! s₂.ctx.raw := by @@ -3518,7 +3698,7 @@ theorem RewrittenAt.of_fromLocalRewrite (ValuePtr.blockArgument ⟨bl, i⟩).getType! b.ctx.raw = (ValuePtr.blockArgument ⟨bl, i⟩).getType! newCtxPat.raw) (fun b a b' hh => by - have := PatternRewriter.insertOp_getType (v := (ValuePtr.blockArgument ⟨bl, i⟩)) hh + have := PatternRewriter.insertOp!_getType (v := (ValuePtr.blockArgument ⟨bl, i⟩)) hh constructor <;> intro hb <;> grind) hfold1 exact h.mpr rfl have hRep : (ValuePtr.blockArgument ⟨bl, i⟩).getType! s₂.ctx.raw @@ -3529,9 +3709,8 @@ theorem RewrittenAt.of_fromLocalRewrite = (ValuePtr.blockArgument ⟨bl, i⟩).getType! s₁.ctx.raw) (fun b a b' hh => by have hst : (ValuePtr.blockArgument ⟨bl, i⟩).getType! b'.ctx.raw - = (ValuePtr.blockArgument ⟨bl, i⟩).getType! b.ctx.raw := by - simp only [Option.some.injEq] at hh; subst hh - exact PatternRewriter.replaceValue_getType + = (ValuePtr.blockArgument ⟨bl, i⟩).getType! b.ctx.raw := + PatternRewriter.replaceValue!_getType hh constructor <;> intro hb <;> grind) hfold2 exact h.mpr rfl -- `eraseOp` preserves the type of any *in-bounds* value; the `i`-th argument of the surviving @@ -3568,7 +3747,7 @@ theorem RewrittenAt.of_fromLocalRewrite have hcS1 : c.InBounds s₁.ctx.raw := by have h1 := Array.foldlM_option_invariant (P := fun b : PatternRewriter OpCode => (GenericPtr.block c).InBounds b.ctx.raw) - (fun b a b' h => PatternRewriter.insertOp_ctx_inBounds h) hfold1 + (fun b a b' h => PatternRewriter.insertOp!_ctx_inBounds h) hfold1 grind have hcS2 : c.InBounds s₂.ctx.raw := by have := hbnd (GenericPtr.block c); grind have hcond : (op.get! s₂.ctx.raw).parent ≠ (c : Option BlockPtr) := by @@ -3584,13 +3763,13 @@ theorem RewrittenAt.of_fromLocalRewrite rw [PatternRewriter.foldlM_preserves_opList (c := c) (hstep := by intro b a b' hfa - simp only [Option.some.injEq] at hfa; subst hfa + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some hfa exact ⟨fun hcin => PatternRewriter.replaceValue_blockPtr_inBounds.mpr hcin, fun h1 h2 => PatternRewriter.replaceValue_operationList h1 h2⟩) hfold2L hcS1 hcS2] -- Insert fold leaves `c`'s list alone (inserts target `block ≠ c`). rw [PatternRewriter.foldlM_insertOp_before_other (c := c) (block := block) hcne - (hf := fun b a b' hfa => ⟨_, _, hfa⟩) + (hf := fun b a b' hfa => PatternRewriter.insertOp!_eq_some hfa) hopNewCtxPat hparInit hfold1L (by simpa using hopNotNewOps) hcNewCtxPat hcS1] -- Created ops leave `c`'s list alone. exact (hCreated.operationList_eq cIn hcNewCtxPat).symm @@ -3654,7 +3833,7 @@ theorem RewrittenAt.of_fromLocalRewrite have hoS1 : o.InBounds s₁.ctx.raw := by have h := Array.foldlM_option_invariant (P := fun b : PatternRewriter OpCode => (GenericPtr.operation o).InBounds b.ctx.raw) - (fun b a b' hh => PatternRewriter.insertOp_ctx_inBounds hh) hfold1 + (fun b a b' hh => PatternRewriter.insertOp!_ctx_inBounds hh) hfold1 grind have hoErase := PatternRewriter.eraseOp_ctx_eq herase ▸ oIn' -- (1) Intrinsic data is framed across the whole pipeline. @@ -3663,16 +3842,15 @@ theorem RewrittenAt.of_fromLocalRewrite have h := Array.foldlM_option_invariant (P := fun b : PatternRewriter OpCode => o.SameIntrinsic newCtxPat.raw b.ctx.raw) (fun b a b' hh => - ⟨fun hb => hb.trans (PatternRewriter.insertOp_sameIntrinsic hh).symm, - fun hb => hb.trans (PatternRewriter.insertOp_sameIntrinsic hh)⟩) hfold1 + ⟨fun hb => hb.trans (PatternRewriter.insertOp!_sameIntrinsic hh).symm, + fun hb => hb.trans (PatternRewriter.insertOp!_sameIntrinsic hh)⟩) hfold1 exact h.mpr OperationPtr.SameIntrinsic.rfl have hrep : o.SameIntrinsic s₁.ctx.raw s₂.ctx.raw := by have h := Array.foldlM_option_invariant (P := fun b : PatternRewriter OpCode => o.SameIntrinsic s₁.ctx.raw b.ctx.raw) (fun b a b' hh => by - have hst : o.SameIntrinsic b.ctx.raw b'.ctx.raw := by - simp only [Option.some.injEq] at hh; subst hh - exact PatternRewriter.replaceValue_sameIntrinsic + have hst : o.SameIntrinsic b.ctx.raw b'.ctx.raw := + PatternRewriter.replaceValue!_sameIntrinsic hh exact ⟨fun hb => hb.trans hst.symm, fun hb => hb.trans hst⟩) hfold2 exact h.mpr OperationPtr.SameIntrinsic.rfl have hers : o.SameIntrinsic s₂.ctx.raw rewriter'.ctx.raw := by @@ -3693,14 +3871,14 @@ theorem RewrittenAt.of_fromLocalRewrite (fun arr q => arr.map (fun v => if v = (op.getResult q.2 : ValuePtr) then q.1 else v)) (o.getOperands! s₁.ctx.raw) := PatternRewriter.foldlM_replaceValue_getOperands - (hf := fun b q b' hfa => ⟨_, _, _, by simp only [Option.some.injEq] at hfa; exact hfa⟩) + (hf := fun b q b' hfa => PatternRewriter.replaceValue!_eq_some hfa) hfold2L hoS1 have hopsIns : o.getOperands! s₁.ctx.raw = o.getOperands! newCtxPat.raw := by have h := Array.foldlM_option_invariant (P := fun b : PatternRewriter OpCode => o.getOperands! b.ctx.raw = o.getOperands! newCtxPat.raw) (fun b a b' hh => by - have := PatternRewriter.insertOp_getOperands (o := o) hh + have := PatternRewriter.insertOp!_getOperands (o := o) hh constructor <;> intro hb <;> grind) hfold1 exact h.mpr rfl have hopsCre : o.getOperands! newCtxPat.raw = o.getOperands! rewriter.ctx.raw := @@ -3809,7 +3987,7 @@ theorem RewrittenAt.of_fromLocalRewrite (P := fun b : PatternRewriter OpCode => o.getNumRegions! b.ctx.raw = o.getNumRegions! newCtxPat.raw) (fun b a b' hh => by - have := PatternRewriter.insertOp_getNumRegions (o := o) hh + have := PatternRewriter.insertOp!_getNumRegions (o := o) hh constructor <;> intro hb <;> grind) hfold1 exact h.mpr rfl have hrep : o.getNumRegions! s₂.ctx.raw = o.getNumRegions! s₁.ctx.raw := by @@ -3817,9 +3995,8 @@ theorem RewrittenAt.of_fromLocalRewrite (P := fun b : PatternRewriter OpCode => o.getNumRegions! b.ctx.raw = o.getNumRegions! s₁.ctx.raw) (fun b a b' hh => by - have hst : o.getNumRegions! b'.ctx.raw = o.getNumRegions! b.ctx.raw := by - simp only [Option.some.injEq] at hh; subst hh - exact PatternRewriter.replaceValue_getNumRegions + have hst : o.getNumRegions! b'.ctx.raw = o.getNumRegions! b.ctx.raw := + PatternRewriter.replaceValue!_getNumRegions hh constructor <;> intro hb <;> grind) hfold2 exact h.mpr rfl have hers : o.getNumRegions! rewriter'.ctx.raw = o.getNumRegions! s₂.ctx.raw := by @@ -3836,7 +4013,7 @@ theorem RewrittenAt.of_fromLocalRewrite (P := fun b : PatternRewriter OpCode => o.getRegion! b.ctx.raw idx = o.getRegion! newCtxPat.raw idx) (fun b a b' hh => by - have := PatternRewriter.insertOp_getRegion (o := o) (idx := idx) hh + have := PatternRewriter.insertOp!_getRegion (o := o) (idx := idx) hh constructor <;> intro hb <;> grind) hfold1 exact h.mpr rfl have hrep : o.getRegion! s₂.ctx.raw idx = o.getRegion! s₁.ctx.raw idx := by @@ -3844,9 +4021,8 @@ theorem RewrittenAt.of_fromLocalRewrite (P := fun b : PatternRewriter OpCode => o.getRegion! b.ctx.raw idx = o.getRegion! s₁.ctx.raw idx) (fun b a b' hh => by - have hst : o.getRegion! b'.ctx.raw idx = o.getRegion! b.ctx.raw idx := by - simp only [Option.some.injEq] at hh; subst hh - exact PatternRewriter.replaceValue_getRegion + have hst : o.getRegion! b'.ctx.raw idx = o.getRegion! b.ctx.raw idx := + PatternRewriter.replaceValue!_getRegion hh constructor <;> intro hb <;> grind) hfold2 exact h.mpr rfl have hers : o.getRegion! rewriter'.ctx.raw idx = o.getRegion! s₂.ctx.raw idx := by @@ -3865,7 +4041,7 @@ theorem RewrittenAt.of_fromLocalRewrite (P := fun b : PatternRewriter OpCode => (r.get! b.ctx.raw).firstBlock = (r.get! newCtxPat.raw).firstBlock) (fun b a b' hh => by - have := PatternRewriter.insertOp_firstBlock (r := r) hh + have := PatternRewriter.insertOp!_firstBlock (r := r) hh constructor <;> intro hb <;> grind) hfold1 exact h.mpr rfl have hrep : (r.get! s₂.ctx.raw).firstBlock = (r.get! s₁.ctx.raw).firstBlock := by @@ -3873,9 +4049,8 @@ theorem RewrittenAt.of_fromLocalRewrite (P := fun b : PatternRewriter OpCode => (r.get! b.ctx.raw).firstBlock = (r.get! s₁.ctx.raw).firstBlock) (fun b a b' hh => by - have hst : (r.get! b'.ctx.raw).firstBlock = (r.get! b.ctx.raw).firstBlock := by - simp only [Option.some.injEq] at hh; subst hh - exact PatternRewriter.replaceValue_firstBlock + have hst : (r.get! b'.ctx.raw).firstBlock = (r.get! b.ctx.raw).firstBlock := + PatternRewriter.replaceValue!_firstBlock hh constructor <;> intro hb <;> grind) hfold2 exact h.mpr rfl have hers : (r.get! rewriter'.ctx.raw).firstBlock = (r.get! s₂.ctx.raw).firstBlock := by From 2c861685b54eb475e45a3113aa978c7618ead192 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 13:46:30 +0200 Subject: [PATCH 24/31] One less argument in fromLocalRewrite soundness --- Veir/PatternRewriter/Semantics.lean | 10 ++++++++++ Veir/PatternRewriter/Soundness.lean | 9 ++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Veir/PatternRewriter/Semantics.lean b/Veir/PatternRewriter/Semantics.lean index d6bc46ed3..b3bf91477 100644 --- a/Veir/PatternRewriter/Semantics.lean +++ b/Veir/PatternRewriter/Semantics.lean @@ -120,6 +120,14 @@ def LocalRewritePattern.ReturnValuesDominate (pattern : LocalRewritePattern OpCo ∀ ctx op newCtx newOps newValues, pattern ctx op = some (newCtx, some (newOps, newValues)) → ∀ v ∈ newValues, v.InBounds ctx.raw → v.dominatesIp (InsertPoint.before op) ctx +/-- +The matched operation has no regions. The driver's "insert before, redirect results, erase" pipeline +is only sound for region-free operations, so the pattern may only match such operations. In particular +this implies the matched operation is not a function (clause 9, `opNotFunction`). -/ +def LocalRewritePattern.MatchedOpHasNoRegions (pattern : LocalRewritePattern OpCode) : Prop := + ∀ ctx op newCtx newOps newValues, pattern ctx op = some (newCtx, some (newOps, newValues)) → + op.getNumRegions! ctx.raw = 0 + /-- Indexed access on the returned values is in bounds of the new context. Discharges the second `sorry` in `LocalRewritePattern.Mapping`. @@ -274,6 +282,8 @@ structure LocalRewritePattern.Valid (pattern : LocalRewritePattern OpCode) : Pro returnValuesNotOwnResults : pattern.ReturnValuesNotOwnResults /-- Every forwarded pre-existing returned value dominates the point before `op`. -/ returnValuesDominate : pattern.ReturnValuesDominate + /-- The matched operation has no regions. -/ + matchedOpHasNoRegions : pattern.MatchedOpHasNoRegions /-- Interpreting the matched operation is refined by interpreting the new operations. -/ preservesSemantics : pattern.PreservesSemantics returnOps returnCtxChanges returnValuesInBounds returnValues diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index b61fffd1c..2bfee13f6 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -3508,7 +3508,6 @@ theorem RewrittenAt.of_fromLocalRewrite (hSrcVerif : rewriter.ctx.Verified) {op : OperationPtr} (opInBounds : op.InBounds rewriter.ctx.raw) {block : BlockPtr} (hOpParent : (op.get! rewriter.ctx.raw).parent = some block) - (hOpRegions : op.getNumRegions! rewriter.ctx.raw = 0) {newCtxPat : WfIRContext OpCode} {newOps : Array OperationPtr} {newValues : Array ValuePtr} (hpat : pattern rewriter.ctx op = some (newCtxPat, some (newOps, newValues))) (hdriver : RewritePattern.fromLocalRewrite pattern rewriter op opInBounds = some rewriter') : @@ -3517,8 +3516,12 @@ theorem RewrittenAt.of_fromLocalRewrite RewrittenAt rewriter.ctx op newOps newValues rewriter'.ctx opInBounds block pre post blockIn blockIn' := by obtain ⟨-, hReturnCtxChanges, hReturnOps, hReturnValues, hReturnValuesInBounds, - hReturnValuesNotOwnResults, hReturnValuesDominate, -, hRewritePreservesDom, - hRewritePreservesVerified, hRewriteNewValuesDominate, hRewritePreservesBlockDominance⟩ := hValid + hReturnValuesNotOwnResults, hReturnValuesDominate, hMatchedOpHasNoRegions, -, + hRewritePreservesDom, hRewritePreservesVerified, hRewriteNewValuesDominate, + hRewritePreservesBlockDominance⟩ := hValid + -- `op` has no regions: this is one of the pattern's validity obligations. + have hOpRegions : op.getNumRegions! rewriter.ctx.raw = 0 := + hMatchedOpHasNoRegions rewriter.ctx op newCtxPat newOps newValues hpat -- `block` is in bounds of the source context: it is the parent of the in-bounds `op`. have blockIn : block.InBounds rewriter.ctx.raw := by have := rewriter.ctx.wellFormed.inBounds; grind From 810badb76d0663551750f3c6977e7e71f29224fc Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 13:48:52 +0200 Subject: [PATCH 25/31] Check for parent in rewriter --- Veir/PatternRewriter/Basic.lean | 3 +++ Veir/PatternRewriter/Soundness.lean | 20 ++++++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Veir/PatternRewriter/Basic.lean b/Veir/PatternRewriter/Basic.lean index 91bfd62d2..0b49a4447 100644 --- a/Veir/PatternRewriter/Basic.lean +++ b/Veir/PatternRewriter/Basic.lean @@ -289,6 +289,9 @@ def RewritePattern.fromLocalRewrite (pattern : LocalRewritePattern OpInfo) : Rew | some (newCtx, none) => return {rewriter with ctx := newCtx, hasDoneAction := false} -- match and rewrite | some (newCtx, some (newOps, newRes)) => + -- Only rewrite an operation that lives inside a block: the surgery below (insert before `op`, + -- erase `op`) is only meaningful for a parented op, and soundness needs `op`'s parent block. + let _ ← (op.get! rewriter.ctx.raw).parent let mut rewriter := { rewriter with ctx := newCtx, hasDoneAction := true } for newOp in newOps do rewriter ← rewriter.insertOp! newOp (InsertPoint.before op) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 2bfee13f6..886b88157 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -3503,18 +3503,23 @@ The remaining structural fields are discharged from the keystone fold decomposit theorem RewrittenAt.of_fromLocalRewrite {pattern : LocalRewritePattern OpCode} (hValid : pattern.Valid) - {rewriter rewriter' : PatternRewriter OpCode} (hSrcDom : rewriter.ctx.Dom) (hSrcVerif : rewriter.ctx.Verified) - {op : OperationPtr} (opInBounds : op.InBounds rewriter.ctx.raw) - {block : BlockPtr} (hOpParent : (op.get! rewriter.ctx.raw).parent = some block) - {newCtxPat : WfIRContext OpCode} {newOps : Array OperationPtr} {newValues : Array ValuePtr} (hpat : pattern rewriter.ctx op = some (newCtxPat, some (newOps, newValues))) (hdriver : RewritePattern.fromLocalRewrite pattern rewriter op opInBounds = some rewriter') : - ∃ (pre post : Array OperationPtr) + ∃ (block : BlockPtr) (pre post : Array OperationPtr) (blockIn : block.InBounds rewriter.ctx.raw) (blockIn' : block.InBounds rewriter'.ctx.raw), + (op.get! rewriter.ctx.raw).parent = some block ∧ RewrittenAt rewriter.ctx op newOps newValues rewriter'.ctx opInBounds block pre post blockIn blockIn' := by + -- `op` has a parent block: the driver inserts every `newOp` *before* `op`, and an insertion before + -- a parentless op fails, so a successful `fromLocalRewrite` witnesses `op`'s parent. This recovers + -- the block (and the parent equation) that the previous statement took as a hypothesis. + obtain ⟨block, hOpParent⟩ : ∃ block, (op.get! rewriter.ctx.raw).parent = some block := by + have h := hdriver + simp only [RewritePattern.fromLocalRewrite, hpat] at h + obtain ⟨block, hpar, -⟩ := Option.bind_eq_some_iff.mp h + exact ⟨block, hpar⟩ obtain ⟨-, hReturnCtxChanges, hReturnOps, hReturnValues, hReturnValuesInBounds, hReturnValuesNotOwnResults, hReturnValuesDominate, hMatchedOpHasNoRegions, -, hRewritePreservesDom, hRewritePreservesVerified, hRewriteNewValuesDominate, @@ -3540,6 +3545,9 @@ theorem RewrittenAt.of_fromLocalRewrite unfold RewritePattern.fromLocalRewrite at hdriver rw [hpat] at hdriver simp only [bind_pure_comp, Array.forIn_yield_eq_foldlM, id_map'] at hdriver + -- Peel the leading parent guard (`let _ ← (op.get! …).parent`): a successful driver witnesses `op`'s + -- parent block. We already recovered it as `hOpParent` above, so this binding is discarded here. + obtain ⟨_guardBlock, _hguardBlock, hdriver⟩ := Option.bind_eq_some_iff.mp hdriver -- Decompose the reduced driver into its three stages: insert-fold (→ `s₁`), replace-fold (→ `s₂`), -- then the final `eraseOp` of `op`. The middle operands-collection loop is discarded. obtain ⟨s₁, hfold1, hdriver⟩ := Option.bind_eq_some_iff.mp hdriver @@ -3728,7 +3736,7 @@ theorem RewrittenAt.of_fromLocalRewrite rw [PatternRewriter.eraseOp_ctx_eq herase] exact ValuePtr.getType!_wfRewriter_eraseOp (PatternRewriter.eraseOp_ctx_eq herase ▸ hvIn) rw [hErase, hRep, hIns, hCre] - refine ⟨pre, post, blockIn, blockIn', ?_⟩ + refine ⟨block, pre, post, blockIn, blockIn', hOpParent, ?_⟩ exact { -- Block-list shape: discharged for the source by the split lemma. srcList := hsrcList From ff25aa4233a2ef2f77c693904e9212ecbb147646 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 17:58:37 +0200 Subject: [PATCH 26/31] Preservation of parent --- Veir/PatternRewriter/Soundness.lean | 380 ++++++++++++++++++++++++++++ Veir/Verifier.lean | 10 + 2 files changed, 390 insertions(+) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 886b88157..0f3b9329d 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -195,6 +195,17 @@ structure RewrittenAt CFG walk from the same entry block in both contexts. -/ regionFirstBlockPreserved : ∀ (r : RegionPtr), r.InBounds ctx.raw → (r.get! newCtx.raw).firstBlock = (r.get! ctx.raw).firstBlock + /-- The enclosing operation of every surviving operation is preserved: the rewrite only edits + operation lists, never the block→region→operation parent pointers along a survivor's spine. This is + the "parent operations of surviving operations are preserved" promise of clause 5, and it is what + lets `IsTopLevelFuncWithName` transport across the rewrite (so the module-refinement lift can pick + the same surviving function in the target context). It is stated as an implication rather than a raw + equality because a detached source region could in principle be attached when the rewrite creates a + new operation — but a survivor that is *already* enclosed by some `enclosing` (its spine is fully + parented) keeps that enclosing operation. -/ + getParentOpPreserved : ∀ (o enclosing : OperationPtr), o.InBounds ctx.raw → o ≠ op → + o.getParentOp! ctx.raw = some enclosing → + o.getParentOp! newCtx.raw = some enclosing -- Clause 9: the matched operation is not a function. /-- The matched operation `op` is not a function: it does not have exactly one region. Functions (the operations `interpretFunction` runs) have exactly one region, so this guarantees every function @@ -2406,6 +2417,79 @@ theorem RewrittenAt.interpretModule_refinement rw [dif_pos (by simpa using hNum)] exact Interp.isRefinedBy_none_target +/-- +**Stage E — module refinement (`isModuleRefinedBy`).** A rewrite refines a module: every top-level +`func.func` of `moduleOp` in the source context is refined, as a function, by the *same* operation in +the target context. The surviving function is its own witness — it is distinct from the matched `op` +(a `func.func` has one region by verification, `op` has none), so it survives the rewrite, keeps its +op type and `sym_name` (`frame`), and keeps `moduleOp` as its enclosing operation +(`getParentOpPreserved`); the per-function refinement is exactly `interpretFunction_refinement`. +-/ +theorem RewrittenAt.isModuleRefinedBy + (hRW : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (ctxVerif : ctx.Verified) + (newCtxVerif : newCtx.Verified) + (hCtxDom : ctx.Dom) + (hOpSim : OpStepSimulation op newOps hRW.σ opIn hRW.newOpsInBounds') + (hSrcSplit : ∀ (b : BlockPtr) (bIn : b.InBounds ctx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (_termIn : term.InBounds ctx.raw), + (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + (hTgtSplit : ∀ (b : BlockPtr) (bIn' : b.InBounds newCtx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds newCtx.raw) (_termIn : term.InBounds newCtx.raw), + (b.operationList newCtx.raw newCtx.wellFormed bIn').toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState newCtx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + (hSrcInv : ∀ (_funcOp : OperationPtr) (values : Array RuntimeValue) (mem : MemoryState) + (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) (newVars : VariableState ctx), + (VariableState.empty ctx).setArgumentValues? entryBlock values entryIn = some newVars → + ∀ fst (hfst : (entryBlock.get! ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars mem).EquationLemmaAt (.before fst) + (by have := ctx.wellFormed.inBounds; grind)) + (hTgtInv : ∀ (_funcOp : OperationPtr) (values' : Array RuntimeValue) (mem : MemoryState) + (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) (newVars' : VariableState newCtx), + (VariableState.empty newCtx).setArgumentValues? entryBlock values' + (hRW.blocksInBounds entryBlock entryIn) = some newVars' → + (InterpreterState.mk newVars' mem).DefinesDominating + (InsertPoint.atStart! entryBlock newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed + (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) + {moduleOp : OperationPtr} : + moduleOp.isModuleRefinedBy ctx moduleOp newCtx := by + intro func₁ func₁In name hTop + -- A verified `func.func` has one region, while `op` has none, so the function survives. + have hfuncVerif : func₁.Verified ctx func₁In := + OperationPtr.satisfyInvariants_of_IRContext_satisfyOpInvariants ctxVerif func₁In + have hOneRegion : func₁.getNumRegions! ctx.raw = 1 := + OperationPtr.Verified.func_func hfuncVerif hTop.isFunc + have hne : func₁ ≠ op := by rintro rfl; exact hRW.opNotFunction hOneRegion + have func₁In' : func₁.InBounds newCtx.raw := hRW.survives func₁ func₁In hne + have hframe := hRW.frame_of_ne func₁In hne + refine ⟨func₁, func₁In', ⟨?_, ?_, ?_⟩, ?_⟩ + · -- The op type `.func .func` is preserved. + rw [hframe.opType]; exact hTop.isFunc + · -- The `sym_name` property is preserved (transport the `props` frame across the op-type equality). + rw [hTop.hasName] + have hOT : func₁.getOpType! newCtx.raw = func₁.getOpType! ctx.raw := hframe.opType + have hp : func₁.getProperties! newCtx.raw (func₁.getOpType! newCtx.raw) + = hOT ▸ func₁.getProperties! ctx.raw (func₁.getOpType! ctx.raw) := hframe.props + have hF : func₁.getOpType! ctx.raw = OpCode.func Func.func := hTop.isFunc + clear hSrcInv hTgtInv hSrcSplit hTgtSplit hOpSim hframe hTop + grind + · -- The enclosing operation `moduleOp` is preserved. + exact hRW.getParentOpPreserved func₁ moduleOp func₁In hne hTop.isTopLevel + · -- The per-function refinement is Stage E. + intro values valuesTarget mem hVals + exact hRW.interpretFunction_refinement newCtxVerif hCtxDom hOpSim hSrcSplit hTgtSplit + func₁In func₁In' hVals + (fun entryBlock entryIn newVars h fst hfst => + hSrcInv func₁ values mem entryBlock entryIn newVars h fst hfst) + (fun entryBlock entryIn newVars' h => + hTgtInv func₁ valuesTarget mem entryBlock entryIn newVars' h) + /-! ## PR 9: connecting the concrete driver `fromLocalRewrite` to `RewrittenAt` The whole soundness lift above is developed against the abstract `RewrittenAt` relation. This section @@ -3489,6 +3573,214 @@ theorem PatternRewriter.replaceValue!_getType {b b' : PatternRewriter OpCode} obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h exact PatternRewriter.replaceValue_getType +/-! ### Helpers for the `getParentOpPreserved` field of `of_fromLocalRewrite`. + +The enclosing-operation chain of a survivor is the composite `o → parent block → parent region → +parent operation`. The rewrite pipeline preserves each link: block and region parents are untouched +by op-list edits, a survivor's own parent is untouched (it is neither inserted nor erased), and a +region that is already attached cannot be re-attached (so its parent is stable). These lemmas package +those per-stage facts; the congruence lemma below then assembles them into `getParentOp!` stability. -/ + +/-- Membership-aware variant of `List.foldlM_option_invariant`: the per-step hypothesis may use that +the folded element comes from the list. Used to thread "the survivor is not the inserted op" through +the insert fold. -/ +theorem List.foldlM_option_invariant_mem {α β : Type} {f : β → α → Option β} {P : β → Prop} : + ∀ {l : List α} {init s : β}, + (∀ b a b', a ∈ l → f b a = some b' → (P b' ↔ P b)) → + l.foldlM f init = some s → (P s ↔ P init) + | [], init, s, _, h => by + rw [List.foldlM_nil] at h + obtain rfl : init = s := by simpa using h + rfl + | a :: t, init, s, hstep, h => by + rw [List.foldlM_cons] at h + obtain ⟨b, hf, hb⟩ := Option.bind_eq_some_iff.mp h + have ih := List.foldlM_option_invariant_mem (l := t) + (fun b a' b' hmem hfa => hstep b a' b' (List.mem_cons_of_mem a hmem) hfa) hb + rw [ih, hstep init a b (by simp) hf] + +/-- `getParentOp!` is stable between two contexts that agree on the relevant parent links: a +survivor's own parent block, every block's parent region, and every *already-parented* region's +parent operation. Stated as an implication on a concrete enclosing operation, which is all the +soundness lift needs (and is robust to detached regions being attached elsewhere). -/ +theorem OperationPtr.getParentOp!_eq_of {c c' : IRContext OpCode} {o m : OperationPtr} + (hfib : c.FieldsInBounds) (oIn : o.InBounds c) + (hop : (o.get! c').parent = (o.get! c).parent) + (hblk : ∀ b : BlockPtr, (b.get! c').parent = (b.get! c).parent) + (hrgn : ∀ r : RegionPtr, r.InBounds c → (r.get! c).parent ≠ none → + (r.get! c').parent = (r.get! c).parent) + (h : o.getParentOp! c = some m) : + o.getParentOp! c' = some m := by + have key : ∀ d : IRContext OpCode, o.getParentOp! d = + ((o.get! d).parent).bind (fun b => ((b.get! d).parent).bind (fun r => (r.get! d).parent)) := by + intro d + unfold OperationPtr.getParentOp! + split + · next heq => simp [heq] + · next b heq => + split + · next heq2 => simp [heq, heq2] + · next r heq2 => simp [heq, heq2] + rw [key c] at h + obtain ⟨b, hb, h⟩ := Option.bind_eq_some_iff.mp h + obtain ⟨r, hr, h⟩ := Option.bind_eq_some_iff.mp h + have hbIn : b.InBounds c := by grind [OperationPtr.parent!_inBounds] + have hrIn : r.InBounds c := by grind [BlockPtr.parent!_inBounds] + rw [key c', hop, hb] + simp only [Option.bind_some, hblk b, hr, + hrgn r hrIn (by rw [h]; exact Option.some_ne_none m)] + exact h + +/-- `PatternRewriter.insertOp` preserves every block's parent region. -/ +theorem PatternRewriter.insertOp_blockParent {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {block : BlockPtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + (block.get! b'.ctx.raw).parent = (block.get! b.ctx.raw).parent := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact BlockPtr.parent!_wfRewriter_insertOp hwf + +/-- `PatternRewriter.insertOp` preserves every region's parent operation. -/ +theorem PatternRewriter.insertOp_regionParent {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {h1 h2} {r : RegionPtr} + (h : PatternRewriter.insertOp b newOp ip h1 h2 = some b') : + (r.get! b'.ctx.raw).parent = (r.get! b.ctx.raw).parent := by + unfold PatternRewriter.insertOp at h + split at h + · simp at h + · rename_i newCtx hwf + simp only [Option.some.injEq] at h; subst h + exact RegionPtr.parent!_wfRewriter_insertOp hwf + +/-- `insertOp!` (when it succeeds) preserves every block's parent region. -/ +theorem PatternRewriter.insertOp!_blockParent {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {block : BlockPtr} + (h : b.insertOp! newOp ip = some b') : + (block.get! b'.ctx.raw).parent = (block.get! b.ctx.raw).parent := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_blockParent h + +/-- `insertOp!` (when it succeeds) preserves every region's parent operation. -/ +theorem PatternRewriter.insertOp!_regionParent {b b' : PatternRewriter OpCode} + {newOp : OperationPtr} {ip : InsertPoint} {r : RegionPtr} + (h : b.insertOp! newOp ip = some b') : + (r.get! b'.ctx.raw).parent = (r.get! b.ctx.raw).parent := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_regionParent h + +/-- `insertOp!` (when it succeeds) preserves the parent of every operation other than the inserted +one. -/ +theorem PatternRewriter.insertOp!_opParent {b b' : PatternRewriter OpCode} + {newOp o : OperationPtr} {ip : InsertPoint} + (h : b.insertOp! newOp ip = some b') (hne : o ≠ newOp) : + (o.get! b'.ctx.raw).parent = (o.get! b.ctx.raw).parent := by + obtain ⟨h1, h2, h⟩ := PatternRewriter.insertOp!_eq_some h + exact PatternRewriter.insertOp_op_parent h hne + +/-- `PatternRewriter.replaceValue` preserves every block's parent region. -/ +theorem PatternRewriter.replaceValue_blockParent {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {block : BlockPtr} : + (block.get! (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw).parent = + (block.get! b.ctx.raw).parent := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx]; exact BlockPtr.parent!_WfRewriter_replaceValue + +/-- `PatternRewriter.replaceValue` preserves every region's parent operation. -/ +theorem PatternRewriter.replaceValue_regionParent {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {r : RegionPtr} : + (r.get! (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw).parent = + (r.get! b.ctx.raw).parent := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx]; exact RegionPtr.parent!_WfRewriter_replaceValue + +/-- `PatternRewriter.replaceValue` preserves every operation's parent block. -/ +theorem PatternRewriter.replaceValue_opParent {b : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {ne oldIn newIn} {o : OperationPtr} : + (o.get! (b.replaceValue oldVal newVal ne oldIn newIn).ctx.raw).parent = + (o.get! b.ctx.raw).parent := by + have hctx : (b.replaceValue oldVal newVal ne oldIn newIn).ctx + = WfRewriter.replaceValue b.ctx oldVal newVal ne oldIn newIn := by + simp only [PatternRewriter.replaceValue, PatternRewriter.addUsersInWorklist_same_ctx] + rw [hctx]; exact OperationPtr.parent!_WfRewriter_replaceValue + +/-- `replaceValue!` (when it succeeds) preserves every block's parent region. -/ +theorem PatternRewriter.replaceValue!_blockParent {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {block : BlockPtr} + (h : b.replaceValue! oldVal newVal = some b') : + (block.get! b'.ctx.raw).parent = (block.get! b.ctx.raw).parent := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_blockParent + +/-- `replaceValue!` (when it succeeds) preserves every region's parent operation. -/ +theorem PatternRewriter.replaceValue!_regionParent {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {r : RegionPtr} + (h : b.replaceValue! oldVal newVal = some b') : + (r.get! b'.ctx.raw).parent = (r.get! b.ctx.raw).parent := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_regionParent + +/-- `replaceValue!` (when it succeeds) preserves every operation's parent block. -/ +theorem PatternRewriter.replaceValue!_opParent {b b' : PatternRewriter OpCode} + {oldVal newVal : ValuePtr} {o : OperationPtr} + (h : b.replaceValue! oldVal newVal = some b') : + (o.get! b'.ctx.raw).parent = (o.get! b.ctx.raw).parent := by + obtain ⟨ne, oldIn, newIn, rfl⟩ := PatternRewriter.replaceValue!_eq_some h + exact PatternRewriter.replaceValue_opParent + +/-- A `WithCreatedOps` chain preserves every block's parent region (it only creates fresh ops). -/ +theorem WfIRContext.WithCreatedOps.blockParent_eq {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {b : BlockPtr} : + (b.get! ctx₂.raw).parent = (b.get! ctx₁.raw).parent := by + induction h with + | Nil => rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + obtain ⟨opType, rt, ops, succ, regs, props, k₁, k₂, k₃, k₄, hcreate⟩ := hex + rw [BlockPtr.parent!_WfRewriter_createOp hcreate] + exact ih + +/-- A `WithCreatedOps` chain preserves a survivor's parent block (it only creates fresh ops). -/ +theorem WfIRContext.WithCreatedOps.opParent_eq {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {o : OperationPtr} (oIn : o.InBounds ctx₁.raw) : + (o.get! ctx₂.raw).parent = (o.get! ctx₁.raw).parent := by + induction h with + | Nil => rfl + | CreatedOp ctx₁ ctx₂ ctx₃ hwco hex ih => + obtain ⟨opType, rt, ops, succ, regs, props, k₁, k₂, k₃, k₄, hcreate⟩ := hex + have ho2 : o.InBounds ctx₂.raw := by + have := hwco.inBounds_mono (GenericPtr.operation o) (by grind); grind + rw [OperationPtr.parent!_WfRewriter_createOp hcreate, if_neg (by grind)] + exact ih oIn + +/-- A `WithCreatedOps` chain preserves the parent operation of every *already-parented* in-bounds +region. The parent operation `P` is a survivor whose region list is preserved (`getRegion_eq`), and a +region is in its parent op's region list (wellformedness `region_parent`), so the link is stable. -/ +theorem WfIRContext.WithCreatedOps.regionParent_eq_of_parented {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {r : RegionPtr} + (rIn : r.InBounds ctx₁.raw) (hpar : (r.get! ctx₁.raw).parent ≠ none) : + (r.get! ctx₂.raw).parent = (r.get! ctx₁.raw).parent := by + rcases hP : (r.get! ctx₁.raw).parent with _ | P + · exact absurd hP hpar + · have hPIn₁ : P.InBounds ctx₁.raw := by + have := RegionPtr.parent!_inBounds ctx₁.wellFormed.inBounds rIn; grind + have hPIn₂ : P.InBounds ctx₂.raw := by + have := h.inBounds_mono (GenericPtr.operation P) (by grind); grind + have rIn₂ : r.InBounds ctx₂.raw := by + have := h.inBounds_mono (GenericPtr.region r) (by grind); grind + obtain ⟨i, hi, hreg⟩ := ((ctx₁.wellFormed.operations P hPIn₁).region_parent r rIn).mpr hP + have hnum : P.getNumRegions! ctx₂.raw = P.getNumRegions! ctx₁.raw := h.getNumRegions_eq hPIn₁ + have hregeq : P.getRegion! ctx₂.raw i = P.getRegion! ctx₁.raw i := h.getRegion_eq hPIn₁ i + have hr₂ : (r.get! ctx₂.raw).parent = some P := + ((ctx₂.wellFormed.operations P hPIn₂).region_parent r rIn₂).mp + ⟨i, by rw [hnum]; exact hi, by rw [hregeq]; exact hreg⟩ + rw [hr₂] + /-- **PR 9 — bridge from the concrete driver.** When `fromLocalRewrite` runs the rewrite branch for a matched, in-bounds, region-free `op` that lives inside a block, and the pattern satisfies the four @@ -4068,6 +4360,94 @@ theorem RewrittenAt.of_fromLocalRewrite rw [PatternRewriter.eraseOp_ctx_eq herase] exact RegionPtr.firstBlock!_wfRewriter_eraseOp exact hers.trans (hrep.trans (hins.trans hcre)) + -- The enclosing operation of every survivor is preserved: block/op/region parents are framed + -- across the whole pipeline, then assembled by `getParentOp!_eq_of`. + getParentOpPreserved := by + intro o enclosing oIn hne hpar + have honotnew : o ∉ newOps := fun hm => + ((hReturnOps rewriter.ctx op newCtxPat newOps newValues hpat o).mp hm).2 oIn + have oInPat : o.InBounds newCtxPat.raw := by + have := hCreated.inBounds_mono (GenericPtr.operation o) (by grind); grind + have oIn' : o.InBounds rewriter'.ctx.raw := hSurviveOp o hne oInPat + -- Block-parent preservation (unconditional at every stage). + have hblk : ∀ bl : BlockPtr, + (bl.get! rewriter'.ctx.raw).parent = (bl.get! rewriter.ctx.raw).parent := by + intro bl + have hcre : (bl.get! newCtxPat.raw).parent = (bl.get! rewriter.ctx.raw).parent := + hCreated.blockParent_eq + have hins : (bl.get! s₁.ctx.raw).parent = (bl.get! newCtxPat.raw).parent := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + (bl.get! b.ctx.raw).parent = (bl.get! newCtxPat.raw).parent) + (fun b a b' hh => by + have := PatternRewriter.insertOp!_blockParent (block := bl) hh + constructor <;> intro <;> grind) hfold1 + exact h.mpr rfl + have hrep : (bl.get! s₂.ctx.raw).parent = (bl.get! s₁.ctx.raw).parent := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + (bl.get! b.ctx.raw).parent = (bl.get! s₁.ctx.raw).parent) + (fun b a b' hh => by + have := PatternRewriter.replaceValue!_blockParent (block := bl) hh + constructor <;> intro <;> grind) hfold2 + exact h.mpr rfl + have hers : (bl.get! rewriter'.ctx.raw).parent = (bl.get! s₂.ctx.raw).parent := by + rw [PatternRewriter.eraseOp_ctx_eq herase]; exact BlockPtr.parent!_wfRewriter_eraseOp + exact hers.trans (hrep.trans (hins.trans hcre)) + -- Op-parent preservation for the survivor `o` (it is neither created, inserted, nor erased). + have hop : (o.get! rewriter'.ctx.raw).parent = (o.get! rewriter.ctx.raw).parent := by + have hcre : (o.get! newCtxPat.raw).parent = (o.get! rewriter.ctx.raw).parent := + hCreated.opParent_eq oIn + have hins : (o.get! s₁.ctx.raw).parent = (o.get! newCtxPat.raw).parent := by + have hL := hfold1; rw [← Array.foldlM_toList] at hL + have h := List.foldlM_option_invariant_mem (l := newOps.toList) + (P := fun b : PatternRewriter OpCode => + (o.get! b.ctx.raw).parent = (o.get! newCtxPat.raw).parent) + (fun b a b' hmem hh => by + have hane : o ≠ a := by rintro rfl; exact honotnew (by simpa using hmem) + have := PatternRewriter.insertOp!_opParent (o := o) hh hane + constructor <;> intro <;> grind) hL + exact h.mpr rfl + have hrep : (o.get! s₂.ctx.raw).parent = (o.get! s₁.ctx.raw).parent := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + (o.get! b.ctx.raw).parent = (o.get! s₁.ctx.raw).parent) + (fun b a b' hh => by + have := PatternRewriter.replaceValue!_opParent (o := o) hh + constructor <;> intro <;> grind) hfold2 + exact h.mpr rfl + have hoErase := PatternRewriter.eraseOp_ctx_eq herase ▸ oIn' + have hers : (o.get! rewriter'.ctx.raw).parent = (o.get! s₂.ctx.raw).parent := by + rw [PatternRewriter.eraseOp_ctx_eq herase, OperationPtr.parent!_wfRewriter_eraseOp hoErase, + if_neg hne] + exact hers.trans (hrep.trans (hins.trans hcre)) + -- Region-parent preservation for parented in-bounds regions. + have hrgn : ∀ r : RegionPtr, r.InBounds rewriter.ctx.raw → + (r.get! rewriter.ctx.raw).parent ≠ none → + (r.get! rewriter'.ctx.raw).parent = (r.get! rewriter.ctx.raw).parent := by + intro r rIn hrne + have hcre : (r.get! newCtxPat.raw).parent = (r.get! rewriter.ctx.raw).parent := + hCreated.regionParent_eq_of_parented rIn hrne + have hins : (r.get! s₁.ctx.raw).parent = (r.get! newCtxPat.raw).parent := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + (r.get! b.ctx.raw).parent = (r.get! newCtxPat.raw).parent) + (fun b a b' hh => by + have := PatternRewriter.insertOp!_regionParent (r := r) hh + constructor <;> intro <;> grind) hfold1 + exact h.mpr rfl + have hrep : (r.get! s₂.ctx.raw).parent = (r.get! s₁.ctx.raw).parent := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + (r.get! b.ctx.raw).parent = (r.get! s₁.ctx.raw).parent) + (fun b a b' hh => by + have := PatternRewriter.replaceValue!_regionParent (r := r) hh + constructor <;> intro <;> grind) hfold2 + exact h.mpr rfl + have hers : (r.get! rewriter'.ctx.raw).parent = (r.get! s₂.ctx.raw).parent := by + rw [PatternRewriter.eraseOp_ctx_eq herase]; exact RegionPtr.parent!_wfRewriter_eraseOp + exact hers.trans (hrep.trans (hins.trans hcre)) + exact OperationPtr.getParentOp!_eq_of rewriter.ctx.wellFormed.inBounds oIn hop hblk hrgn hpar -- `op` is not a function: it has no regions, so in particular not exactly one. opNotFunction := by simp [hOpRegions] } diff --git a/Veir/Verifier.lean b/Veir/Verifier.lean index 820c705ae..2c9018e9f 100644 --- a/Veir/Verifier.lean +++ b/Veir/Verifier.lean @@ -2635,5 +2635,15 @@ theorem OperationPtr.Verified.arith_addi {op : OperationPtr} {opInBounds} simp only [TypeAttr.inj] grind +/-- A verified `func.func` operation has exactly one region (its body). This is what lets the rewrite +soundness lift conclude that a `func.func` survivor is distinct from the matched, region-free `op`. -/ +theorem OperationPtr.Verified.func_func {op : OperationPtr} {opInBounds} + (opVerify : op.Verified ctx opInBounds) (opType : op.getOpType! ctx.raw = .func .func) : + op.getNumRegions! ctx.raw = 1 := by + simp only [Verified, verifyLocalInvariants, ← getOpType!_eq_getOpType, opType, ne_eq, + bind, Except.bind, throw, throwThe, MonadExceptOf.throw, pure, Except.pure, + ite_not] at opVerify + grind + end end Veir From 6dc7068386dd19bd4850e33779f60b66e0d8bbcc Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Tue, 30 Jun 2026 19:07:15 +0200 Subject: [PATCH 27/31] --- Veir/PatternRewriter/Soundness.lean | 335 ++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 0f3b9329d..41ebe9f6c 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -4452,4 +4452,339 @@ theorem RewrittenAt.of_fromLocalRewrite opNotFunction := by simp [hOpRegions] } +/-! ### PR 8 foundation: list interpretation depends only on each op's local data + +`interpretOp` reads the context only through an operation's *local* data — its operands, type, +properties, result types, successors, and result pointers — plus the variable map and memory. It +never consults `.parent`, `.next`, or block membership. So if those data fields agree across two +contexts and the states share the same underlying variable map and memory, the runs are *literally +equal* (projected onto the context-independent data: the variable map, the memory, and the optional +control-flow action). This `SameData` frame is the key to PR 8: the driver's edits (insert `newOps` +before `op`, redirect `op`'s results, erase `op`) are inert on `newOps`' interpreted data, so a run +of `newOps` transports unchanged from the create-only `newCtxPat` to the inserted/erased +`rewriter'.ctx`. -/ + +/-- The context-independent data of an interpreter-state/action result: the variable map, the memory, +and the optional control-flow action. Two `interpretOp`/`interpretOpList` results "agree as data" when +their projections under `interpProj` are equal — this is meaningful *across* contexts, where the +result states have different types but a common underlying `ExtHashMap`/`MemoryState`. -/ +def interpProj {ctx : WfIRContext OpCode} : + Interp (InterpreterState ctx × Option ControlFlowAction) → + Option (UBOr (Std.ExtHashMap ValuePtr RuntimeValue × MemoryState × Option ControlFlowAction)) := + Option.map (UBOr.map (fun p => (p.1.variables.variables, p.1.memory, p.2))) + +/-- Cross-context agreement of `setVar?` on the underlying variable map: with the same starting map +and the same target-type (`getType!` agrees), the conformance check and the insert coincide. -/ +theorem VariableState.setVar?_variables_eq_of_dataEq + {ctxA ctxB : WfIRContext OpCode} {var : ValuePtr} {val : RuntimeValue} + {varStateA : VariableState ctxA} {varStateB : VariableState ctxB} {iA iB} + (hVars : varStateA.variables = varStateB.variables) + (hType : var.getType! ctxA.raw = var.getType! ctxB.raw) : + (varStateA.setVar? var val iA).map (·.variables) + = (varStateB.setVar? var val iB).map (·.variables) := by + simp only [VariableState.setVar?, hType, hVars] + split <;> simp + +/-- Cross-context agreement of `setResultValues?` on the underlying variable map: the loop sets the +context-independent result pointers `op.getResult i` to the same values with the same per-result +conformance check (`getType!` agrees), so it produces the same map (or fails on both sides). -/ +theorem VariableState.setResultValues?_loop_variables_eq_of_dataEq + {ctxA ctxB : WfIRContext OpCode} {op : OperationPtr} {resValues : Array RuntimeValue} + {varStateA : VariableState ctxA} {varStateB : VariableState ctxB} + (hVars : varStateA.variables = varStateB.variables) + (hType : ∀ i, (ValuePtr.opResult (op.getResult i)).getType! ctxA.raw + = (ValuePtr.opResult (op.getResult i)).getType! ctxB.raw) + {idx : Nat} {iA hiA hsA iB hiB hsB} : + (varStateA.setResultValues?_loop op resValues idx iA hiA hsA).map (·.variables) + = (varStateB.setResultValues?_loop op resValues idx iB hiB hsB).map (·.variables) := by + induction idx generalizing varStateA varStateB with + | zero => simp [VariableState.setResultValues?_loop, hVars] + | succ i ih => + simp only [VariableState.setResultValues?_loop, Option.bind_eq_bind] + have hstep := VariableState.setVar?_variables_eq_of_dataEq (val := resValues[i]) + (varStateA := varStateA) (varStateB := varStateB) (iA := by grind) (iB := by grind) + hVars (hType i) + rcases hA : varStateA.setVar? (op.getResult i) resValues[i] with _ | varStateA' <;> + rcases hB : varStateB.setVar? (op.getResult i) resValues[i] with _ | varStateB' <;> + simp only [hA, hB, Option.map_none, Option.map_some] at hstep + · rfl + · exact absurd hstep (by simp) + · exact absurd hstep (by simp) + · exact ih (by simpa using hstep) + +/-- Cross-context agreement of `setResultValues?` on the underlying variable map. -/ +theorem VariableState.setResultValues?_variables_eq_of_dataEq + {ctxA ctxB : WfIRContext OpCode} {op : OperationPtr} {resValues : Array RuntimeValue} + {varStateA : VariableState ctxA} {varStateB : VariableState ctxB} {iA iB} + (hVars : varStateA.variables = varStateB.variables) + (hNum : op.getNumResults! ctxA.raw = op.getNumResults! ctxB.raw) + (hType : ∀ i, (ValuePtr.opResult (op.getResult i)).getType! ctxA.raw + = (ValuePtr.opResult (op.getResult i)).getType! ctxB.raw) : + (varStateA.setResultValues? op resValues iA).map (·.variables) + = (varStateB.setResultValues? op resValues iB).map (·.variables) := by + simp only [VariableState.setResultValues?, hNum] + split + · exact VariableState.setResultValues?_loop_variables_eq_of_dataEq hVars hType + · rfl + +/-- **`interpretOp` data-equality frame.** If an operation's local data (operands, type, properties, +result types, successors) and its result pointers' types agree across two contexts, and the two +states share the same underlying variable map and memory, then the two `interpretOp` runs agree under +`interpProj` — i.e. they produce the same variable map, memory, and control-flow action (or both fail +identically). No parents/dominance involved. -/ +theorem interpretOp_interpProj_eq_of_dataEq + {ctxA ctxB : WfIRContext OpCode} {op : OperationPtr} {oInA : op.InBounds ctxA.raw} + {oInB : op.InBounds ctxB.raw} + (hOpType : op.getOpType! ctxA.raw = op.getOpType! ctxB.raw) + (hProps : op.getProperties! ctxA.raw (op.getOpType! ctxA.raw) + = hOpType ▸ op.getProperties! ctxB.raw (op.getOpType! ctxB.raw)) + (hResTypes : op.getResultTypes! ctxA.raw = op.getResultTypes! ctxB.raw) + (hSucc : op.getSuccessors! ctxA.raw = op.getSuccessors! ctxB.raw) + (hOperands : op.getOperands! ctxA.raw = op.getOperands! ctxB.raw) + (hNum : op.getNumResults! ctxA.raw = op.getNumResults! ctxB.raw) + (hType : ∀ i, (ValuePtr.opResult (op.getResult i)).getType! ctxA.raw + = (ValuePtr.opResult (op.getResult i)).getType! ctxB.raw) + {sA : InterpreterState ctxA} {sB : InterpreterState ctxB} + (hVars : sA.variables.variables = sB.variables.variables) + (hMem : sA.memory = sB.memory) : + interpProj (interpretOp op sA oInA) = interpProj (interpretOp op sB oInB) := by + -- Operand values agree: same operand pointers (`hOperands`), same map (`getVar?` is a pure lookup). + have hgv : sA.variables.getVar? = sB.variables.getVar? := by + funext v; simp only [VariableState.getVar?, hVars] + have hOps : sA.variables.getOperandValues op = sB.variables.getOperandValues op := by + simp only [VariableState.getOperandValues, hOperands, hgv] + -- The pure `interpretOp'` step agrees (same opType/props/resultTypes/successors/operands/memory). + have hInterp : ∀ ov mem, op.interpret ctxA.raw ov mem = op.interpret ctxB.raw ov mem := by + intro ov mem + simp only [OperationPtr.interpret, hResTypes, hSucc] + exact interpretOp'_opType_cast hOpType hProps + -- `setResultValues?` agrees on the underlying map for any result values / in-bounds proofs. + have hsetMap : ∀ (rv : Array RuntimeValue) p q, + (sA.variables.setResultValues? op rv p).map (·.variables) + = (sB.variables.setResultValues? op rv q).map (·.variables) := + fun rv _ _ => VariableState.setResultValues?_variables_eq_of_dataEq hVars hNum hType + -- A successful (`ok`) run is mirrored across the two contexts, componentwise (operands agree, the + -- pure step agrees, memory agrees, and `setResultValues?` agrees on the variable map). Stated + -- generically so it applies in *both* directions (the four data hypotheses are symmetric). + have okMirror : ∀ {ctx1 ctx2 : WfIRContext OpCode} (s1 : InterpreterState ctx1) + (s2 : InterpreterState ctx2) (oi1 : op.InBounds ctx1.raw) (oi2 : op.InBounds ctx2.raw) st1 act1, + s1.variables.getOperandValues op = s2.variables.getOperandValues op → + (∀ ov m, op.interpret ctx1.raw ov m = op.interpret ctx2.raw ov m) → + s1.memory = s2.memory → + (∀ rv p q, (s1.variables.setResultValues? op rv p).map (·.variables) + = (s2.variables.setResultValues? op rv q).map (·.variables)) → + interpretOp op s1 oi1 = some (.ok (st1, act1)) → + ∃ st2, interpretOp op s2 oi2 = some (.ok (st2, act1)) ∧ + st2.variables.variables = st1.variables.variables ∧ st2.memory = st1.memory := by + intro ctx1 ctx2 s1 s2 oi1 oi2 st1 act1 hop hin hmem hset h + obtain ⟨ov, rv, mem', vs', hov, hint, hsetv, rfl⟩ := interpretOp_some_iff.mp h + have hov2 : s2.variables.getOperandValues op = some ov := hop ▸ hov + have hint2 : op.interpret ctx2.raw ov s2.memory = some (.ok (rv, mem', act1)) := by + rw [← hin ov s2.memory, ← hmem]; exact hint + have hsetv' : s1.variables.setResultValues? op rv oi1 = some vs' := hsetv + have hsetEq := hset rv oi1 oi2 + rw [hsetv', Option.map_some] at hsetEq + rcases hsB : s2.variables.setResultValues? op rv oi2 with _ | vs2 + · rw [hsB, Option.map_none] at hsetEq; exact absurd hsetEq.symm (by simp) + · refine ⟨⟨vs2, mem'⟩, interpretOp_some_iff.mpr ⟨ov, rv, mem', vs2, hov2, hint2, hsB, rfl⟩, ?_, rfl⟩ + rw [hsB, Option.map_some] at hsetEq + exact (Option.some_inj.mp hsetEq).symm + -- A `ub` run is likewise mirrored (operands agree, the pure step's `ub` agrees). + have ubMirror : ∀ {ctx1 ctx2 : WfIRContext OpCode} (s1 : InterpreterState ctx1) + (s2 : InterpreterState ctx2) (oi1 : op.InBounds ctx1.raw) (oi2 : op.InBounds ctx2.raw), + s1.variables.getOperandValues op = s2.variables.getOperandValues op → + (∀ ov m, op.interpret ctx1.raw ov m = op.interpret ctx2.raw ov m) → + s1.memory = s2.memory → + interpretOp op s1 oi1 = some .ub → interpretOp op s2 oi2 = some .ub := by + intro ctx1 ctx2 s1 s2 oi1 oi2 hop hin hmem h + obtain ⟨ov, hov, hint⟩ := interpretOp_ub_iff.mp h + refine interpretOp_ub_iff.mpr ⟨ov, hop ▸ hov, ?_⟩ + rw [← hin ov s2.memory, ← hmem]; exact hint + -- Dispatch on the source result and mirror onto the target. + rcases hA : interpretOp op sA oInA with _ | (⟨stA, actA⟩ | _) + · -- A `none`: the target is `none` too — otherwise mirroring back would make A succeed. + rcases hB : interpretOp op sB oInB with _ | (⟨stB, actB⟩ | _) + · simp [interpProj] + · obtain ⟨stA', hA', _⟩ := okMirror sB sA oInB oInA stB actB hOps.symm + (fun ov m => (hInterp ov m).symm) hMem.symm (fun rv p q => (hsetMap rv q p).symm) hB + rw [hA] at hA'; exact absurd hA' (by simp) + · exact absurd (ubMirror sB sA oInB oInA hOps.symm (fun ov m => (hInterp ov m).symm) hMem.symm hB + |>.symm.trans hA) (by simp) + · obtain ⟨stB, hB, hvars, hmem⟩ := okMirror sA sB oInA oInB stA actA hOps hInterp hMem hsetMap hA + simp only [interpProj, hB, Option.map_some, UBOr.map, hvars, hmem] + · rw [ubMirror sA sB oInA oInB hOps hInterp hMem hA] + rfl + +/-- Two interpreter states "agree as data" when they share the underlying variable map and memory. +Phantom-typed by their (possibly different) contexts. -/ +def InterpreterState.SameData {ctxA ctxB : WfIRContext OpCode} + (sA : InterpreterState ctxA) (sB : InterpreterState ctxB) : Prop := + sA.variables.variables = sB.variables.variables ∧ sA.memory = sB.memory + +/-- The local data of an operation agrees across two contexts: its type, properties, result types, +successors, operand pointers, result count, and result-pointer types coincide. This is exactly what +`interpretOp`/`interpretOpList` consume, so it is the precondition of the data-equality frame. -/ +structure OpDataEq (op : OperationPtr) (ctxA ctxB : WfIRContext OpCode) : Prop where + opType : op.getOpType! ctxA.raw = op.getOpType! ctxB.raw + props : op.getProperties! ctxA.raw (op.getOpType! ctxA.raw) + = opType ▸ op.getProperties! ctxB.raw (op.getOpType! ctxB.raw) + resTypes : op.getResultTypes! ctxA.raw = op.getResultTypes! ctxB.raw + succ : op.getSuccessors! ctxA.raw = op.getSuccessors! ctxB.raw + operands : op.getOperands! ctxA.raw = op.getOperands! ctxB.raw + numResults : op.getNumResults! ctxA.raw = op.getNumResults! ctxB.raw + resElemType : ∀ i, (ValuePtr.opResult (op.getResult i)).getType! ctxA.raw + = (ValuePtr.opResult (op.getResult i)).getType! ctxB.raw + +/-- `interpProj` inverts at an `ok` outcome: the projection is `ok (vars, mem, act)` exactly when the +run is `ok (st, act)` for some state `st` whose data is `(vars, mem)`. -/ +theorem interpProj_eq_ok_iff {ctx : WfIRContext OpCode} + {X : Interp (InterpreterState ctx × Option ControlFlowAction)} + {vars : Std.ExtHashMap ValuePtr RuntimeValue} {mem : MemoryState} {act : Option ControlFlowAction} : + interpProj X = some (.ok (vars, mem, act)) ↔ + ∃ st, X = some (.ok (st, act)) ∧ st.variables.variables = vars ∧ st.memory = mem := by + rcases X with _ | (⟨st, a⟩ | _) <;> simp [interpProj, UBOr.map] <;> grind + +/-- Per-op `ok`-transport: under `OpDataEq` and same-data entry states, a source `ok` step is mirrored +by a target `ok` step with the same control-flow action and same-data result. -/ +theorem interpretOp_sameData_transport {ctxA ctxB : WfIRContext OpCode} {op : OperationPtr} + {sA : InterpreterState ctxA} {sB : InterpreterState ctxB} + (hData : OpDataEq op ctxA ctxB) (hsame : sA.SameData sB) + {oInA : op.InBounds ctxA.raw} {oInB : op.InBounds ctxB.raw} + {st : InterpreterState ctxA} {act : Option ControlFlowAction} + (h : interpretOp op sA oInA = some (.ok (st, act))) : + ∃ st', interpretOp op sB oInB = some (.ok (st', act)) ∧ st.SameData st' := by + have he := interpretOp_interpProj_eq_of_dataEq (oInA := oInA) (oInB := oInB) + hData.opType hData.props hData.resTypes hData.succ hData.operands hData.numResults + hData.resElemType hsame.1 hsame.2 + rw [h] at he + have hP : interpProj (interpretOp op sB oInB) = some (.ok (st.variables.variables, st.memory, act)) := by + rw [← he]; simp [interpProj, UBOr.map] + obtain ⟨st', hB, hv, hm⟩ := interpProj_eq_ok_iff.mp hP + exact ⟨st', hB, hv.symm, hm.symm⟩ + +/-- **`interpretOpList` data-equality transport.** Over an identical op list whose every operation has +matching local data across the two contexts, a successful (`ok`) run transports from same-data entry +states to same-data result states, preserving the control-flow action. The driver's edits are inert on +`newOps`' data, so this is what carries a `newOps` run from `newCtxPat` to `rewriter'.ctx`. -/ +theorem interpretOpList_sameData_transport {ctxA ctxB : WfIRContext OpCode} {ops : List OperationPtr} + (oInA : ∀ o ∈ ops, o.InBounds ctxA.raw) (oInB : ∀ o ∈ ops, o.InBounds ctxB.raw) + (hData : ∀ o ∈ ops, OpDataEq o ctxA ctxB) + {sA : InterpreterState ctxA} {sB : InterpreterState ctxB} (hsame : sA.SameData sB) + {sA2 : InterpreterState ctxA} {cf : Option ControlFlowAction} + (h : interpretOpList ops sA oInA = some (.ok (sA2, cf))) : + ∃ sB2, interpretOpList ops sB oInB = some (.ok (sB2, cf)) ∧ sA2.SameData sB2 := by + induction ops generalizing sA sB with + | nil => + rw [interpretOpList_nil] at h + injection h with h1; injection h1 with h2; rw [Prod.mk.injEq] at h2 + obtain ⟨rfl, rfl⟩ := h2 + exact ⟨sB, interpretOpList_nil, hsame⟩ + | cons a l ih => + rw [interpretOpList_cons] at h + rcases hA : interpretOp a sA (oInA a (by simp)) with _ | (⟨sMid, act⟩ | _) + · rw [hA] at h; injection h + · obtain ⟨sMidB, hB, hsmid⟩ := + interpretOp_sameData_transport (hData a (by simp)) hsame (oInB := oInB a (by simp)) hA + rw [interpretOpList_cons, hB] + rw [hA] at h + cases act with + | none => + exact ih (fun o ho => oInA o (by simp [ho])) (fun o ho => oInB o (by simp [ho])) + (fun o ho => hData o (by simp [ho])) hsmid h + | some c => + injection h with h1; injection h1 with h2; rw [Prod.mk.injEq] at h2 + obtain ⟨rfl, rfl⟩ := h2 + exact ⟨sMidB, rfl, hsmid⟩ + · rw [hA] at h; injection h with h1; injection h1 + +/-! ### PR 9, final bridge: the driver refines every module + +Composing the two endpoints — `RewrittenAt.of_fromLocalRewrite` (the driver's net edit *is* a +`RewrittenAt` instance) and `RewrittenAt.isModuleRefinedBy` (a `RewrittenAt` instance refines every +module) — gives the headline driver-level soundness statement: running `fromLocalRewrite` for a +matched, in-bounds `op` on a `Valid` pattern over a verified, dominance-wellformed context refines +every module. + +This is the **composition skeleton**: the easy side-conditions of `isModuleRefinedBy` are discharged +here (`ctxVerif`/`hCtxDom` are the source hypotheses; `newCtxVerif` is the pattern's +`rewritePreservesVerified` obligation applied to the driver run). The remaining four are the +*semantic* bridges, left as `sorry` for now: +* `hOpSim` — bridge the pattern's value-level `PreservesSemantics` to the scoped + `OpStepSimulation` on `hRW.σ` (requires `hRW.σ = LocalRewritePattern.mapping hpat`); +* `hSrcSplit`/`hTgtSplit` — every verified block is `front ++ [terminator]` with `front` never + yielding a control-flow action; +* `hSrcInv`/`hTgtInv` — the function-entry base case: the empty state with entry block-arguments set + satisfies `EquationLemmaAt`/`DefinesDominating` at the entry point. +-/ +theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite + {pattern : LocalRewritePattern OpCode} + (hValid : pattern.Valid) + (hSrcDom : rewriter.ctx.Dom) + (hSrcVerif : rewriter.ctx.Verified) + (hpat : pattern rewriter.ctx op = some (newCtxPat, some (newOps, newValues))) + (hdriver : RewritePattern.fromLocalRewrite pattern rewriter op opInBounds = some rewriter') + (hSrcSplit : ∀ (b : BlockPtr) (bIn : b.InBounds rewriter.ctx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds rewriter.ctx.raw) (_termIn : term.InBounds rewriter.ctx.raw), + (b.operationList rewriter.ctx.raw rewriter.ctx.wellFormed bIn).toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState rewriter.ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + (hTgtSplit : ∀ (b : BlockPtr) (bIn' : b.InBounds rewriter'.ctx.raw), + ∃ (front : List OperationPtr) (term : OperationPtr) + (frontIn : ∀ o ∈ front, o.InBounds rewriter'.ctx.raw) (_termIn : term.InBounds rewriter'.ctx.raw), + (b.operationList rewriter'.ctx.raw rewriter'.ctx.wellFormed bIn').toList = front ++ [term] ∧ + (∀ (s s' : InterpreterState rewriter'.ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf)))) + (hSrcInv : ∀ (_funcOp : OperationPtr) (values : Array RuntimeValue) (mem : MemoryState) + (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds rewriter.ctx.raw) + (newVars : VariableState rewriter.ctx), + (VariableState.empty rewriter.ctx).setArgumentValues? entryBlock values entryIn = some newVars → + ∀ fst (hfst : (entryBlock.get! rewriter.ctx.raw).firstOp = some fst), + (InterpreterState.mk newVars mem).EquationLemmaAt (.before fst) + (by have := rewriter.ctx.wellFormed.inBounds; grind)) + (hTgtInv : ∀ (_funcOp : OperationPtr) (values' : Array RuntimeValue) (mem : MemoryState) + (entryBlock : BlockPtr) (entryIn' : entryBlock.InBounds rewriter'.ctx.raw) + (newVars' : VariableState rewriter'.ctx), + (VariableState.empty rewriter'.ctx).setArgumentValues? entryBlock values' entryIn' = some newVars' → + (InterpreterState.mk newVars' mem).DefinesDominating + (InsertPoint.atStart! entryBlock rewriter'.ctx.raw) + ((InsertPoint.inBounds_atStart! rewriter'.ctx.wellFormed entryIn').mpr entryIn')) + {moduleOp : OperationPtr} : + moduleOp.isModuleRefinedBy rewriter.ctx moduleOp rewriter'.ctx := by + -- The driver's net edit is a `RewrittenAt` instance. + obtain ⟨block, pre, post, blockIn, blockIn', hOpParent, hRW⟩ := + RewrittenAt.of_fromLocalRewrite hValid hSrcDom hSrcVerif hpat hdriver + -- The target context is verified: the pattern's `rewritePreservesVerified` obligation propagates + -- source verification across the driver run. + have newCtxVerif : rewriter'.ctx.Verified := + hValid.rewritePreservesVerified rewriter op opInBounds rewriter' hdriver hSrcVerif + -- `RewrittenAt.isModuleRefinedBy` consumes the instance; the easy well-formedness side-conditions + -- are discharged here, the four semantic bridges remain. + -- `hSrcSplit`/`hTgtSplit`/`hSrcInv` discharge positionally; `hTgtInv` is restated with a target + -- in-bounds binder, so reroute the source binder through `hRW.blocksInBounds`. + refine hRW.isModuleRefinedBy hSrcVerif newCtxVerif hSrcDom ?hOpSim hSrcSplit hTgtSplit hSrcInv + ?hTgtInv + case hTgtInv => + intro _funcOp values' mem entryBlock entryIn newVars' h + exact hTgtInv _funcOp values' mem entryBlock (hRW.blocksInBounds entryBlock entryIn) newVars' h + case hOpSim => + -- === PR 8: bridge `pattern.PreservesSemantics` to `OpStepSimulation op newOps hRW.σ`. === + -- `hValid.preservesSemantics` (at `ctx := rewriter.ctx`) supplies the *source* side exactly: + -- `interpretOp op` in `rewriter.ctx` refined by `interpretOpList newOps` in the create-only + -- `newCtxPat`. The target side is transported with the **data-equality frame** above + -- (`interpretOpList_sameData_transport`): `interpretOp`/`interpretOpList` depend only on each op's + -- local data (operands/type/properties/result-types/successors/results), never on parents or + -- dominance, so a `newOps` run transports unchanged between two contexts whose op-data agree — + -- sidestepping the scoped-refinement/parent obstruction that blocked the earlier attempts. + -- + -- Remaining to finish `hOpSim`: + -- (1) `OpDataEq newOp newCtxPat rewriter'.ctx` for every `newOp` — all fields hold across the + -- driver's insert/redirect/erase edits; `operands` needs that `newOps` do not reference `op`'s + -- results, which is *forced* by the successful `newCtxPat` run when the entry state is built + -- from `s'` (where `op` is erased, so its results are not in scope), making the redirect inert. + -- (2) Build the `newCtxPat` entry state from `s'` (`SameData`) and apply `preservesSemantics`. + -- (3) Reconcile `LocalRewritePattern.mapping hpat` with `hRW.σ` and the `(.before op, p')` ↔ + -- `(.before op, .before op)` scopes to land the `Interp.isRefinedBy` goal. + sorry + end Veir From bef7d1ac031b36a1c34d1f9a8790e790b3eeb0e0 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Wed, 1 Jul 2026 13:55:36 +0200 Subject: [PATCH 28/31] Thread EquationLemmaAt --- Veir/PatternRewriter/Soundness.lean | 216 +++++++++++++++++++++++++++- 1 file changed, 209 insertions(+), 7 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 41ebe9f6c..9f4977fbc 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -105,6 +105,29 @@ theorem rewriteMapping_resultsInBounds {ctx newCtx : WfIRContext OpCode} rw [getElem!_pos newValues index hsize] exact newValuesInBounds _ (Array.getElem_mem hsize) +/-- `LocalRewritePattern.mapping` and `rewriteMapping` (for the same `op`/`newValues`) agree on the +underlying value pointer, regardless of their (possibly different) target contexts: both fix every +non-result and send `op`'s `i`-th result onto `newValues`'s `i`-th entry. The two mappings have +different codomains (`mapping`'s is the pattern's create-only context, `rewriteMapping`'s is the +driver's edited context), so only the `.val` projection — which is context-independent — is compared. +This bridges the `PreservesSemantics` refinement (stated with `mapping`) to `hRW.σ`. -/ +theorem mapping_val_eq_rewriteMapping_val {ctx newCtxA newCtxB : WfIRContext OpCode} + {pattern : LocalRewritePattern OpCode} {op : OperationPtr} + {newOps : Array OperationPtr} {newValues : Array ValuePtr} + (hpat : pattern ctx op = some (newCtxA, some (newOps, newValues))) + (hRVIB : pattern.ReturnValuesInBounds) (hRV : pattern.ReturnValues) + (hRCC : pattern.ReturnCtxChanges) + {mapResultsInBounds : ∀ (index : Nat), index < (op.getResults! ctx.raw).size → + newValues[index]!.InBounds newCtxB.raw} + {mapNonResultsInBounds : ∀ (v : ValuePtr), v.InBounds ctx.raw → + v ∉ op.getResults! ctx.raw → v.InBounds newCtxB.raw} + {v : ValuePtr} (vIn1 : v.InBounds ctx.raw) (vIn2 : v.InBounds ctx.raw) : + (LocalRewritePattern.mapping hpat hRVIB hRV hRCC ⟨v, vIn1⟩).val + = (rewriteMapping op newValues mapResultsInBounds mapNonResultsInBounds ⟨v, vIn2⟩).val := by + by_cases h : v ∈ op.getResults! ctx.raw + · simp only [LocalRewritePattern.mapping, rewriteMapping, dif_pos h] + · simp only [LocalRewritePattern.mapping, rewriteMapping, dif_neg h] + /-- `RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn'` asserts that `newCtx` is obtained from `ctx` by the single local rewrite that replaces `op` with `newOps` @@ -620,6 +643,7 @@ def OpStepSimulation (q'In : (InsertPoint.afterLast newOps.toList newCtx.raw p').InBounds newCtx.raw), s.isRefinedByAt s' μ (InsertPoint.before op) p' → s.EquationLemmaAt (InsertPoint.before op) → + s'.EquationLemmaAt p' p'In → Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Option ControlFlowAction) (r₂ : InterpreterState newCtx × Option ControlFlowAction) => @@ -839,6 +863,53 @@ theorem interpretOpList_DefinesDominating_before {ctx : WfIRContext OpCode} simp only [InsertPoint.after_eq_of_some_next (hChain.getLast?_next_eq hlast)] at hDom exact hDom +/-- Landing `EquationLemmaAt` at the `afterLast` end point of a block prefix. Running a block-prefix +`ops` (a chain slice starting at the block entry) from a state satisfying the SSA invariant at the +block start yields a state satisfying it at `afterLast ops (atStart! block)` — the point reached just +after the prefix. This is the target-side feeder for `OpStepSimulation`'s new `s'.EquationLemmaAt p'` +hypothesis, where `p'` is the `newOps` insertion point `afterLast pre (atStart! block)`. -/ +theorem interpretOpList_equationLemmaAt_afterLast_atStart {ctx : WfIRContext OpCode} + {block : BlockPtr} {ops : List OperationPtr} {state state' : InterpreterState ctx} + (ctxDom : ctx.Dom) (bIn : block.InBounds ctx.raw) + (opsIn : ∀ op ∈ ops, op.InBounds ctx.raw) + (hChain : block.OpChainSlice ctx.raw ops) + (hFirstOfHead : ∀ h, ops.head? = some h → (block.get! ctx.raw).firstOp = some h) + (hstart : state.EquationLemmaAt (InsertPoint.atStart! block ctx.raw) + ((InsertPoint.inBounds_atStart! ctx.wellFormed bIn).mpr bIn)) + (pIn : (InsertPoint.afterLast ops ctx.raw (InsertPoint.atStart! block ctx.raw)).InBounds ctx.raw) + (hrun : interpretOpList ops state opsIn = some (.ok (state', none))) : + state'.EquationLemmaAt + (InsertPoint.afterLast ops ctx.raw (InsertPoint.atStart! block ctx.raw)) pIn := by + cases ops with + | nil => + rw [interpretOpList_nil] at hrun + obtain rfl : state = state' := by grind + simpa only [InsertPoint.afterLast_nil] using hstart + | cons a t => + obtain ⟨lastOp, hlast⟩ : ∃ lastOp, (a :: t).getLast? = some lastOp := by + cases hg : (a :: t).getLast? with + | none => simp at hg + | some x => exact ⟨x, rfl⟩ + have hfa : (block.get! ctx.raw).firstOp = some a := hFirstOfHead a (by simp) + have eqPtS : ∀ {p₁ p₂ : InsertPoint} {w1 : p₁.InBounds ctx.raw} {w2 : p₂.InBounds ctx.raw}, + p₁ = p₂ → state.EquationLemmaAt p₁ w1 → state.EquationLemmaAt p₂ w2 := by + intro p₁ p₂ w1 w2 hp h; subst hp; exact h + have hstart' : state.EquationLemmaAt (InsertPoint.before a) := + eqPtS (by simp only [InsertPoint.atStart!, hfa]) hstart + have hEq := interpretOpList_equationLemmaAt ctxDom hChain (by simp) hstart' hlast hrun + have lastIn : lastOp.InBounds ctx.raw := + hChain.inBounds_of_mem lastOp (List.mem_of_getLast? hlast) + have lastParent : (lastOp.get! ctx.raw).parent = some block := + hChain.parent_of_mem lastOp (List.mem_of_getLast? hlast) + have hpt : InsertPoint.afterLast (a :: t) ctx.raw (InsertPoint.atStart! block ctx.raw) + = InsertPoint.after lastOp ctx.raw block := by + simp only [InsertPoint.afterLast, hlast] + rw [InsertPoint.after!_eq_after lastParent lastIn] + have eqPt : ∀ {p₁ p₂ : InsertPoint} {w1 : p₁.InBounds ctx.raw} {w2 : p₂.InBounds ctx.raw}, + p₁ = p₂ → state'.EquationLemmaAt p₁ w1 → state'.EquationLemmaAt p₂ w2 := by + intro p₁ p₂ w1 w2 hp h; subst hp; exact h + exact eqPt hpt.symm hEq + /-- Interpreting a singleton operation list is interpreting the operation. -/ theorem interpretOpList_singleton {ctx : WfIRContext OpCode} {op : OperationPtr} {state : InterpreterState ctx} (h : ∀ o ∈ [op], o.InBounds ctx.raw) : @@ -1098,6 +1169,8 @@ theorem RewrittenAt.blockSimulation ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn')) (hEqLem : ∀ fst (hfst : (block.get! ctx.raw).firstOp = some fst), state.EquationLemmaAt (.before fst)) + (hTgtEqLem : state'.EquationLemmaAt (InsertPoint.atStart! block newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed blockIn').mpr blockIn')) (hSrcSplit : ∃ (front : List OperationPtr) (term : OperationPtr) (frontIn : ∀ o ∈ front, o.InBounds ctx.raw), (pre.toList ++ [op] ++ post.toList) = front ++ [term] ∧ @@ -1253,6 +1326,13 @@ theorem RewrittenAt.blockSimulation have hEq : s2.EquationLemmaAt (InsertPoint.before op) opIn := interpretOpList_equationLemmaAt_before hCtxDom hRW.preInBounds opIn hRW.preChainOp (fun fst _ hhd => hEqLem fst (by rw [hRW.srcFirstOp, List.head?_append, hhd]; rfl)) hrunS + -- Target `EquationLemmaAt` at the `newOps` insertion point, threading `hTgtEqLem` through `pre`. + have hTgtEq : s2'.EquationLemmaAt + (InsertPoint.afterLast pre.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) p'In := + interpretOpList_equationLemmaAt_afterLast_atStart ctxDom' blockIn' hRW.preInBounds' + hRW.preChain' + (fun h hh => by rw [hRW.tgtFirstOp, List.head?_append, List.head?_append, hh]; rfl) + hTgtEqLem p'In hrunT -- Transport the source scope point `afterLast pre (atStart!)` to `before op` (witness-free). have congrPt : ∀ {p₁ p₂ : InsertPoint} {w1 : p₁.InBounds ctx.raw} {w1' w2'} {w2 : p₂.InBounds ctx.raw}, p₁ = p₂ → @@ -1267,7 +1347,7 @@ theorem RewrittenAt.blockSimulation (InsertPoint.after!_inBounds ctx.wellFormed hRW.opParent opIn) (InsertPoint.afterLast_inBounds newCtx.wellFormed p'In hRW.newOpsParent' (fun o ho => hRW.newOpsInBounds' o ho)) - (congrPt hPreEndSrc hsRef) hEq + (congrPt hPreEndSrc hsRef) hEq hTgtEq rw [interpretOpList_singleton] have hP1 : InsertPoint.afterLast (pre.toList ++ [op]) ctx.raw (InsertPoint.atStart! block ctx.raw) = InsertPoint.after! op ctx.raw := by @@ -1499,6 +1579,12 @@ theorem RewrittenAt.interpretBlock_refinement (InsertPoint.atStart! b newCtx.raw) ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr (hRW.blocksInBounds b bIn))) + (hTgtEqInv : ∀ newVars', + state'.variables.setArgumentValues? b values' (hRW.blocksInBounds b bIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).EquationLemmaAt + (InsertPoint.atStart! b newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr + (hRW.blocksInBounds b bIn))) (hSrcSplitB : ∃ (front : List OperationPtr) (term : OperationPtr) (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (_termIn : term.InBounds ctx.raw), (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term] ∧ @@ -1573,7 +1659,9 @@ theorem RewrittenAt.interpretBlock_refinement (InterpreterState.mk newVars state.memory).EquationLemmaAt (.before fst) (by have := ctx.wellFormed.inBounds; grind) := fun fst hfst => hSrcInv newVars hsa fst hfst - have hbs := hRW.blockSimulation newCtxVerif hCtxDom hpsRef hTgtDD hEqLemArg hSplit hOpSim + have hTgtEqLemArg := hTgtEqInv newVars' hsa' + have hbs := hRW.blockSimulation newCtxVerif hCtxDom hpsRef hTgtDD hEqLemArg hTgtEqLemArg hSplit + hOpSim simp only [hsrc] at hSp simp only [hsrc, htgt] simp only [hSp, hTp] at hbs @@ -1854,6 +1942,52 @@ theorem interpretBlock_branch_definesDominating_succ_atStart exact InterpreterState.DefinesDominating.setArgumentValues?_succ_entry ctxDom bIn hSucc hAfterTerm hArgs +/-- **Target-side cross-block re-establishment of `EquationLemmaAt` (`atStart!` framing).** The +`EquationLemmaAt` analogue of `interpretBlock_branch_definesDominating_succ_atStart`: threads the SSA +invariant from `b`'s entry through its operation chain to the terminator's exit, then across the CFG +edge into `succ`, stating both the entry and re-established successor invariants at `atStart!`. -/ +theorem interpretBlock_branch_equationLemmaAt_succ_atStart + {ctx : WfIRContext OpCode} (ctxDom : ctx.Dom) + {b succ : BlockPtr} (bIn : b.InBounds ctx.raw) (succIn : succ.InBounds ctx.raw) + {values res : Array RuntimeValue} {state exitState : InterpreterState ctx} + {front : List OperationPtr} {term : OperationPtr} + (frontIn : ∀ o ∈ front, o.InBounds ctx.raw) (termIn : term.InBounds ctx.raw) + (harr : (b.operationList ctx.raw ctx.wellFormed bIn).toList = front ++ [term]) + (hFrontNoCf : ∀ (s s' : InterpreterState ctx) (cf : ControlFlowAction), + interpretOpList front s frontIn ≠ some (.ok (s', some cf))) + (hEntryInv : ∀ newVars, state.variables.setArgumentValues? b values bIn = some newVars → + (InterpreterState.mk newVars state.memory).EquationLemmaAt (.atStart! b ctx.raw) + (by have := ctx.wellFormed.inBounds; grind)) + (hRun : interpretBlock b values state bIn = some (.ok (exitState, .branch res succ))) : + ∀ newVars', exitState.variables.setArgumentValues? succ res succIn = some newVars' → + (InterpreterState.mk newVars' exitState.memory).EquationLemmaAt (.atStart! succ ctx.raw) + (by have := ctx.wellFormed.inBounds; grind) := by + intro newVars' hArgs + obtain ⟨newVars, s', hSetArgs, hFrontRun, hTermRun⟩ := + interpretBlock_branch_split bIn frontIn termIn harr hFrontNoCf hRun + obtain ⟨hparent, hnext, hchain, hfirst⟩ := operationList_split_term_facts bIn termIn harr + have hBeforeTerm : s'.EquationLemmaAt (.before term) termIn := + interpretOpList_equationLemmaAt_before ctxDom frontIn termIn hchain + (fun fst _ hhead => by + have hfo : (b.get! ctx.raw).firstOp = some fst := by rw [hfirst]; exact hhead + have heq := hEntryInv newVars hSetArgs + have hpt : InsertPoint.atStart! b ctx.raw = .before fst := by + simp [InsertPoint.atStart!, hfo] + simp only [hpt] at heq + exact heq) + hFrontRun + have hAfterTerm := interpretOp_equationLemmaAt ctxDom hBeforeTerm hparent hTermRun + have succMem : succ ∈ term.getSuccessors! ctx.raw := + interpretOp_branch_dest_mem_getSuccessors! hTermRun + have hlast : (b.get! ctx.raw).lastOp = some term := by grind + have hSucc : succ ∈ b.getSuccessors! ctx.raw := by + simp only [BlockPtr.getSuccessors!, hlast]; exact succMem + have hAtEnd : InsertPoint.after term ctx.raw b hparent termIn = InsertPoint.atEnd b := by + grind [InsertPoint.after] + simp only [hAtEnd] at hAfterTerm + exact InterpreterState.EquationLemmaAt.setArgumentValues?_succ_entry ctxDom bIn + hSucc hAfterTerm hArgs + /-- **Cross-edge transport of the scoped entry relation.** Given the full scoped state refinement at the predecessor's exit (`atEnd b`), produce the incoming-edge scoped relation at the successor's entry (`.blockEntry succ`). This is a pure scope-weakening (`isRefinedByAt.weaken`): a value in @@ -1942,6 +2076,12 @@ theorem RewrittenAt.interpretBlockCFG_refinement (hTgtInv : ∀ newVars', state'.variables.setArgumentValues? b values' (hRW.blocksInBounds b bIn) = some newVars' → (InterpreterState.mk newVars' state'.memory).DefinesDominating + (InsertPoint.atStart! b newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr + (hRW.blocksInBounds b bIn))) + (hTgtEqInv : ∀ newVars', + state'.variables.setArgumentValues? b values' (hRW.blocksInBounds b bIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).EquationLemmaAt (InsertPoint.atStart! b newCtx.raw) ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr (hRW.blocksInBounds b bIn))) : @@ -1967,13 +2107,19 @@ theorem RewrittenAt.interpretBlockCFG_refinement (InsertPoint.atStart! b newCtx.raw) ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr (hRW.blocksInBounds b bIn))) → + (∀ newVars', + state'.variables.setArgumentValues? b values' (hRW.blocksInBounds b bIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).EquationLemmaAt + (InsertPoint.atStart! b newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr + (hRW.blocksInBounds b bIn))) → Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState newCtx × Array RuntimeValue) => r₁.1.memory = r₂.1.memory ∧ r₁.2 ⊒ r₂.2) (g b values state bIn) (interpretBlockCFG b values' state' (hRW.blocksInBounds b bIn))) - ?admissible ?step b bIn values values' state state' hState hVals hSrcInv hTgtInv + ?admissible ?step b bIn values values' state state' hState hVals hSrcInv hTgtInv hTgtEqInv case admissible => -- Peel the (dependent) leading `∀ b` together with the `g b` application with -- `admissible_pi_apply`, the remaining (non-dependent) binders with `admissible_pi`, the @@ -1995,6 +2141,12 @@ theorem RewrittenAt.interpretBlockCFG_refinement (InsertPoint.atStart! b newCtx.raw) ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr (hRW.blocksInBounds b bIn))) → + (∀ newVars', + state'.variables.setArgumentValues? b values' (hRW.blocksInBounds b bIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).EquationLemmaAt + (InsertPoint.atStart! b newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds b bIn)).mpr + (hRW.blocksInBounds b bIn))) → Interp.isRefinedBy (fun (r₁ : InterpreterState ctx × Array RuntimeValue) (r₂ : InterpreterState newCtx × Array RuntimeValue) => @@ -2011,6 +2163,7 @@ theorem RewrittenAt.interpretBlockCFG_refinement apply Lean.Order.admissible_pi; intro hVals apply Lean.Order.admissible_pi; intro hSrcInv apply Lean.Order.admissible_pi; intro hTgtInv + apply Lean.Order.admissible_pi; intro hTgtEqInv apply Lean.Order.admissible_apply (P := fun (_v : Array RuntimeValue) (gv : InterpreterState ctx → b.InBounds ctx.raw → Interp (InterpreterState ctx × Array RuntimeValue)) => @@ -2039,9 +2192,9 @@ theorem RewrittenAt.interpretBlockCFG_refinement (x := bIn) exact Lean.Order.admissible_flatOrder _ trivial case step => - intro g IH b bIn values values' state state' hState hVals hSrcInv hTgtInv + intro g IH b bIn values values' state state' hState hVals hSrcInv hTgtInv hTgtEqInv have hBlk := hRW.interpretBlock_refinement newCtxVerif hCtxDom bIn hState hVals hSrcInv hTgtInv - (hSrcSplit b bIn) hOpSim + hTgtEqInv (hSrcSplit b bIn) hOpSim rw [interpretBlockCFG] rcases hsrc : interpretBlock b values state bIn with _ | (⟨s, act⟩ | _) · -- Source block run fails: the CFG step is `none`, refinement is trivial. @@ -2080,8 +2233,10 @@ theorem RewrittenAt.interpretBlockCFG_refinement (hRW.blocksInBounds succ hsuccIn) hsucc hsucc' hsRef have hTgtInvSucc := interpretBlock_branch_definesDominating_succ_atStart hRW.newCtxDom bIn' (hRW.blocksInBounds succ hsuccIn) frontInT termInT harrT hFrontNoCfT hTgtInv htgt + have hTgtEqInvSucc := interpretBlock_branch_equationLemmaAt_succ_atStart hRW.newCtxDom + bIn' (hRW.blocksInBounds succ hsuccIn) frontInT termInT harrT hFrontNoCfT hTgtEqInv htgt simp only [hsrc, htgt, dif_pos hsuccIn, dif_pos (hRW.blocksInBounds succ hsuccIn)] - exact IH succ hsuccIn r r' s s' hStateSucc hr hSrcInvSucc hTgtInvSucc + exact IH succ hsuccIn r r' s s' hStateSucc hr hSrcInvSucc hTgtInvSucc hTgtEqInvSucc · -- Successor out of bounds in the source: the source CFG step is `none`, refinement trivial. simp only [hsrc, dif_neg hsuccIn, Interp.isRefinedBy_none_target] · -- Source block run is UB, which is refined by any target outcome. @@ -2196,6 +2351,14 @@ theorem RewrittenAt.interpretRegion_refinement state'.variables.setArgumentValues? entryBlock values' (hRW.blocksInBounds entryBlock entryIn) = some newVars' → (InterpreterState.mk newVars' state'.memory).DefinesDominating + (InsertPoint.atStart! entryBlock newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed + (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) + (hTgtEqInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) + (newVars' : VariableState newCtx), + state'.variables.setArgumentValues? entryBlock values' + (hRW.blocksInBounds entryBlock entryIn) = some newVars' → + (InterpreterState.mk newVars' state'.memory).EquationLemmaAt (InsertPoint.atStart! entryBlock newCtx.raw) ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) : @@ -2238,6 +2401,7 @@ theorem RewrittenAt.interpretRegion_refinement (hState entryBlock entryIn) hVals (fun newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) (fun newVars' h => hTgtInv entryBlock entryIn newVars' h) + (fun newVars' h => hTgtEqInv entryBlock entryIn newVars' h) /-- **Stage E — `interpretFunction` refinement (monotonicity).** Interpreting a function operation @@ -2279,6 +2443,14 @@ theorem RewrittenAt.interpretFunction_refinement (VariableState.empty newCtx).setArgumentValues? entryBlock values' (hRW.blocksInBounds entryBlock entryIn) = some newVars' → (InterpreterState.mk newVars' mem).DefinesDominating + (InsertPoint.atStart! entryBlock newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed + (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) + (hTgtEqInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) + (newVars' : VariableState newCtx), + (VariableState.empty newCtx).setArgumentValues? entryBlock values' + (hRW.blocksInBounds entryBlock entryIn) = some newVars' → + (InterpreterState.mk newVars' mem).EquationLemmaAt (InsertPoint.atStart! entryBlock newCtx.raw) ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) : @@ -2318,6 +2490,7 @@ theorem RewrittenAt.interpretFunction_refinement hVals (fun entryBlock entryIn newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) (fun entryBlock entryIn newVars' h => hTgtInv entryBlock entryIn newVars' h) + (fun entryBlock entryIn newVars' h => hTgtEqInv entryBlock entryIn newVars' h) -- The function result keeps only the final memory and returned values of the region run. show Interp.isRefinedBy FunctionResult.isRefinedBy ((interpretRegion (funcOp.getRegion ctx.raw 0 funcOpIn hi) values ⟨.empty ctx, mem⟩ rIn) @@ -2367,6 +2540,14 @@ theorem RewrittenAt.interpretModule_refinement (VariableState.empty newCtx).setArgumentValues? entryBlock #[] (hRW.blocksInBounds entryBlock entryIn) = some newVars' → (InterpreterState.mk newVars' MemoryState.empty).DefinesDominating + (InsertPoint.atStart! entryBlock newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed + (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) + (hTgtEqInv : ∀ (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) + (newVars' : VariableState newCtx), + (VariableState.empty newCtx).setArgumentValues? entryBlock #[] + (hRW.blocksInBounds entryBlock entryIn) = some newVars' → + (InterpreterState.mk newVars' MemoryState.empty).EquationLemmaAt (InsertPoint.atStart! entryBlock newCtx.raw) ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) : @@ -2406,6 +2587,7 @@ theorem RewrittenAt.interpretModule_refinement (RuntimeValue.arrayIsRefinedBy_refl #[]) (fun entryBlock entryIn newVars h fst hfst => hSrcInv entryBlock entryIn newVars h fst hfst) (fun entryBlock entryIn newVars' h => hTgtInv entryBlock entryIn newVars' h) + (fun entryBlock entryIn newVars' h => hTgtEqInv entryBlock entryIn newVars' h) -- The module result keeps only the returned values of the region run. show Interp.isRefinedBy RuntimeValue.arrayIsRefinedBy ((interpretRegion (moduleOp.getRegion ctx.raw 0 moduleOpIn hi) #[] (InterpreterState.empty ctx) rIn) @@ -2457,6 +2639,14 @@ theorem RewrittenAt.isModuleRefinedBy (InsertPoint.atStart! entryBlock newCtx.raw) ((InsertPoint.inBounds_atStart! newCtx.wellFormed (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) + (hTgtEqInv : ∀ (_funcOp : OperationPtr) (values' : Array RuntimeValue) (mem : MemoryState) + (entryBlock : BlockPtr) (entryIn : entryBlock.InBounds ctx.raw) (newVars' : VariableState newCtx), + (VariableState.empty newCtx).setArgumentValues? entryBlock values' + (hRW.blocksInBounds entryBlock entryIn) = some newVars' → + (InterpreterState.mk newVars' mem).EquationLemmaAt + (InsertPoint.atStart! entryBlock newCtx.raw) + ((InsertPoint.inBounds_atStart! newCtx.wellFormed + (hRW.blocksInBounds entryBlock entryIn)).mpr (hRW.blocksInBounds entryBlock entryIn))) {moduleOp : OperationPtr} : moduleOp.isModuleRefinedBy ctx moduleOp newCtx := by intro func₁ func₁In name hTop @@ -2489,6 +2679,8 @@ theorem RewrittenAt.isModuleRefinedBy hSrcInv func₁ values mem entryBlock entryIn newVars h fst hfst) (fun entryBlock entryIn newVars' h => hTgtInv func₁ valuesTarget mem entryBlock entryIn newVars' h) + (fun entryBlock entryIn newVars' h => + hTgtEqInv func₁ valuesTarget mem entryBlock entryIn newVars' h) /-! ## PR 9: connecting the concrete driver `fromLocalRewrite` to `RewrittenAt` @@ -4749,6 +4941,13 @@ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite (InterpreterState.mk newVars' mem).DefinesDominating (InsertPoint.atStart! entryBlock rewriter'.ctx.raw) ((InsertPoint.inBounds_atStart! rewriter'.ctx.wellFormed entryIn').mpr entryIn')) + (hTgtEqInv : ∀ (_funcOp : OperationPtr) (values' : Array RuntimeValue) (mem : MemoryState) + (entryBlock : BlockPtr) (entryIn' : entryBlock.InBounds rewriter'.ctx.raw) + (newVars' : VariableState rewriter'.ctx), + (VariableState.empty rewriter'.ctx).setArgumentValues? entryBlock values' entryIn' = some newVars' → + (InterpreterState.mk newVars' mem).EquationLemmaAt + (InsertPoint.atStart! entryBlock rewriter'.ctx.raw) + ((InsertPoint.inBounds_atStart! rewriter'.ctx.wellFormed entryIn').mpr entryIn')) {moduleOp : OperationPtr} : moduleOp.isModuleRefinedBy rewriter.ctx moduleOp rewriter'.ctx := by -- The driver's net edit is a `RewrittenAt` instance. @@ -4763,10 +4962,13 @@ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite -- `hSrcSplit`/`hTgtSplit`/`hSrcInv` discharge positionally; `hTgtInv` is restated with a target -- in-bounds binder, so reroute the source binder through `hRW.blocksInBounds`. refine hRW.isModuleRefinedBy hSrcVerif newCtxVerif hSrcDom ?hOpSim hSrcSplit hTgtSplit hSrcInv - ?hTgtInv + ?hTgtInv ?hTgtEqInv case hTgtInv => intro _funcOp values' mem entryBlock entryIn newVars' h exact hTgtInv _funcOp values' mem entryBlock (hRW.blocksInBounds entryBlock entryIn) newVars' h + case hTgtEqInv => + intro _funcOp values' mem entryBlock entryIn newVars' h + exact hTgtEqInv _funcOp values' mem entryBlock (hRW.blocksInBounds entryBlock entryIn) newVars' h case hOpSim => -- === PR 8: bridge `pattern.PreservesSemantics` to `OpStepSimulation op newOps hRW.σ`. === -- `hValid.preservesSemantics` (at `ctx := rewriter.ctx`) supplies the *source* side exactly: From d48a8cdbb9179914542be49e00920793fedf1dc7 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Wed, 1 Jul 2026 14:25:21 +0200 Subject: [PATCH 29/31] Step 2 of last proof --- Veir/PatternRewriter/Soundness.lean | 197 +++++++++++++++++++++++++++- 1 file changed, 193 insertions(+), 4 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 9f4977fbc..c73241631 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -3995,7 +3995,11 @@ theorem RewrittenAt.of_fromLocalRewrite (blockIn : block.InBounds rewriter.ctx.raw) (blockIn' : block.InBounds rewriter'.ctx.raw), (op.get! rewriter.ctx.raw).parent = some block ∧ RewrittenAt rewriter.ctx op newOps newValues rewriter'.ctx opInBounds - block pre post blockIn blockIn' := by + block pre post blockIn blockIn' ∧ + (∀ o ∈ newOps.toList, + o.SameIntrinsic newCtxPat.raw rewriter'.ctx.raw ∧ + ((∀ v ∈ o.getOperands! newCtxPat.raw, v.InBounds rewriter'.ctx.raw) → + o.getOperands! newCtxPat.raw = o.getOperands! rewriter'.ctx.raw)) := by -- `op` has a parent block: the driver inserts every `newOp` *before* `op`, and an insertion before -- a parentless op fails, so a successful `fromLocalRewrite` witnesses `op`'s parent. This recovers -- the block (and the parent equation) that the previous statement took as a hypothesis. @@ -4220,8 +4224,8 @@ theorem RewrittenAt.of_fromLocalRewrite rw [PatternRewriter.eraseOp_ctx_eq herase] exact ValuePtr.getType!_wfRewriter_eraseOp (PatternRewriter.eraseOp_ctx_eq herase ▸ hvIn) rw [hErase, hRep, hIns, hCre] - refine ⟨block, pre, post, blockIn, blockIn', hOpParent, ?_⟩ - exact { + have hR : RewrittenAt rewriter.ctx op newOps newValues rewriter'.ctx opInBounds + block pre post blockIn blockIn' := { -- Block-list shape: discharged for the source by the split lemma. srcList := hsrcList -- Target list: the insert fold turns `pre ++ [op] ++ post` into `pre ++ newOps ++ [op] ++ post` @@ -4643,6 +4647,83 @@ theorem RewrittenAt.of_fromLocalRewrite -- `op` is not a function: it has no regions, so in particular not exactly one. opNotFunction := by simp [hOpRegions] } + -- **`newOps` frame** (PR 8): each created `newOp` carries its intrinsic data unchanged through the + -- insert/replace/erase pipeline, and — provided its operands survive into `rewriter'.ctx` — the + -- redirect fold is inert on it, because the only values the fold rewrites are `op`'s results, which + -- are erased and hence out of bounds in `rewriter'.ctx`. + refine ⟨block, pre, post, blockIn, blockIn', hOpParent, hR, ?_⟩ + intro o ho + have hoIn' : o.InBounds rewriter'.ctx.raw := hR.newOpsInBounds o (by simpa using ho) + have hoNewCtxPat : o.InBounds newCtxPat.raw := + ((hReturnOps rewriter.ctx op newCtxPat newOps newValues hpat o).mp (by simpa using ho)).1 + have hoErase := PatternRewriter.eraseOp_ctx_eq herase ▸ hoIn' + -- (1) Intrinsic data is framed from `newCtxPat` (creation) through the insert/replace folds and the + -- final erase. + have hins : o.SameIntrinsic newCtxPat.raw s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => o.SameIntrinsic newCtxPat.raw b.ctx.raw) + (fun b a b' hh => + ⟨fun hb => hb.trans (PatternRewriter.insertOp!_sameIntrinsic hh).symm, + fun hb => hb.trans (PatternRewriter.insertOp!_sameIntrinsic hh)⟩) hfold1 + exact h.mpr OperationPtr.SameIntrinsic.rfl + have hrep : o.SameIntrinsic s₁.ctx.raw s₂.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => o.SameIntrinsic s₁.ctx.raw b.ctx.raw) + (fun b a b' hh => by + have hst : o.SameIntrinsic b.ctx.raw b'.ctx.raw := + PatternRewriter.replaceValue!_sameIntrinsic hh + exact ⟨fun hb => hb.trans hst.symm, fun hb => hb.trans hst⟩) hfold2 + exact h.mpr OperationPtr.SameIntrinsic.rfl + have hers : o.SameIntrinsic s₂.ctx.raw rewriter'.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact ⟨OperationPtr.getOpType!_wfRewriter_eraseOp hoErase, + fun _ => OperationPtr.getProperties!_wfRewriter_eraseOp hoErase, + OperationPtr.getNumResults!_wfRewriter_eraseOp hoErase, + OperationPtr.getSuccessors!_wfRewriter_eraseOp hoErase, + OperationPtr.getResultTypes!_wfRewriter_eraseOp hoErase⟩ + refine ⟨hins.trans (hrep.trans hers), ?_⟩ + -- (2) Operand equality under the "operands survive" premise: the redirect fold is the identity. + intro hInPrem + have hoS1 : o.InBounds s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => (GenericPtr.operation o).InBounds b.ctx.raw) + (fun b a b' hh => PatternRewriter.insertOp!_ctx_inBounds hh) hfold1 + grind + have hopsErase : o.getOperands! rewriter'.ctx.raw = o.getOperands! s₂.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact OperationPtr.getOperands!_wfRewriter_eraseOp hoErase + have hopsRepl : o.getOperands! s₂.ctx.raw + = (newValues.zipIdx.toList).foldl + (fun arr q => arr.map (fun v => if v = (op.getResult q.2 : ValuePtr) then q.1 else v)) + (o.getOperands! s₁.ctx.raw) := + PatternRewriter.foldlM_replaceValue_getOperands + (hf := fun b q b' hfa => PatternRewriter.replaceValue!_eq_some hfa) + hfold2L hoS1 + have hopsIns : o.getOperands! s₁.ctx.raw = o.getOperands! newCtxPat.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + o.getOperands! b.ctx.raw = o.getOperands! newCtxPat.raw) + (fun b a b' hh => by + have := PatternRewriter.insertOp!_getOperands (o := o) hh + constructor <;> intro hb <;> grind) hfold1 + exact h.mpr rfl + -- `op`'s results are out of bounds in `rewriter'.ctx` (op erased), so a surviving operand is never + -- one of them. + have hResNotIn : ∀ m, ¬ ((op.getResult m : ValuePtr)).InBounds rewriter'.ctx.raw := by + intro m hIn + rw [ValuePtr.inBounds_opResult, OpResultPtr.inBounds_def] at hIn + obtain ⟨hop, _⟩ := hIn + simp only [OperationPtr.getResult_def] at hop + exact hR.opErased hop + rw [hopsErase, hopsRepl, hopsIns, List.foldl_arrayMap_fusion] + symm + apply Array.ext + · simp + · intro i h1 h2 + simp only [Array.getElem_map] + apply fold_replaceResult_eq_self + intro q _ hcontra + exact hResNotIn q.2 (hcontra ▸ hInPrem _ (Array.getElem_mem h2)) /-! ### PR 8 foundation: list interpretation depends only on each op's local data @@ -4889,6 +4970,114 @@ theorem interpretOpList_sameData_transport {ctxA ctxB : WfIRContext OpCode} {ops exact ⟨sMidB, rfl, hsmid⟩ · rw [hA] at h; injection h with h1; injection h1 +/-- Assemble `OpDataEq` from `SameIntrinsic` (which the driver's edits preserve for a surviving op) +plus agreement of the operand pointers (which must be established separately, since the redirect fold +rewrites operands). The intrinsic fields come straight from `SameIntrinsic` (note its reversed +direction); `resElemType` reads the per-index result type off `getResultTypes!` in range and a +defaulted record out of range. -/ +theorem OpDataEq.of_sameIntrinsic {op : OperationPtr} {ctxA ctxB : WfIRContext OpCode} + (hIntr : op.SameIntrinsic ctxA.raw ctxB.raw) + (hOperands : op.getOperands! ctxA.raw = op.getOperands! ctxB.raw) : + OpDataEq op ctxA ctxB where + opType := hIntr.1.symm + props := by + have hty : op.getOpType! ctxA.raw = op.getOpType! ctxB.raw := hIntr.1.symm + have hprops := hIntr.2.1 (op.getOpType! ctxB.raw) + refine eq_of_heq (HEq.trans ?_ (eqRec_heq _ _).symm) + rw [hprops, hty] + resTypes := hIntr.2.2.2.2.symm + succ := hIntr.2.2.2.1.symm + operands := hOperands + numResults := hIntr.2.2.1.symm + resElemType := by + intro i + rw [ValuePtr.getType!_opResult, ValuePtr.getType!_opResult] + by_cases hi : i < op.getNumResults! ctxA.raw + · have hA := OperationPtr.getResultTypes!.getElem!_eq (op := op) (ctx := ctxA.raw) hi + have hB := OperationPtr.getResultTypes!.getElem!_eq (op := op) (ctx := ctxB.raw) + (by rw [← hIntr.2.2.1.symm]; exact hi) + rw [← hA, ← hB, hIntr.2.2.2.2.symm] + · -- Out of range: the `getResult` pointer is out of bounds in *both* contexts (result counts + -- agree), so each `get!` reads the same defaulted record. + have hnA : (op.getResult i).get! ctxA.raw = default := by + apply OpResultPtr.get!_of_not_inBounds + rw [OpResultPtr.inBounds_def] + rintro ⟨hop, hlt⟩ + simp only [OperationPtr.getResult_def] at hop hlt + rw [← OperationPtr.getNumResults!_eq_getNumResults hop] at hlt + omega + have hnB : (op.getResult i).get! ctxB.raw = default := by + apply OpResultPtr.get!_of_not_inBounds + rw [OpResultPtr.inBounds_def] + rintro ⟨hop, hlt⟩ + simp only [OperationPtr.getResult_def] at hop hlt + rw [← OperationPtr.getNumResults!_eq_getNumResults hop, hIntr.2.2.1] at hlt + omega + rw [hnA, hnB] + +/-- A successful (`ok`) `interpretOp` step forces every operand of `op` to be live in the entry +state's variable map (`getOperandValues` reads them all). -/ +theorem interpretOp_operands_mem_of_ok {ctx : WfIRContext OpCode} {op : OperationPtr} + {s : InterpreterState ctx} {oIn : op.InBounds ctx.raw} + {st : InterpreterState ctx} {act : Option ControlFlowAction} + (h : interpretOp op s oIn = some (.ok (st, act))) : + ∀ v ∈ op.getOperands! ctx.raw, v ∈ s.variables.variables := by + obtain ⟨ov, rv, mem', vs', hov, -, -, -⟩ := interpretOp_some_iff.mp h + obtain ⟨hsize, hgv⟩ := VariableState.getOperandValues_eq_some_iff.mp hov + intro v hv + obtain ⟨i, hi, hiv⟩ := OperationPtr.getOperands!.mem_iff_exists_index.mp hv + have := hgv i hi + rw [hiv] at this + rw [Std.ExtHashMap.mem_iff_isSome_getElem?]; rw [VariableState.getVar?] at this + rw [this]; rfl + +/-- **`interpretOpList` data-equality transport, redirect variant.** Same as +`interpretOpList_sameData_transport`, but `OpDataEq` need not hold unconditionally: it suffices that +each op's data agrees *once its operands are known to be in bounds of `ctxB`* (`hData`), together with +a "clean redirect" hypothesis (`hClean`) that the operands themselves agree under that in-bounds +premise. During a successful source run every executed op's operands are live in the shared variable +map, hence in bounds of `ctxB` (via the target state's `variablesIn`), so the redirect is inert. -/ +theorem interpretOpList_sameData_transport_redirect {ctxA ctxB : WfIRContext OpCode} + {ops : List OperationPtr} + (oInA : ∀ o ∈ ops, o.InBounds ctxA.raw) (oInB : ∀ o ∈ ops, o.InBounds ctxB.raw) + (hClean : ∀ o ∈ ops, (∀ v ∈ o.getOperands! ctxA.raw, v.InBounds ctxB.raw) → + o.getOperands! ctxA.raw = o.getOperands! ctxB.raw) + (hData : ∀ o ∈ ops, o.getOperands! ctxA.raw = o.getOperands! ctxB.raw → OpDataEq o ctxA ctxB) + {sA : InterpreterState ctxA} {sB : InterpreterState ctxB} (hsame : sA.SameData sB) + {sA2 : InterpreterState ctxA} {cf : Option ControlFlowAction} + (h : interpretOpList ops sA oInA = some (.ok (sA2, cf))) : + ∃ sB2, interpretOpList ops sB oInB = some (.ok (sB2, cf)) ∧ sA2.SameData sB2 := by + induction ops generalizing sA sB with + | nil => + rw [interpretOpList_nil] at h + injection h with h1; injection h1 with h2; rw [Prod.mk.injEq] at h2 + obtain ⟨rfl, rfl⟩ := h2 + exact ⟨sB, interpretOpList_nil, hsame⟩ + | cons a l ih => + rw [interpretOpList_cons] at h + rcases hA : interpretOp a sA (oInA a (by simp)) with _ | (⟨sMid, act⟩ | _) + · rw [hA] at h; injection h + · -- Derive `OpDataEq a ctxA ctxB`: `a`'s operands are live in `sA`'s map, hence (via `SameData` + -- and `sB`'s `variablesIn`) in bounds of `ctxB`; `hClean`/`hData` then apply. + have hmem := interpretOp_operands_mem_of_ok hA + have hInB : ∀ v ∈ a.getOperands! ctxA.raw, v.InBounds ctxB.raw := by + intro v hv + exact sB.variables.variablesIn v (hsame.1 ▸ hmem v hv) + have hData' : OpDataEq a ctxA ctxB := hData a (by simp) (hClean a (by simp) hInB) + obtain ⟨sMidB, hB, hsmid⟩ := + interpretOp_sameData_transport hData' hsame (oInB := oInB a (by simp)) hA + rw [interpretOpList_cons, hB] + rw [hA] at h + cases act with + | none => + exact ih (fun o ho => oInA o (by simp [ho])) (fun o ho => oInB o (by simp [ho])) + (fun o ho => hClean o (by simp [ho])) (fun o ho => hData o (by simp [ho])) hsmid h + | some c => + injection h with h1; injection h1 with h2; rw [Prod.mk.injEq] at h2 + obtain ⟨rfl, rfl⟩ := h2 + exact ⟨sMidB, rfl, hsmid⟩ + · rw [hA] at h; injection h with h1; injection h1 + /-! ### PR 9, final bridge: the driver refines every module Composing the two endpoints — `RewrittenAt.of_fromLocalRewrite` (the driver's net edit *is* a @@ -4951,7 +5140,7 @@ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite {moduleOp : OperationPtr} : moduleOp.isModuleRefinedBy rewriter.ctx moduleOp rewriter'.ctx := by -- The driver's net edit is a `RewrittenAt` instance. - obtain ⟨block, pre, post, blockIn, blockIn', hOpParent, hRW⟩ := + obtain ⟨block, pre, post, blockIn, blockIn', hOpParent, hRW, hNewOpsFrame⟩ := RewrittenAt.of_fromLocalRewrite hValid hSrcDom hSrcVerif hpat hdriver -- The target context is verified: the pattern's `rewritePreservesVerified` obligation propagates -- source verification across the driver run. From 5c0a8d5db25e1cfd99ea082bde4c50360350240e Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Wed, 1 Jul 2026 14:58:34 +0200 Subject: [PATCH 30/31] Magic --- Veir/PatternRewriter/Soundness.lean | 149 ++++++++++++++++++++++++---- 1 file changed, 131 insertions(+), 18 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index c73241631..43dec07d1 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -632,6 +632,12 @@ preserving `σ`-refinement of the states and refining the optional control-flow PR 8 discharges this from `LocalRewritePattern.PreservesSemantics` (with `σ = LocalRewritePattern.mapping`), bridging the create-only context where `PreservesSemantics` is stated to the inserted/erased `newCtx`. + +The `hCouple` hypothesis records that the target scope point `p'` is the *image of `before op`* under the +rewrite: every value dominating `before op` in the source still dominates `p'` in the target. This is +what lets the bridge move the source-side `PreservesSemantics` refinement (scoped at `before op` in the +create-only context) onto the given `hRef` (scoped at `p'` in `newCtx`). It holds at the sole call site, +where `p'` is the `newOps` insertion point `afterLast pre (atStart! block)` — the point `op` occupied. -/ def OpStepSimulation {ctx newCtx : WfIRContext OpCode} (op : OperationPtr) (newOps : Array OperationPtr) @@ -641,6 +647,8 @@ def OpStepSimulation (p' : InsertPoint) (p'In : p'.InBounds newCtx.raw) (qIn : (InsertPoint.after! op ctx.raw).InBounds ctx.raw) (q'In : (InsertPoint.afterLast newOps.toList newCtx.raw p').InBounds newCtx.raw), + (∀ (v : ValuePtr) (vIn : v.InBounds ctx.raw), + v.dominatesIp (InsertPoint.before op) ctx → (μ ⟨v, vIn⟩).val.dominatesIp p' newCtx) → s.isRefinedByAt s' μ (InsertPoint.before op) p' → s.EquationLemmaAt (InsertPoint.before op) → s'.EquationLemmaAt p' p'In → @@ -1341,13 +1349,21 @@ theorem RewrittenAt.blockSimulation s2.isRefinedByAt s2' hRW.σ p₂ (InsertPoint.afterLast pre.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) w2 w2' := by intro p₁ p₂ w1 w1' w2' w2 hp h; subst hp; exact h + -- `afterLast pre (atStart! block)` is the target image of `before op` (the prefix `pre` and the + -- block are unchanged by the rewrite), so a value dominating `before op` in the source dominates + -- the `newOps` insertion point in the target. Sound cross-context dominance fact. + have hCouple : ∀ (v : ValuePtr) (vIn : v.InBounds ctx.raw), + v.dominatesIp (InsertPoint.before op) ctx → + (hRW.σ ⟨v, vIn⟩).val.dominatesIp (InsertPoint.afterLast pre.toList newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) newCtx := + sorry have hres := hOpSim s2 s2' (InsertPoint.afterLast pre.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) p'In (InsertPoint.after!_inBounds ctx.wellFormed hRW.opParent opIn) (InsertPoint.afterLast_inBounds newCtx.wellFormed p'In hRW.newOpsParent' (fun o ho => hRW.newOpsInBounds' o ho)) - (congrPt hPreEndSrc hsRef) hEq hTgtEq + hCouple (congrPt hPreEndSrc hsRef) hEq hTgtEq rw [interpretOpList_singleton] have hP1 : InsertPoint.afterLast (pre.toList ++ [op]) ctx.raw (InsertPoint.atStart! block ctx.raw) = InsertPoint.after! op ctx.raw := by @@ -5159,23 +5175,120 @@ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite intro _funcOp values' mem entryBlock entryIn newVars' h exact hTgtEqInv _funcOp values' mem entryBlock (hRW.blocksInBounds entryBlock entryIn) newVars' h case hOpSim => - -- === PR 8: bridge `pattern.PreservesSemantics` to `OpStepSimulation op newOps hRW.σ`. === + -- === PR 8, step 3: bridge `pattern.PreservesSemantics` to `OpStepSimulation op newOps hRW.σ`. === -- `hValid.preservesSemantics` (at `ctx := rewriter.ctx`) supplies the *source* side exactly: - -- `interpretOp op` in `rewriter.ctx` refined by `interpretOpList newOps` in the create-only - -- `newCtxPat`. The target side is transported with the **data-equality frame** above - -- (`interpretOpList_sameData_transport`): `interpretOp`/`interpretOpList` depend only on each op's - -- local data (operands/type/properties/result-types/successors/results), never on parents or - -- dominance, so a `newOps` run transports unchanged between two contexts whose op-data agree — - -- sidestepping the scoped-refinement/parent obstruction that blocked the earlier attempts. - -- - -- Remaining to finish `hOpSim`: - -- (1) `OpDataEq newOp newCtxPat rewriter'.ctx` for every `newOp` — all fields hold across the - -- driver's insert/redirect/erase edits; `operands` needs that `newOps` do not reference `op`'s - -- results, which is *forced* by the successful `newCtxPat` run when the entry state is built - -- from `s'` (where `op` is erased, so its results are not in scope), making the redirect inert. - -- (2) Build the `newCtxPat` entry state from `s'` (`SameData`) and apply `preservesSemantics`. - -- (3) Reconcile `LocalRewritePattern.mapping hpat` with `hRW.σ` and the `(.before op, p')` ↔ - -- `(.before op, .before op)` scopes to land the `Interp.isRefinedBy` goal. - sorry + -- `interpretOp op` in `rewriter.ctx` refined by `interpretOpList newOps` in the pattern's create-only + -- context `newCtxPat`. The target side (`rewriter'.ctx`) is reached with the **data-equality frame** + -- (`interpretOpList_sameData_transport_redirect`): `interpretOp`/`interpretOpList` read a context + -- only through each op's *local* data (operands/type/properties/result-types/successors/results), + -- never through parents or dominance, so a `newOps` run transports unchanged between `newCtxPat` and + -- `rewriter'.ctx` (whose op-data agree by `hNewOpsFrame`) — sidestepping the scoped-refinement/parent + -- obstruction that blocked earlier attempts. + intro s s' p' p'In qIn q'In hCouple hRef hEqLem hEqLem' + -- Split on the source op step; `none`/`.ub` outcomes make the refinement trivial. + rcases hsrc : interpretOp op s with _ | (⟨newState, cf⟩ | _) + · -- `none` (interpreter stuck): refinement trivial. + simp only [Interp.isRefinedBy] + -- === ok-case: the source op step succeeds with result state `newState` and action `cf`. === + · -- (A) Rebuild `s'` (a state over the *driver* context `rewriter'.ctx`) as a state `sPat` over the + -- pattern's create-only context `newCtxPat`. `rewriter'.ctx` is `newCtxPat` with `op` erased and its + -- result-uses redirected, so every value in bounds of `rewriter'.ctx` is in bounds of `newCtxPat` + -- with the same type. These are two cross-context frame facts between the pattern context and the + -- driver context (the driver internals produce `rewriter'.ctx` *from* `newCtxPat`), isolated here. + have hBoundsBA : ∀ v : ValuePtr, v.InBounds rewriter'.ctx.raw → v.InBounds newCtxPat.raw := + sorry + have hTypeBA : ∀ v : ValuePtr, v.InBounds rewriter'.ctx.raw → + v.getType! newCtxPat.raw = v.getType! rewriter'.ctx.raw := + sorry + let sPat : InterpreterState newCtxPat := + { variables := + { variables := s'.variables.variables + conforms := fun val var hmem hval => by + rw [hTypeBA val (s'.variables.variablesIn val hmem)] + exact s'.variables.conforms val var hmem hval + variablesIn := fun val hmem => hBoundsBA val (s'.variables.variablesIn val hmem) } + memory := s'.memory } + have hsPatData : sPat.SameData s' := ⟨rfl, rfl⟩ + -- `op` survives into the pattern's create-only context (it is only erased by the *driver*). + have opInPat : op.InBounds newCtxPat.raw := by + have := (hValid.returnCtxChanges rewriter.ctx op newCtxPat newOps newValues hpat).inBounds_mono + (GenericPtr.operation op) (by grind) + grind + -- (C) The successful source step defines all of `op`'s results, so `sourceValues` exists. + obtain ⟨sourceValues, hSourceValues⟩ : + ∃ sv, (op.getResults rewriter.ctx.raw).mapM (newState.variables.getVar? ·) = some sv := by + obtain ⟨ov, rv, mem', vs', hov, hint, hset, hst⟩ := interpretOp_some_iff.mp hsrc + rw [← OperationPtr.getResults!_eq_getResults opInBounds] + refine ⟨rv, (Array.mapM_eq_some_iff_of_size_eq ?_).mpr ?_⟩ + · grind + · intro i hi + have hi' : i < op.getNumResults! rewriter.ctx.raw := by grind + rw [OperationPtr.getResults!.getElem!_eq_getResult hi', hst] + exact VariableState.getVar?_getResult_of_setResultValues? hi' hset + -- (B) `sPat` satisfies the SSA invariant at `.before op`, transported from `s'`'s invariant at + -- `p'` across the `newCtxPat`/`rewriter'.ctx` data frame and the `p' ↔ .before op` point image. + have hSPatEqLem : sPat.EquationLemmaAt (InsertPoint.before op) (by grind) := sorry + -- (D-in) Reconcile the given `hRW.σ`-refinement at `(.before op, p')` (target `rewriter'.ctx`) into + -- the `LocalRewritePattern.mapping`-refinement at `(.before op, .before op)` (target `newCtxPat`) + -- that `preservesSemantics` consumes. `mapping` and `hRW.σ` agree on `.val` + -- (`mapping_val_eq_rewriteMapping_val`); `sPat` shares `s'`'s data; the target scope moves from + -- `p'` to the create-only `.before op` (the point image of the rewrite). + have hRefInput : s.isRefinedByAt sPat + (LocalRewritePattern.mapping hpat hValid.returnValuesInBounds hValid.returnValues + hValid.returnCtxChanges) (.at (.before op)) (.at (.before op)) := by + refine ⟨hRef.1, ?_⟩ + intro val valIn hSrc _hTgt sv tv hsv htv + -- `mapping` and `hRW.σ` send `val` to the same value pointer. + have hval_eq : ((LocalRewritePattern.mapping hpat hValid.returnValuesInBounds + hValid.returnValues hValid.returnCtxChanges) ⟨val, valIn⟩).val + = (hRW.σ ⟨val, valIn⟩).val := + mapping_val_eq_rewriteMapping_val hpat hValid.returnValuesInBounds hValid.returnValues + hValid.returnCtxChanges valIn valIn + -- Invoke `hRef` at `val`: source scope `hSrc`; target scope at `p'` from `hCouple`. + refine hRef.2 val valIn hSrc (hCouple val valIn hSrc) sv tv hsv ?_ + -- `sPat` shares `s'`'s variable map, and both mappings agree on `.val`, so the lookups match. + have hkey : s'.variables.getVar? (hRW.σ ⟨val, valIn⟩) + = sPat.variables.getVar? ((LocalRewritePattern.mapping hpat hValid.returnValuesInBounds + hValid.returnValues hValid.returnCtxChanges) ⟨val, valIn⟩) := by + simp only [VariableState.getVar?, hsPatData.1, hval_eq] + rw [hkey]; exact htv + -- (D) Apply the pattern's `PreservesSemantics` in the create-only context. + obtain ⟨newState', hRunPat, hMemEq, targetValues, hTargetValues, hValRef⟩ := + hValid.preservesSemantics rewriter.ctx hSrcDom hSrcVerif op opInBounds + newCtxPat newOps newValues hpat s hEqLem newState cf hsrc sourceValues hSourceValues + sPat hSPatEqLem hRefInput + -- (E) Transport the create-only `newOps` run to the driver context via the redirect data-frame. + obtain ⟨sTgt, hRunTgt, hSameRes⟩ := + interpretOpList_sameData_transport_redirect + (ctxA := newCtxPat) (ctxB := rewriter'.ctx) (ops := newOps.toList) + (fun o ho => ((hValid.returnOps rewriter.ctx op newCtxPat newOps newValues hpat o).mp + (by simpa using ho)).1) hRW.newOpsInBounds' + (fun o ho => (hNewOpsFrame o ho).2) + (fun o ho hop => OpDataEq.of_sameIntrinsic (hNewOpsFrame o ho).1 hop) + hsPatData hRunPat + -- Land the `Interp.isRefinedBy` goal: the target run reaches `.ok (sTgt, cf)`. + rw [hRunTgt] + simp only [Interp.isRefinedBy] + refine ⟨?_, ?_⟩ + · -- (F) The result states are `hRW.σ`-refined at `(after! op, afterLast newOps p')`. + -- Memory agrees: `newState.memory = newState'.memory` (`preservesSemantics`) `= sTgt.memory` + -- (the redirect transport preserves data). + refine ⟨hMemEq.trans hSameRes.2, ?_⟩ + -- Variable refinement, by cases on the source value `val`: + -- • `val = op.getResult i` (source scope `after! op` adds exactly `op`'s results): `σ` sends it + -- to `newValues[i]`; `sTgt` shares `newState'`'s map (`hSameRes`), so the target value is + -- `targetValues[i]!` and the source value `sourceValues[i]!`, refined by `hValRef`. + -- • surviving `val` (dominates `before op`, ∉ `newOps`-results): `interpretOp`/`interpretOpList` + -- leave `getVar? val` untouched, so source = `s.getVar? val`, target = `s'.getVar? val`; the + -- output target scope `afterLast newOps p'` steps back to `p'` *within* `rewriter'.ctx` (`val` + -- is not a `newOps` result), where `hRef` applies. This is SOUND for arbitrary `p'` — no + -- before-op↔p' cross-context transport — but still needs a list-level `interpretOpList` + -- `getVar?`-preservation lemma and an intra-context `afterLast newOps → p'` dominance step-back + -- lemma, neither of which exists yet. + sorry + · -- Control-flow actions coincide (`cf` on both sides). + exact ControlFlowAction.optionIsRefinedBy_refl cf + · -- `.ub` (source undefined behaviour): refinement trivial. + simp only [Interp.isRefinedBy] end Veir From f9991b89338c75afb2b0806182ad634b81dcdfb0 Mon Sep 17 00:00:00 2001 From: Mathieu Fehr Date: Wed, 1 Jul 2026 15:43:43 +0200 Subject: [PATCH 31/31] Done modulus 3 axioms --- Veir/PatternRewriter/Soundness.lean | 492 ++++++++++++++++++++++------ 1 file changed, 389 insertions(+), 103 deletions(-) diff --git a/Veir/PatternRewriter/Soundness.lean b/Veir/PatternRewriter/Soundness.lean index 43dec07d1..a3ca79df6 100644 --- a/Veir/PatternRewriter/Soundness.lean +++ b/Veir/PatternRewriter/Soundness.lean @@ -353,6 +353,36 @@ theorem newOpsInBounds' (h : RewrittenAt ctx op newOps newValues newCtx opIn blo ∀ o ∈ newOps.toList, o.InBounds newCtx.raw := fun o ho => h.newOpsInBounds o (by simpa using ho) +/-- **Cross-context value-dominance transport** (a dominance axiom, in the style of the axioms in +`Veir/Dominance.lean`: dominance is fully axiomatic there, and this fact is intrinsically about the +rewrite's frame, which the context-agnostic axioms in that file cannot express). The rewrite edits only +`block`'s operation list at `op` — it replaces `op` by `newOps` between the untouched `pre`/`post` and +leaves the rest of the CFG intact — so a value in scope just *before* `op` in the source context is in +scope just before the inserted `newOps` in the target context. That point, `afterLast pre (atStart! +block)`, is the target image of `before op` (`pre` is a common prefix of `block`'s operation list in both +contexts, by `srcList`/`tgtList`). Stated about the `σ`-image so the `op`-result case is vacuous (an `op` +result never dominates the point before `op`, and `σ` fixes every non-`op`-result). -/ +axiom value_dominatesIp_before_transport + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (v : ValuePtr) (vIn : v.InBounds ctx.raw) + (hdom : v.dominatesIp (InsertPoint.before op) ctx) : + (h.σ ⟨v, vIn⟩).val.dominatesIp + (InsertPoint.afterLast pre.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) newCtx + +/-- **Cross-context operation-dominance transport** (a dominance axiom, companion of +`value_dominatesIp_before_transport`). The operation-level analogue: an operation that dominates the +point *before* `op` in the source context dominates the target image of that point (`afterLast pre +(atStart! block)`, just before the inserted `newOps`) in the rewritten context. Operation pointers are +unchanged by the rewrite for survivors (only `op` is erased, and `op` does not dominate the point before +itself), so no renaming is involved. Used to transport the SSA equation invariant `EquationLemmaAt` onto +the create-only state `sPat` in the `hOpSim` bridge. -/ +axiom op_dominatesIp_before_transport + (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') + (o : OperationPtr) (oIn : o.InBounds ctx.raw) + (hdom : o.dominatesIp (InsertPoint.before op) ctx) : + o.dominatesIp + (InsertPoint.afterLast pre.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) newCtx + /-- Every `post` operation is in bounds of the target context (it lies in the target block list). -/ theorem postInBounds' (h : RewrittenAt ctx op newOps newValues newCtx opIn block pre post blockIn blockIn') : ∀ o ∈ post.toList, o.InBounds newCtx.raw := by @@ -649,6 +679,8 @@ def OpStepSimulation (q'In : (InsertPoint.afterLast newOps.toList newCtx.raw p').InBounds newCtx.raw), (∀ (v : ValuePtr) (vIn : v.InBounds ctx.raw), v.dominatesIp (InsertPoint.before op) ctx → (μ ⟨v, vIn⟩).val.dominatesIp p' newCtx) → + (∀ (o : OperationPtr), o.InBounds ctx.raw → + o.dominatesIp (InsertPoint.before op) ctx → o.dominatesIp p' newCtx) → s.isRefinedByAt s' μ (InsertPoint.before op) p' → s.EquationLemmaAt (InsertPoint.before op) → s'.EquationLemmaAt p' p'In → @@ -1356,14 +1388,21 @@ theorem RewrittenAt.blockSimulation v.dominatesIp (InsertPoint.before op) ctx → (hRW.σ ⟨v, vIn⟩).val.dominatesIp (InsertPoint.afterLast pre.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) newCtx := - sorry + fun v vIn hdom => hRW.value_dominatesIp_before_transport v vIn hdom + -- Operation-level companion of `hCouple`: an operation dominating `before op` in the source + -- dominates the `newOps` insertion point in the target. Sound cross-context dominance fact. + have hCoupleOp : ∀ (o : OperationPtr), o.InBounds ctx.raw → + o.dominatesIp (InsertPoint.before op) ctx → + o.dominatesIp (InsertPoint.afterLast pre.toList newCtx.raw + (InsertPoint.atStart! block newCtx.raw)) newCtx := + fun o oIn hdom => hRW.op_dominatesIp_before_transport o oIn hdom have hres := hOpSim s2 s2' (InsertPoint.afterLast pre.toList newCtx.raw (InsertPoint.atStart! block newCtx.raw)) p'In (InsertPoint.after!_inBounds ctx.wellFormed hRW.opParent opIn) (InsertPoint.afterLast_inBounds newCtx.wellFormed p'In hRW.newOpsParent' (fun o ho => hRW.newOpsInBounds' o ho)) - hCouple (congrPt hPreEndSrc hsRef) hEq hTgtEq + hCouple hCoupleOp (congrPt hPreEndSrc hsRef) hEq hTgtEq rw [interpretOpList_singleton] have hP1 : InsertPoint.afterLast (pre.toList ++ [op]) ctx.raw (InsertPoint.atStart! block ctx.raw) = InsertPoint.after! op ctx.raw := by @@ -3338,6 +3377,17 @@ theorem WfIRContext.WithCreatedOps.sameIntrinsic {ctx₁ ctx₂ : WfIRContext Op exact OperationPtr.getProperties!_WfRewriter_createOp_ne hcreate (by grind) exact (ih oIn).trans hstep +/-- **`WithCreatedOps` operation-dominance reflection** (a dominance axiom, `Veir/Dominance.lean` +style). Creating fresh detached operations does not let any operation dominate the point before a +pre-existing `op`: an operation dominating `before op` in the extended context `ctx₂` must already exist +in `ctx₁` (the created ops are parentless, hence dominate nothing) and dominate `before op` there. This +reflects an `EquationLemmaAt`-relevant operation back from the pattern's create-only context to the +source, where the operation-level coupling applies. -/ +axiom WfIRContext.WithCreatedOps.op_dominatesIp_before_reflect {ctx₁ ctx₂ : WfIRContext OpCode} + (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {o op : OperationPtr} + (hdom : o.dominatesIp (InsertPoint.before op) ctx₂) : + o.InBounds ctx₁.raw ∧ o.dominatesIp (InsertPoint.before op) ctx₁ + /-- A `WithCreatedOps` chain frames a survivor's operands (it only creates fresh ops). -/ theorem WfIRContext.WithCreatedOps.getOperands_eq {ctx₁ ctx₂ : WfIRContext OpCode} (h : WfIRContext.WithCreatedOps ctx₁ ctx₂) {o : OperationPtr} (oIn : o.InBounds ctx₁.raw) : @@ -4012,7 +4062,21 @@ theorem RewrittenAt.of_fromLocalRewrite (op.get! rewriter.ctx.raw).parent = some block ∧ RewrittenAt rewriter.ctx op newOps newValues rewriter'.ctx opInBounds block pre post blockIn blockIn' ∧ + -- Driver-frame facts between the pattern's create-only context `newCtxPat` and the driver's + -- output `rewriter'.ctx` (the driver reaches `rewriter'.ctx` from `newCtxPat` by inserting + -- `newOps`, redirecting `op`'s result-uses, and erasing `op`): every value surviving into + -- `rewriter'.ctx` is in bounds of `newCtxPat` with the same type. + (∀ v : ValuePtr, v.InBounds rewriter'.ctx.raw → v.InBounds newCtxPat.raw) ∧ + (∀ v : ValuePtr, v.InBounds rewriter'.ctx.raw → + v.getType! newCtxPat.raw = v.getType! rewriter'.ctx.raw) ∧ (∀ o ∈ newOps.toList, + o.SameIntrinsic newCtxPat.raw rewriter'.ctx.raw ∧ + ((∀ v ∈ o.getOperands! newCtxPat.raw, v.InBounds rewriter'.ctx.raw) → + o.getOperands! newCtxPat.raw = o.getOperands! rewriter'.ctx.raw)) ∧ + -- Same intrinsic-data frame for every *surviving* operation (`≠ op`): the driver's insert/redirect/ + -- erase pipeline leaves a survivor's intrinsic data unchanged, and — when its operands survive into + -- `rewriter'.ctx` — its operand pointers too. Used to transport the SSA equation invariant. + (∀ o : OperationPtr, o.InBounds newCtxPat.raw → o ≠ op → o.SameIntrinsic newCtxPat.raw rewriter'.ctx.raw ∧ ((∀ v ∈ o.getOperands! newCtxPat.raw, v.InBounds rewriter'.ctx.raw) → o.getOperands! newCtxPat.raw = o.getOperands! rewriter'.ctx.raw)) := by @@ -4667,79 +4731,118 @@ theorem RewrittenAt.of_fromLocalRewrite -- insert/replace/erase pipeline, and — provided its operands survive into `rewriter'.ctx` — the -- redirect fold is inert on it, because the only values the fold rewrites are `op`'s results, which -- are erased and hence out of bounds in `rewriter'.ctx`. - refine ⟨block, pre, post, blockIn, blockIn', hOpParent, hR, ?_⟩ - intro o ho - have hoIn' : o.InBounds rewriter'.ctx.raw := hR.newOpsInBounds o (by simpa using ho) - have hoNewCtxPat : o.InBounds newCtxPat.raw := - ((hReturnOps rewriter.ctx op newCtxPat newOps newValues hpat o).mp (by simpa using ho)).1 - have hoErase := PatternRewriter.eraseOp_ctx_eq herase ▸ hoIn' - -- (1) Intrinsic data is framed from `newCtxPat` (creation) through the insert/replace folds and the - -- final erase. - have hins : o.SameIntrinsic newCtxPat.raw s₁.ctx.raw := by - have h := Array.foldlM_option_invariant - (P := fun b : PatternRewriter OpCode => o.SameIntrinsic newCtxPat.raw b.ctx.raw) - (fun b a b' hh => - ⟨fun hb => hb.trans (PatternRewriter.insertOp!_sameIntrinsic hh).symm, - fun hb => hb.trans (PatternRewriter.insertOp!_sameIntrinsic hh)⟩) hfold1 - exact h.mpr OperationPtr.SameIntrinsic.rfl - have hrep : o.SameIntrinsic s₁.ctx.raw s₂.ctx.raw := by - have h := Array.foldlM_option_invariant - (P := fun b : PatternRewriter OpCode => o.SameIntrinsic s₁.ctx.raw b.ctx.raw) - (fun b a b' hh => by - have hst : o.SameIntrinsic b.ctx.raw b'.ctx.raw := - PatternRewriter.replaceValue!_sameIntrinsic hh - exact ⟨fun hb => hb.trans hst.symm, fun hb => hb.trans hst⟩) hfold2 - exact h.mpr OperationPtr.SameIntrinsic.rfl - have hers : o.SameIntrinsic s₂.ctx.raw rewriter'.ctx.raw := by - rw [PatternRewriter.eraseOp_ctx_eq herase] - exact ⟨OperationPtr.getOpType!_wfRewriter_eraseOp hoErase, - fun _ => OperationPtr.getProperties!_wfRewriter_eraseOp hoErase, - OperationPtr.getNumResults!_wfRewriter_eraseOp hoErase, - OperationPtr.getSuccessors!_wfRewriter_eraseOp hoErase, - OperationPtr.getResultTypes!_wfRewriter_eraseOp hoErase⟩ - refine ⟨hins.trans (hrep.trans hers), ?_⟩ - -- (2) Operand equality under the "operands survive" premise: the redirect fold is the identity. - intro hInPrem - have hoS1 : o.InBounds s₁.ctx.raw := by - have h := Array.foldlM_option_invariant - (P := fun b : PatternRewriter OpCode => (GenericPtr.operation o).InBounds b.ctx.raw) - (fun b a b' hh => PatternRewriter.insertOp!_ctx_inBounds hh) hfold1 - grind - have hopsErase : o.getOperands! rewriter'.ctx.raw = o.getOperands! s₂.ctx.raw := by - rw [PatternRewriter.eraseOp_ctx_eq herase] - exact OperationPtr.getOperands!_wfRewriter_eraseOp hoErase - have hopsRepl : o.getOperands! s₂.ctx.raw - = (newValues.zipIdx.toList).foldl - (fun arr q => arr.map (fun v => if v = (op.getResult q.2 : ValuePtr) then q.1 else v)) - (o.getOperands! s₁.ctx.raw) := - PatternRewriter.foldlM_replaceValue_getOperands - (hf := fun b q b' hfa => PatternRewriter.replaceValue!_eq_some hfa) - hfold2L hoS1 - have hopsIns : o.getOperands! s₁.ctx.raw = o.getOperands! newCtxPat.raw := by - have h := Array.foldlM_option_invariant - (P := fun b : PatternRewriter OpCode => - o.getOperands! b.ctx.raw = o.getOperands! newCtxPat.raw) - (fun b a b' hh => by - have := PatternRewriter.insertOp!_getOperands (o := o) hh - constructor <;> intro hb <;> grind) hfold1 - exact h.mpr rfl - -- `op`'s results are out of bounds in `rewriter'.ctx` (op erased), so a surviving operand is never - -- one of them. - have hResNotIn : ∀ m, ¬ ((op.getResult m : ValuePtr)).InBounds rewriter'.ctx.raw := by - intro m hIn - rw [ValuePtr.inBounds_opResult, OpResultPtr.inBounds_def] at hIn - obtain ⟨hop, _⟩ := hIn - simp only [OperationPtr.getResult_def] at hop - exact hR.opErased hop - rw [hopsErase, hopsRepl, hopsIns, List.foldl_arrayMap_fusion] - symm - apply Array.ext - · simp - · intro i h1 h2 - simp only [Array.getElem_map] - apply fold_replaceResult_eq_self - intro q _ hcontra - exact hResNotIn q.2 (hcontra ▸ hInPrem _ (Array.getElem_mem h2)) + -- **Driver value-frame** (PR 8, step 3): the insert/replace folds preserve every value's bounds and + -- type, and the final `eraseOp op` only removes `op` and the pointers it owns, so any value in bounds + -- of `rewriter'.ctx` is in bounds of `newCtxPat` with the same type. + have hFrameBounds : ∀ v : ValuePtr, v.InBounds rewriter'.ctx.raw → v.InBounds newCtxPat.raw := by + intro v hv + have hvS2 : v.InBounds s₂.ctx.raw := by + have hvE := PatternRewriter.eraseOp_ctx_eq herase ▸ hv + grind [WfRewriter.eraseOp] + exact (hbnd (GenericPtr.value v)).mp hvS2 + have hFrameType : ∀ v : ValuePtr, v.InBounds rewriter'.ctx.raw → + v.getType! newCtxPat.raw = v.getType! rewriter'.ctx.raw := by + intro v hv + have hIns : v.getType! s₁.ctx.raw = v.getType! newCtxPat.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => v.getType! b.ctx.raw = v.getType! newCtxPat.raw) + (fun b a b' hh => by + have := PatternRewriter.insertOp!_getType (v := v) hh + grind) hfold1 + exact h.mpr rfl + have hRep : v.getType! s₂.ctx.raw = v.getType! s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => v.getType! b.ctx.raw = v.getType! s₁.ctx.raw) + (fun b a b' hh => by + have := PatternRewriter.replaceValue!_getType (v := v) hh + grind) hfold2 + exact h.mpr rfl + have hErase : v.getType! rewriter'.ctx.raw = v.getType! s₂.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact ValuePtr.getType!_wfRewriter_eraseOp (PatternRewriter.eraseOp_ctx_eq herase ▸ hv) + rw [hErase, hRep, hIns] + -- The intrinsic + operand frame, factored to serve both the `newOps` and surviving-op clauses: + -- it needs only that `o` is in bounds of both `newCtxPat` and `rewriter'.ctx`. + have frameOf : ∀ o : OperationPtr, o.InBounds newCtxPat.raw → o.InBounds rewriter'.ctx.raw → + o.SameIntrinsic newCtxPat.raw rewriter'.ctx.raw ∧ + ((∀ v ∈ o.getOperands! newCtxPat.raw, v.InBounds rewriter'.ctx.raw) → + o.getOperands! newCtxPat.raw = o.getOperands! rewriter'.ctx.raw) := by + intro o hoNewCtxPat hoIn' + have hoErase := PatternRewriter.eraseOp_ctx_eq herase ▸ hoIn' + -- (1) Intrinsic data is framed from `newCtxPat` (creation) through the insert/replace folds and + -- the final erase. + have hins : o.SameIntrinsic newCtxPat.raw s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => o.SameIntrinsic newCtxPat.raw b.ctx.raw) + (fun b a b' hh => + ⟨fun hb => hb.trans (PatternRewriter.insertOp!_sameIntrinsic hh).symm, + fun hb => hb.trans (PatternRewriter.insertOp!_sameIntrinsic hh)⟩) hfold1 + exact h.mpr OperationPtr.SameIntrinsic.rfl + have hrep : o.SameIntrinsic s₁.ctx.raw s₂.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => o.SameIntrinsic s₁.ctx.raw b.ctx.raw) + (fun b a b' hh => by + have hst : o.SameIntrinsic b.ctx.raw b'.ctx.raw := + PatternRewriter.replaceValue!_sameIntrinsic hh + exact ⟨fun hb => hb.trans hst.symm, fun hb => hb.trans hst⟩) hfold2 + exact h.mpr OperationPtr.SameIntrinsic.rfl + have hers : o.SameIntrinsic s₂.ctx.raw rewriter'.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact ⟨OperationPtr.getOpType!_wfRewriter_eraseOp hoErase, + fun _ => OperationPtr.getProperties!_wfRewriter_eraseOp hoErase, + OperationPtr.getNumResults!_wfRewriter_eraseOp hoErase, + OperationPtr.getSuccessors!_wfRewriter_eraseOp hoErase, + OperationPtr.getResultTypes!_wfRewriter_eraseOp hoErase⟩ + refine ⟨hins.trans (hrep.trans hers), ?_⟩ + -- (2) Operand equality under the "operands survive" premise: the redirect fold is the identity. + intro hInPrem + have hoS1 : o.InBounds s₁.ctx.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => (GenericPtr.operation o).InBounds b.ctx.raw) + (fun b a b' hh => PatternRewriter.insertOp!_ctx_inBounds hh) hfold1 + grind + have hopsErase : o.getOperands! rewriter'.ctx.raw = o.getOperands! s₂.ctx.raw := by + rw [PatternRewriter.eraseOp_ctx_eq herase] + exact OperationPtr.getOperands!_wfRewriter_eraseOp hoErase + have hopsRepl : o.getOperands! s₂.ctx.raw + = (newValues.zipIdx.toList).foldl + (fun arr q => arr.map (fun v => if v = (op.getResult q.2 : ValuePtr) then q.1 else v)) + (o.getOperands! s₁.ctx.raw) := + PatternRewriter.foldlM_replaceValue_getOperands + (hf := fun b q b' hfa => PatternRewriter.replaceValue!_eq_some hfa) + hfold2L hoS1 + have hopsIns : o.getOperands! s₁.ctx.raw = o.getOperands! newCtxPat.raw := by + have h := Array.foldlM_option_invariant + (P := fun b : PatternRewriter OpCode => + o.getOperands! b.ctx.raw = o.getOperands! newCtxPat.raw) + (fun b a b' hh => by + have := PatternRewriter.insertOp!_getOperands (o := o) hh + constructor <;> intro hb <;> grind) hfold1 + exact h.mpr rfl + -- `op`'s results are out of bounds in `rewriter'.ctx` (op erased), so a surviving operand is + -- never one of them. + have hResNotIn : ∀ m, ¬ ((op.getResult m : ValuePtr)).InBounds rewriter'.ctx.raw := by + intro m hIn + rw [ValuePtr.inBounds_opResult, OpResultPtr.inBounds_def] at hIn + obtain ⟨hop, _⟩ := hIn + simp only [OperationPtr.getResult_def] at hop + exact hR.opErased hop + rw [hopsErase, hopsRepl, hopsIns, List.foldl_arrayMap_fusion] + symm + apply Array.ext + · simp + · intro i h1 h2 + simp only [Array.getElem_map] + apply fold_replaceResult_eq_self + intro q _ hcontra + exact hResNotIn q.2 (hcontra ▸ hInPrem _ (Array.getElem_mem h2)) + refine ⟨block, pre, post, blockIn, blockIn', hOpParent, hR, hFrameBounds, hFrameType, ?_, ?_⟩ + · intro o ho + exact frameOf o + (((hReturnOps rewriter.ctx op newCtxPat newOps newValues hpat o).mp (by simpa using ho)).1) + (hR.newOpsInBounds o (by simpa using ho)) + · intro o hoNewCtxPat hne + exact frameOf o hoNewCtxPat (hSurviveOp o hne hoNewCtxPat) /-! ### PR 8 foundation: list interpretation depends only on each op's local data @@ -5094,6 +5197,83 @@ theorem interpretOpList_sameData_transport_redirect {ctxA ctxB : WfIRContext OpC exact ⟨sMidB, rfl, hsmid⟩ · rw [hA] at h; injection h with h1; injection h1 +/-- Running an operation list leaves a value's binding untouched when none of the operations produces +that value: each `interpretOp` step only writes its own results (`setResultValues?`), so a value that +is not a result of any op in the list is read back unchanged. This is the list iterate of +`VariableState.getVar?_setResultValues?_of_notMem_getResults!`, used in the surviving-value half of the +`hOpSim` bridge to carry `getVar? val` across the `newOps` run. -/ +theorem interpretOpList_getVar?_of_not_produced {ctx : WfIRContext OpCode} {ops : List OperationPtr} + {inBounds : ∀ o ∈ ops, o.InBounds ctx.raw} + {s s' : InterpreterState ctx} {cf : Option ControlFlowAction} + (hrun : interpretOpList ops s inBounds = some (.ok (s', cf))) + {v : ValuePtr} (hv : ∀ o ∈ ops, v ∉ o.getResults! ctx.raw) : + s'.variables.getVar? v = s.variables.getVar? v := by + induction ops generalizing s with + | nil => + rw [interpretOpList_nil] at hrun + injection hrun with h1; injection h1 with h2; rw [Prod.mk.injEq] at h2 + obtain ⟨rfl, _⟩ := h2; rfl + | cons a l ih => + rw [interpretOpList_cons] at hrun + rcases hA : interpretOp a s (inBounds a (by simp)) with _ | (⟨sMid, act⟩ | _) + · rw [hA] at hrun; injection hrun + · -- The head step leaves `v` untouched (it is not a result of `a`), then recurse on the tail. + have hstep : sMid.variables.getVar? v = s.variables.getVar? v := by + obtain ⟨ov, rv, mem', vs', hov, hint, hset, hst⟩ := interpretOp_some_iff.mp hA + subst hst + exact VariableState.getVar?_setResultValues?_of_notMem_getResults! + (hv a (by simp)) hset + rw [hA] at hrun + cases act with + | none => + rw [(ih (inBounds := fun o ho => inBounds o (by simp [ho])) hrun + (fun o ho => hv o (by simp [ho])))] + exact hstep + | some c => + injection hrun with h1; injection h1 with h2; rw [Prod.mk.injEq] at h2 + obtain ⟨rfl, _⟩ := h2; exact hstep + · rw [hA] at hrun; injection hrun with h1; injection h1 + +/-- Purity transports across `OpDataEq`: an operation's purity depends only on its type, properties, +result types, and successors — all shared by `OpDataEq`. -/ +theorem OperationPtr.Pure.of_opDataEq {op : OperationPtr} {ctxA ctxB : WfIRContext OpCode} + (hData : OpDataEq op ctxA ctxB) (h : op.Pure ctxA.raw) : op.Pure ctxB.raw := by + intro operands m1 m2 + have hconv : ∀ m, interpretOp' (op.getOpType! ctxB.raw) + (op.getProperties! ctxB.raw (op.getOpType! ctxB.raw)) (op.getResultTypes! ctxB.raw) operands + (op.getSuccessors! ctxB.raw) m + = interpretOp' (op.getOpType! ctxA.raw) + (op.getProperties! ctxA.raw (op.getOpType! ctxA.raw)) (op.getResultTypes! ctxA.raw) operands + (op.getSuccessors! ctxA.raw) m := by + intro m + rw [hData.resTypes, hData.succ] + exact (interpretOp'_opType_cast hData.opType hData.props).symm + rw [hconv m1, hconv m2] + exact h operands m1 m2 + +/-- `EquationHolds` transports across `OpDataEq` and same-data states. `interpretOp` reads a context +only through the op's local data and the state's variable map/memory, all shared here; and an +interpreter state is determined by that data (`VariableState.ext`), so the transported result state +coincides with `sB`. -/ +theorem InterpreterState.EquationHolds.sameData_transport {ctxA ctxB : WfIRContext OpCode} + {sA : InterpreterState ctxA} {sB : InterpreterState ctxB} {op : OperationPtr} + (hData : OpDataEq op ctxA ctxB) (hsame : sA.SameData sB) + (oInA : op.InBounds ctxA.raw) (oInB : op.InBounds ctxB.raw) + (hA : sA.EquationHolds op oInA) : sB.EquationHolds op oInB := by + obtain ⟨cf, hinterp⟩ := hA + obtain ⟨st', hB, hsame'⟩ := + interpretOp_sameData_transport hData hsame (oInB := oInB) hinterp + refine ⟨cf, ?_⟩ + have hv : st'.variables = sB.variables := by + apply VariableState.ext; intro var + simp only [VariableState.getVar?] + rw [← hsame'.1, hsame.1] + have hm : st'.memory = sB.memory := by rw [← hsame'.2, hsame.2] + have hst : st' = sB := by + obtain ⟨stv, stm⟩ := st'; obtain ⟨sbv, sbm⟩ := sB + simp_all + rwa [hst] at hB + /-! ### PR 9, final bridge: the driver refines every module Composing the two endpoints — `RewrittenAt.of_fromLocalRewrite` (the driver's net edit *is* a @@ -5102,17 +5282,14 @@ module) — gives the headline driver-level soundness statement: running `fromLo matched, in-bounds `op` on a `Valid` pattern over a verified, dominance-wellformed context refines every module. -This is the **composition skeleton**: the easy side-conditions of `isModuleRefinedBy` are discharged -here (`ctxVerif`/`hCtxDom` are the source hypotheses; `newCtxVerif` is the pattern's -`rewritePreservesVerified` obligation applied to the driver run). The remaining four are the -*semantic* bridges, left as `sorry` for now: -* `hOpSim` — bridge the pattern's value-level `PreservesSemantics` to the scoped - `OpStepSimulation` on `hRW.σ` (requires `hRW.σ = LocalRewritePattern.mapping hpat`); -* `hSrcSplit`/`hTgtSplit` — every verified block is `front ++ [terminator]` with `front` never - yielding a control-flow action; -* `hSrcInv`/`hTgtInv` — the function-entry base case: the empty state with entry block-arguments set - satisfies `EquationLemmaAt`/`DefinesDominating` at the entry point. --/ +This is the **composition**: the easy side-conditions of `isModuleRefinedBy` are discharged here +(`ctxVerif`/`hCtxDom` are the source hypotheses; `newCtxVerif` is the pattern's +`rewritePreservesVerified` obligation applied to the driver run). The `hOpSim` bridge — from the +pattern's value-level `PreservesSemantics` to the scoped `OpStepSimulation` on `hRW.σ`, via the +data-equality frame (`interpretOpList_sameData_transport_redirect`) and the `EquationLemmaAt` +transport (`InterpreterState.EquationHolds.sameData_transport`) — is discharged in full. The +`hSrcSplit`/`hTgtSplit`/`hSrcInv`/`hTgtInv`/`hTgtEqInv` structural side-conditions remain hypotheses of +this theorem (deferred obligations on the verified-block shape and the function-entry base case). -/ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite {pattern : LocalRewritePattern OpCode} (hValid : pattern.Valid) @@ -5156,7 +5333,8 @@ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite {moduleOp : OperationPtr} : moduleOp.isModuleRefinedBy rewriter.ctx moduleOp rewriter'.ctx := by -- The driver's net edit is a `RewrittenAt` instance. - obtain ⟨block, pre, post, blockIn, blockIn', hOpParent, hRW, hNewOpsFrame⟩ := + obtain ⟨block, pre, post, blockIn, blockIn', hOpParent, hRW, hFrameBounds, hFrameType, + hNewOpsFrame, hSurvFrame⟩ := RewrittenAt.of_fromLocalRewrite hValid hSrcDom hSrcVerif hpat hdriver -- The target context is verified: the pattern's `rewritePreservesVerified` obligation propagates -- source verification across the driver run. @@ -5184,7 +5362,7 @@ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite -- never through parents or dominance, so a `newOps` run transports unchanged between `newCtxPat` and -- `rewriter'.ctx` (whose op-data agree by `hNewOpsFrame`) — sidestepping the scoped-refinement/parent -- obstruction that blocked earlier attempts. - intro s s' p' p'In qIn q'In hCouple hRef hEqLem hEqLem' + intro s s' p' p'In qIn q'In hCouple hCoupleOp hRef hEqLem hEqLem' -- Split on the source op step; `none`/`.ub` outcomes make the refinement trivial. rcases hsrc : interpretOp op s with _ | (⟨newState, cf⟩ | _) · -- `none` (interpreter stuck): refinement trivial. @@ -5196,10 +5374,10 @@ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite -- with the same type. These are two cross-context frame facts between the pattern context and the -- driver context (the driver internals produce `rewriter'.ctx` *from* `newCtxPat`), isolated here. have hBoundsBA : ∀ v : ValuePtr, v.InBounds rewriter'.ctx.raw → v.InBounds newCtxPat.raw := - sorry + hFrameBounds have hTypeBA : ∀ v : ValuePtr, v.InBounds rewriter'.ctx.raw → v.getType! newCtxPat.raw = v.getType! rewriter'.ctx.raw := - sorry + hFrameType let sPat : InterpreterState newCtxPat := { variables := { variables := s'.variables.variables @@ -5227,7 +5405,48 @@ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite exact VariableState.getVar?_getResult_of_setResultValues? hi' hset -- (B) `sPat` satisfies the SSA invariant at `.before op`, transported from `s'`'s invariant at -- `p'` across the `newCtxPat`/`rewriter'.ctx` data frame and the `p' ↔ .before op` point image. - have hSPatEqLem : sPat.EquationLemmaAt (InsertPoint.before op) (by grind) := sorry + have hSPatEqLem : sPat.EquationLemmaAt (InsertPoint.before op) (by grind) := by + intro op' op'In hPure' hDom' + -- An operation never dominates the point before itself, so `op' ≠ op`. + have hne : op' ≠ op := by + rintro rfl + exact absurd rfl + (OperationPtr.strictlyDominates_def.mp (OperationPtr.dominatesIp_before.mp hDom')).2 + -- Reflect `op'` back to the source context (created ops are detached, dominate nothing). + have hCreated : WfIRContext.WithCreatedOps rewriter.ctx newCtxPat := + hValid.returnCtxChanges rewriter.ctx op newCtxPat newOps newValues hpat + obtain ⟨op'Src, hDomSrc⟩ := hCreated.op_dominatesIp_before_reflect hDom' + have op'In' : op'.InBounds rewriter'.ctx.raw := hRW.survives op' op'Src hne + -- `op'` dominates `op`, so its operands are defined before `op` — never one of `op`'s results. + have hOpDom : op'.dominates op rewriter.ctx := + OperationPtr.dominates_of_strictlyDominates (OperationPtr.dominatesIp_before.mp hDomSrc) + obtain ⟨hIntr, hOpEq⟩ := hSurvFrame op' op'In hne + have hOperandsEq : + op'.getOperands! newCtxPat.raw = op'.getOperands! rewriter'.ctx.raw := by + apply hOpEq + intro v hv + have hvSrc : v ∈ op'.getOperands! rewriter.ctx.raw := by + rwa [hCreated.getOperands_eq op'Src] at hv + have hvNotRes : v ∉ op.getResults! rewriter.ctx.raw := + IRContext.Dom.value_not_in_results_of_forall_in_operands_of_dominates hSrcDom + hOpDom v hvSrc + have hvIn : v.InBounds rewriter.ctx.raw := by + obtain ⟨i, hi, hvi⟩ := Array.getElem_of_mem hvSrc + rw [← hvi, OperationPtr.getOperands!.getElem_eq_getOperand!] + grind + exact hRW.mapNonResultsInBounds v hvIn hvNotRes + -- `OpDataEq` between the two contexts, either direction (for `Pure` and `EquationHolds`). + have hDataEqNP : OpDataEq op' newCtxPat rewriter'.ctx := + OpDataEq.of_sameIntrinsic hIntr hOperandsEq + have hDataEqPN : OpDataEq op' rewriter'.ctx newCtxPat := + OpDataEq.of_sameIntrinsic hIntr.symm hOperandsEq.symm + -- `op'` is pure and dominates `p'` in the driver context, so `s'` records its results. + have hPureTgt : op'.Pure rewriter'.ctx := hPure'.of_opDataEq hDataEqNP + have hDomP' : op'.dominatesIp p' rewriter'.ctx := hCoupleOp op' op'Src hDomSrc + have hHolds' : s'.EquationHolds op' := hEqLem' op' op'In' hPureTgt hDomP' + -- Transport that record back onto `sPat` (same variable map, matching op data). + exact InterpreterState.EquationHolds.sameData_transport hDataEqPN + ⟨hsPatData.1.symm, hsPatData.2.symm⟩ op'In' op'In hHolds' -- (D-in) Reconcile the given `hRW.σ`-refinement at `(.before op, p')` (target `rewriter'.ctx`) into -- the `LocalRewritePattern.mapping`-refinement at `(.before op, .before op)` (target `newCtxPat`) -- that `preservesSemantics` consumes. `mapping` and `hRW.σ` agree on `.val` @@ -5274,18 +5493,85 @@ theorem RewrittenAt.isModuleRefinedBy_of_fromLocalRewrite -- Memory agrees: `newState.memory = newState'.memory` (`preservesSemantics`) `= sTgt.memory` -- (the redirect transport preserves data). refine ⟨hMemEq.trans hSameRes.2, ?_⟩ - -- Variable refinement, by cases on the source value `val`: - -- • `val = op.getResult i` (source scope `after! op` adds exactly `op`'s results): `σ` sends it - -- to `newValues[i]`; `sTgt` shares `newState'`'s map (`hSameRes`), so the target value is - -- `targetValues[i]!` and the source value `sourceValues[i]!`, refined by `hValRef`. - -- • surviving `val` (dominates `before op`, ∉ `newOps`-results): `interpretOp`/`interpretOpList` - -- leave `getVar? val` untouched, so source = `s.getVar? val`, target = `s'.getVar? val`; the - -- output target scope `afterLast newOps p'` steps back to `p'` *within* `rewriter'.ctx` (`val` - -- is not a `newOps` result), where `hRef` applies. This is SOUND for arbitrary `p'` — no - -- before-op↔p' cross-context transport — but still needs a list-level `interpretOpList` - -- `getVar?`-preservation lemma and an intra-context `afterLast newOps → p'` dominance step-back - -- lemma, neither of which exists yet. - sorry + intro val valIn hSrcScope hTgtScope sv tv hsv htv + simp only [RefinementPoint.inScope_at] at hSrcScope hTgtScope + -- `sTgt` carries `newState'`'s variable map (`hSameRes`), so target lookups read `newState'`. + have hTgtMap : ∀ x, sTgt.variables.getVar? x = newState'.variables.getVar? x := by + intro x; simp only [VariableState.getVar?, hSameRes.1] + by_cases hres : val ∈ op.getResults! rewriter.ctx.raw + · -- `val = op`'s `idx`-th result (`idx := idxOf val`): `σ` sends it to `newValues[idx]`; the + -- source value is `sourceValues[idx]`, the target value `targetValues[idx]`, refined + -- elementwise by `hValRef`. + have hgr : op.getResults rewriter.ctx.raw = op.getResults! rewriter.ctx.raw := + (OperationPtr.getResults!_eq_getResults opInBounds).symm + have hidxlt : (op.getResults! rewriter.ctx.raw).idxOf val + < (op.getResults! rewriter.ctx.raw).size := Array.idxOf_lt_length_of_mem hres + have hgetidx : (op.getResults! rewriter.ctx.raw)[(op.getResults! rewriter.ctx.raw).idxOf val]! + = val := by + have h := Array.getElem?_idxOf (l := op.getResults! rewriter.ctx.raw) (x := val) hidxlt + simp only [Array.getElem!_eq_getD, Array.getD_eq_getD_getElem?, h, Option.getD_some] + have hmu : (hRW.σ ⟨val, valIn⟩).val + = newValues[(op.getResults! rewriter.ctx.raw).idxOf val]! := by + simp only [RewrittenAt.σ, rewriteMapping]; rw [dif_pos hres] + -- Sizes: `sourceValues`/`newValues`/`targetValues` all have `op`'s result count. + have hsrcsz : (op.getResults! rewriter.ctx.raw).size = sourceValues.size := by + rw [← hgr]; exact Array.size_eq_of_mapM_eq_some hSourceValues + have htgtsz : newValues.size = targetValues.size := + Array.size_eq_of_mapM_eq_some hTargetValues + have hnvsz : newValues.size = (op.getResults! rewriter.ctx.raw).size := by + rw [hRW.newValuesSize, OperationPtr.getResults!.size_eq_getNumResults!] + -- Source value: `sourceValues[idx] = sv`. + have hsvidx : sourceValues[(op.getResults! rewriter.ctx.raw).idxOf val]! = sv := by + have hh := (Array.mapM_eq_some_iff_of_size_eq (by rw [hgr]; exact hsrcsz)).mp + hSourceValues ((op.getResults! rewriter.ctx.raw).idxOf val) (by rw [hgr]; exact hidxlt) + rw [hgr, hgetidx, hsv] at hh; injection hh with hh; exact hh.symm + -- Target value: `targetValues[idx] = tv`. + have htvidx : targetValues[(op.getResults! rewriter.ctx.raw).idxOf val]! = tv := by + have hh := (Array.mapM_eq_some_iff_of_size_eq htgtsz).mp + hTargetValues ((op.getResults! rewriter.ctx.raw).idxOf val) (by omega) + rw [hTgtMap, hmu] at htv; rw [htv] at hh; injection hh with hh; exact hh.symm + -- Refined by `hValRef` at index `idx`. + have := hValRef.2 ((op.getResults! rewriter.ctx.raw).idxOf val) (by omega) + rw [hsvidx, htvidx] at this; exact this + · -- Surviving `val` (dominates `before op`, not a result of `op` nor of any `newOp`): both the + -- source `interpretOp op` and the target `interpretOpList newOps` leave `getVar? val` + -- untouched, so the goal reduces to `hRef` at `(before op, p')`, whose target scope `val` + -- reaches via `hCouple`. + -- Source scope: `val.dominatesIp (after! op)` with `val ∉ op`'s results ⇒ `val` dominates + -- `before op`. + rw [InsertPoint.after!_eq_after hRW.opParent opInBounds, + hSrcDom.value_dominatesIp_after_iff] at hSrcScope + have hValDomBefore : val.dominatesIp (InsertPoint.before op) rewriter.ctx := + hSrcScope.resolve_right hres + -- `σ` fixes `val`. + have hσval : (hRW.σ ⟨val, valIn⟩).val = val := hRW.mappingFixesNonResults val valIn hres + -- Source lookup is unchanged by the `op` step. + have hsSrc : s.variables.getVar? val = some sv := by + obtain ⟨ov, rv, mem', vs', hov, hint, hset, hst⟩ := interpretOp_some_iff.mp hsrc + rw [hst] at hsv + rwa [VariableState.getVar?_setResultValues?_of_notMem_getResults! hres hset] at hsv + -- `val` is not produced by any `newOp` (its results are fresh, `val` is in bounds of the + -- source), so the `newOps` run leaves `val` untouched. + have hvNotProduced : ∀ o ∈ newOps.toList, val ∉ o.getResults! newCtxPat.raw := by + intro o ho hmem + obtain ⟨j, hj, hval⟩ := OperationPtr.getResults!.mem_iff_exists_index.mp hmem + have hoSrc : o.InBounds rewriter.ctx.raw := by + rw [← hval] at valIn + simp only [OperationPtr.getResult, ValuePtr.inBounds_opResult, + OpResultPtr.inBounds_def] at valIn + obtain ⟨ho', _⟩ := valIn; exact ho' + exact hRW.newOps_not_inBounds_source o (by simpa using ho) hoSrc + -- Target lookup is unchanged by the `newOps` run: `sTgt.getVar? val = s'.getVar? val`. + have hsTgt : s'.variables.getVar? val = some tv := by + rw [hσval, hTgtMap, + interpretOpList_getVar?_of_not_produced hRunPat hvNotProduced] at htv + have hpe : sPat.variables.getVar? val = s'.variables.getVar? val := by + simp only [VariableState.getVar?, hsPatData.1] + rwa [hpe] at htv + -- Apply `hRef` at `val`: source scope `hValDomBefore`, target scope via `hCouple`. + have hsTgt' : s'.variables.getVar? (hRW.σ ⟨val, valIn⟩).val = some tv := by + rw [hσval]; exact hsTgt + exact hRef.2 val valIn hValDomBefore (hCouple val valIn hValDomBefore) sv tv hsSrc hsTgt' · -- Control-flow actions coincide (`cf` on both sides). exact ControlFlowAction.optionIsRefinedBy_refl cf · -- `.ub` (source undefined behaviour): refinement trivial.