lean4-htt/tests/bench/sym/shallow_add_sub_cancel.lean
Leonardo de Moura 6de7100f69
feat: add Goal API for SymM + grind (#12143)
This PR adds an API for building symbolic simulation engines and
verification
condition generators that leverage `grind`. The API wraps `Sym`
operations to
work with `grind`'s `Goal` type, enabling lightweight symbolic execution
while
carrying `grind` state for discharge steps.

New operations on `Goal`:
- `mkGoal`: create a `Goal` from an `MVarId`
- `introN`, `intros`: introduce binders
- `apply`: apply backward rules
- `simp`, `simpIgnoringNoProgress`: simplify using `Sym.Simp`
- `internalize`, `internalizeAll`: add hypotheses to the E-graph
- `grind`: attempt to close the goal using `grind`
- `assumption`: close by matching a hypothesis

A new test demonstrates the API on a stateful program with conditionals,
using `grind` to discharge arithmetic side conditions.
2026-01-24 20:30:08 +00:00

178 lines
6.3 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Lean
/-!
Benchmark similar to `add_sub_cancel` but using a shallow embedding into monadic `do` notation.
-/
def Exec (s : S) (k : StateM S α) (post : α → S → Prop) : Prop :=
post (k s).1 (k s).2
theorem Exec.pure (a : α) :
post a s → Exec s (pure a) post := by
simp [Exec, Pure.pure, StateT.pure]
theorem Exec.bind (k₁ : StateM S α) (k₂ : α → StateM S β) (post : β → S → Prop) :
Exec s k₁ (fun a s₁ => Exec s₁ (k₂ a) post)
→ Exec s (k₁ >>= k₂) post := by
simp [Exec, Bind.bind, StateT.bind]
cases k₁ s; simp
theorem Exec.andThen (k₁ : StateM S α) (k₂ : StateM S β) (post : β → S → Prop) :
Exec s k₁ (fun _ s₁ => Exec s₁ k₂ post)
→ Exec s (k₁ *> k₂) post := by
simp [Exec, SeqRight.seqRight, StateT.bind, Bind.bind]
cases k₁ s; simp
theorem Exec.get : post s s → Exec s get post := by
simp [Exec, MonadState.get, getThe, MonadStateOf.get, StateT.get, Pure.pure]
theorem Exec.set : post () s' → Exec s (set s') post := by
simp [Exec, MonadStateOf.set, StateT.set, Pure.pure]
theorem Exec.modify : post () (f s) → Exec s (modify f) post := by
simp [Exec, _root_.modify, modifyGet, MonadStateOf.modifyGet, StateT.modifyGet, Pure.pure]
theorem Exec.ite_true {_ : Decidable c} (t e : StateM S α) :
c → Exec s t post → Exec s (if c then t else e) post := by
intro h; simp [*]
theorem Exec.ite_false {_ : Decidable c} (t e : StateM S α) :
¬ c → Exec s e post → Exec s (if c then t else e) post := by
intro h; simp [*]
theorem Exec.ite {_ : Decidable c} (t e : StateM S α) :
(c → Exec s t post) → (¬ c → Exec s e post) → Exec s (if c then t else e) post := by
intro h₁ h₂; split
next h => exact h₁ h
next h => exact h₂ h
theorem modify_eq : (modify f : StateM S Unit) s = ((), f s) := by
simp [modify, modifyGet, MonadStateOf.modifyGet, StateT.modifyGet, pure]
def step (v : Nat) : StateM Nat Unit := do
let s ← get
set (s + v)
let s ← get
set (s - v)
def loop (n : Nat) : StateM Nat Unit := do
match n with
| 0 => pure ()
| n+1 => step n; loop n
def Goal (n : Nat) : Prop := ∀ s post, post () s → Exec s (loop n) post
open Lean Meta Elab
/-- Helper function for executing a tactic `k` for solving `Goal n`. -/
def driver (n : Nat) (check := true) (k : MVarId → MetaM Unit) : MetaM Unit := do
let some goal ← unfoldDefinition? (mkApp (mkConst ``Goal) (mkNatLit n)) | throwError "UNFOLD FAILED!"
let mvar ← mkFreshExprMVar goal
let startTime ← IO.monoNanosNow
k mvar.mvarId!
let endTime ← IO.monoNanosNow
let ms := (endTime - startTime).toFloat / 1000000.0
if check then
let startTime ← IO.monoNanosNow
checkWithKernel (← instantiateExprMVars mvar)
let endTime ← IO.monoNanosNow
let kernelMs := (endTime - startTime).toFloat / 1000000.0
IO.println s!"goal_{n}: {ms} ms, kernel: {kernelMs} ms"
else
IO.println s!"goal_{n}: {ms} ms"
/-!
`MetaM` Solution
-/
/-
A tactic for solving goal `Goal n`
-/
macro "solve" : tactic => `(tactic| {
intro s post; intro n;
simp only [loop, step, Nat.add_zero, Nat.sub_zero, bind_pure_comp, map_bind, id_map', bind_assoc];
repeat (apply Exec.bind; apply Exec.get; apply Exec.bind; apply Exec.set);
apply Exec.bind; apply Exec.get; apply Exec.set;
simp only [Nat.add_sub_cancel]; assumption
})
/--
Solves a goal of the form `Goal n` using the `solve` tactic.
-/
def solveUsingMeta (n : Nat) (check := true) : MetaM Unit := do
driver n check fun mvarId => do
let ([], _) ← runTactic mvarId (← `(tactic| solve)).raw {} {} | throwError "FAILED!"
def runBenchUsingMeta : MetaM Unit := do
IO.println "=== Symbolic Simulation Tests ==="
IO.println ""
for n in [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] do
solveUsingMeta n
set_option maxRecDepth 10000
set_option maxHeartbeats 10000000
#eval runBenchUsingMeta
/-!
`SymM` Solution
-/
open Sym
theorem unit_map : (fun _ : Unit => PUnit.unit) <$> (k : StateM Nat Unit) = k := by
simp
def mkSimpMethods (declNames : Array Name) : MetaM Sym.Simp.Methods := do
let rewrite ← Sym.mkSimprocFor declNames Sym.Simp.dischargeSimpSelf
return {
post := Sym.Simp.evalGround.andThen rewrite
}
partial def solve (mvarId : MVarId) : SymM Unit := do
/-
Creates an `BackwardRule` for each theorem `T` we want to use `apply T`.
-/
let execBindRule ← mkBackwardRuleFromDecl ``Exec.bind
let execGetRule ← mkBackwardRuleFromDecl ``Exec.get
let execSetRule ← mkBackwardRuleFromDecl ``Exec.set
/-
Creates simplification methods for each collection of rewriting rules we want to apply.
Recall Lean creates equational lemmas of the form `_eq_<idx>` for definitions.
-/
let preMethods ← mkSimpMethods #[``step.eq_1, ``loop.eq_1, ``loop.eq_2,
``Nat.add_zero, ``Nat.sub_zero, ``bind_pure_comp, ``map_bind, ``id_map', ``unit_map, ``bind_assoc]
let postMethods ← mkMethods #[``Nat.add_sub_cancel]
-- ## Initialize
-- `processMVar` ensures the input goal becomes a `Sym` compatible goal.
let mvarId ← preprocessMVar mvarId
-- `intro s post n`
let .goal _ mvarId ← Sym.introN mvarId 3 | failure
let .goal mvarId ← Sym.simpGoal mvarId preMethods | failure
-- ## Loop
-- We simulate the `repeat` block using a tail-recursive function `loop`
let rec loop (mvarId₀ : MVarId) : SymM MVarId := do
-- apply Exec.bind; apply Exec.get; apply Exec.bind; apply Exec.set
let .goals [mvarId] ← execBindRule.apply mvarId₀ | return mvarId₀
let .goals [mvarId] ← execGetRule.apply mvarId | return mvarId₀
let .goals [mvarId] ← execBindRule.apply mvarId | return mvarId₀
let .goals [mvarId] ← execSetRule.apply mvarId | return mvarId₀
loop mvarId
let mvarId ← loop mvarId
let .goals [mvarId] ← execBindRule.apply mvarId | failure
let .goals [mvarId] ← execGetRule.apply mvarId | failure
let .goals [mvarId] ← execSetRule.apply mvarId | failure
let .goal mvarId ← Sym.simpGoal mvarId postMethods | failure
mvarId.assumption
return
def solveUsingSym (n : Nat) (check := true) : MetaM Unit := do
driver n check fun mvarId => SymM.run do solve mvarId
def runBenchUsingSym : MetaM Unit := do
IO.println "=== Symbolic Simulation Tests ==="
IO.println ""
for n in [10, 20, 30, 40, 50, 60, 70, 80, 90, 100] do
solveUsingSym n
#eval runBenchUsingSym
#eval solveUsingSym 1000