lean4-htt/tests/elab/mvcgenInvariantsSuggestions.lean
Sebastian Graf 734566088f
feat: add withEarlyReturnNewDo variants for new do elaborator (#12881)
This PR adds `Invariant.withEarlyReturnNewDo`,
`StringInvariant.withEarlyReturnNewDo`, and
`StringSliceInvariant.withEarlyReturnNewDo` which use `Prod` instead of
`MProd` for the state tuple, matching the new do elaborator's output.
The existing `withEarlyReturn` definitions are reverted to `MProd` for
backwards compatibility with the legacy do elaborator. Tests and
invariant suggestions are updated to use the `NewDo` variants.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 10:44:34 +00:00

363 lines
10 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

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

import Std.Tactic.Do
import Std
set_option backward.do.legacy false
open Std Do
set_option grind.warning false
set_option mvcgen.warning false
set_option pp.rawOnError true
set_option warn.sorry false
def mySum (l : List Nat) : Nat := Id.run do
let mut acc := 0
for x in l do
acc := acc + x
return acc
/--
info: Try this:
[apply] invariants
· ⇓⟨xs, letMuts⟩ => ⌜xs.prefix = [] ∧ letMuts = 0 xs.suffix = [] ∧ letMuts = l.sum⌝
-/
#guard_msgs (info) in
theorem mySum_suggest_invariant (l : List Nat) : mySum l = l.sum := by
generalize h : mySum l = r
apply Id.of_wp_run_eq h
mvcgen invariants?
all_goals admit
/--
info: Try this:
[apply] mvcgen invariants?
---
info: Try this:
[apply] mvcgen [mySum] invariants?
---
info: Try this:
[apply] mvcgen +elimLets invariants?
---
info: Try this:
[apply] mvcgen +elimLets [mySum] invariants?
-/
#guard_msgs (info) in
theorem mySum_suggest_invariant_short (l : List Nat) : mySum l = l.sum := by
generalize h : mySum l = r
apply Id.of_wp_run_eq h
mvcgen?
mvcgen? [mySum]
mvcgen? +elimLets
mvcgen? +elimLets [mySum]
all_goals admit
def mySum2 (l : List Nat) : Nat := Id.run do
let mut acc := 0
let mut acc2 := 0
for x in l do
acc := acc + x
acc2 := acc2 + x
return acc
/--
info: Try this:
[apply] invariants
· ⇓⟨xs, letMuts⟩ => ⌜xs.prefix = [] ∧ letMuts = (0, 0) xs.suffix = [] ∧ letMuts.fst = l.sum⌝
-/
#guard_msgs (info) in
theorem mySum2_suggest_invariant (l : List Nat) : mySum2 l = l.sum := by
generalize h : mySum2 l = r
apply Id.of_wp_run_eq h
mvcgen invariants?
all_goals admit
def copy (l : List Nat) : Id (Array Nat) := do
let mut acc := #[]
for x in l do
acc := acc.push x
return acc
/--
info: Try this:
[apply] invariants
· ⇓⟨xs, letMuts⟩ => ⌜xs.prefix = [] ∧ letMuts = acc✝ xs.suffix = [] ∧ letMuts = l.toArray⌝
-/
#guard_msgs (info) in
theorem copy_suggest_invariant (l : List Nat) : ⦃⌜True⌝⦄ copy l ⦃⇓ r => ⌜r = l.toArray⌝⦄ := by
mvcgen [copy] invariants?
/-
case inv1
l : List Nat
acc✝ : Array Nat := #[]
⊢ Invariant l (Array Nat) PostShape.pure
...
-/
all_goals admit
def nodup (l : List Int) : Bool := Id.run do
let mut seen : HashSet Int := ∅
for x in l do
if x ∈ seen then
return false
seen := seen.insert x
return true
/--
info: Try this:
[apply] invariants
·
Invariant.withEarlyReturnNewDo (onReturn := fun r letMuts => ⌜(r = true ↔ l.Nodup) ∧ l.Nodup⌝) (onContinue :=
fun xs letMuts => ⌜xs.prefix = [] ∧ letMuts = ∅ xs.suffix = [] ∧ l.Nodup⌝)
-/
#guard_msgs (info) in
theorem nodup_suggest_invariant (l : List Int) : nodup l ↔ l.Nodup := by
generalize h : nodup l = r
apply Id.of_wp_run_eq h
mvcgen invariants?
all_goals admit
def nodup_twice (l : List Int) : Bool := Id.run do
let mut seen : HashSet Int := ∅
for x in l do
if x ∈ seen then
return false
seen := seen.insert x
let mut seen2 : HashSet Int := ∅
for x in l do
if x ∈ seen2 then
return false
seen2 := seen2.insert x
return true
/--
info: Try this:
[apply] invariants
·
Invariant.withEarlyReturnNewDo (onReturn := fun r letMuts =>
spred({ down := r = true ↔ l.Nodup } ∧ Prod.fst ?inv2 ({ «prefix» := [], suffix := l, property := ⋯ }, none, ∅)))
(onContinue := fun xs letMuts =>
spred({ down := xs.prefix = [] ∧ letMuts = ∅ }
⌜xs.suffix = []⌝ ∧
{ down := True } ∧ Prod.fst ?inv2 ({ «prefix» := [], suffix := l, property := ⋯ }, none, ∅)))
·
Invariant.withEarlyReturnNewDo (onReturn := fun r letMuts => ⌜(r = true ↔ l.Nodup) ∧ l.Nodup⌝) (onContinue :=
fun xs letMuts => ⌜xs.prefix = [] ∧ letMuts = ∅ xs.suffix = [] ∧ l.Nodup⌝)
-/
#guard_msgs (info) in
theorem nodup_twice_suggest_invariant (l : List Int) : nodup_twice l ↔ l.Nodup := by
generalize h : nodup_twice l = r
apply Id.of_wp_run_eq h
mvcgen invariants?
all_goals admit
structure Supply where
counter : Nat
def mkFresh [Monad m] : StateT Supply m Nat := do
let n ← (·.counter) <$> get
modify (fun s => {s with counter := s.counter + 1})
pure n
abbrev AppM := StateT Bool (StateT Supply (StateM String))
abbrev liftCounterM : StateT Supply (StateM String) α → AppM α := liftM
def mkFreshN (n : Nat) : AppM (List Nat) := do
let mut acc := #[]
for _ in [:n] do
let n ← liftCounterM mkFresh
acc := acc.push n
return acc.toList
-- In the following, we suggest an inaccessible variable `acc` in the invariant. Unfortunate.
/--
info: Try this:
[apply] invariants
· ⇓⟨xs, letMuts⟩ => ⌜xs.prefix = [] ∧ letMuts = acc✝ xs.suffix = [] ∧ letMuts.toList.Nodup⌝
-/
#guard_msgs (info) in
theorem mkFreshN_suggest_invariant (n : Nat) : ⦃⌜True⌝⦄ mkFreshN n ⦃⇓ r => ⌜r.Nodup⌝⦄ := by
mvcgen [mkFreshN, mkFresh, liftCounterM] invariants?
all_goals admit
def mkFreshN_early_return (n : Nat) : AppM (List Nat) := do
let mut acc := #[]
for _ in [:n] do
let n ← liftCounterM mkFresh
if n > 13 then return acc.toList
acc := acc.push n
return acc.toList
/--
info: Try this:
[apply] invariants
·
Invariant.withEarlyReturnNewDo (onReturn := fun r letMuts => ⌜r.Nodup ∧ letMuts.toList.Nodup⌝) (onContinue :=
fun xs letMuts => ⌜xs.prefix = [] ∧ letMuts = acc✝ xs.suffix = [] ∧ letMuts.toList.Nodup⌝)
-/
#guard_msgs (info) in
theorem mkFreshN_early_return_suggest_invariant (n : Nat) : ⦃⌜True⌝⦄ mkFreshN_early_return n ⦃⇓ r => ⌜r.Nodup⌝⦄ := by
mvcgen [mkFreshN_early_return, mkFresh, liftCounterM] invariants?
all_goals admit
def earlyReturnWithoutLetMut (l : List Int) : Bool := Id.run do
for x in l do
if x < 0 then return true
return true
/--
info: Try this:
[apply] invariants
·
Invariant.withEarlyReturnNewDo (onReturn := fun r letMuts => ⌜r = true⌝) (onContinue := fun xs letMuts =>
⌜xs.prefix = [] xs.suffix = []⌝)
-/
#guard_msgs (info) in
theorem earlyReturnWithoutLetMut_suggest_invariant (l : List Int) : earlyReturnWithoutLetMut l := by
generalize h : earlyReturnWithoutLetMut l = r
apply Id.of_wp_run_eq h
mvcgen invariants?
all_goals admit
def notQuiteEarlyReturn (l : List Nat) : Option Nat := Id.run do
-- The idea here is that the type of the state tuple *looks* like it's an early return, but `last`
-- is not used as if it is an early return variable.
let mut last : Option Nat := none
let mut mdummy : Unit := () -- m* is important because let mut vars are sorted alphabetically
for x in l do
last := some x
mdummy := ()
return last
/--
info: Try this:
[apply] invariants
· ⇓⟨xs, letMuts⟩ => ⌜xs.prefix = [] ∧ letMuts = (none, ()) xs.suffix = [] ∧ letMuts.fst = l.getLast?⌝
-/
#guard_msgs (info) in
theorem notQuiteEarlyReturn_suggest_invariant (l : List Nat) : notQuiteEarlyReturn l = l.getLast? := by
generalize h : notQuiteEarlyReturn l = r
apply Id.of_wp_run_eq h
mvcgen invariants?
all_goals admit
def polySum [Monad m] (l : List Nat) : m Nat := do
let mut acc := 0
let mut acc2 := 0
for x in l do
acc := acc + x
acc2 := acc2 + x
return acc
/--
info: Try this:
[apply] invariants
· ⇓⟨xs, letMuts⟩ =>
⌜xs.prefix = [] ∧ letMuts = (0, 0)
xs.suffix = [] ∧
let acc := letMuts.fst;
acc = l.sum⌝
-/
#guard_msgs (info) in
theorem polySum_suggest_invariant [Monad m] [WPMonad m ps] (l : List Nat) : ⦃⌜True⌝⦄ @polySum m _ l ⦃⇓ r => ⌜r = l.sum⌝⦄ := by
mvcgen [polySum] invariants?
all_goals admit
def polyNodup [Monad m] (l : List Int) : m Bool := do
let mut seen : HashSet Int := ∅
for x in l do
if x ∈ seen then
return false
seen := seen.insert x
return true
/--
info: Try this:
[apply] invariants
·
Invariant.withEarlyReturnNewDo (onReturn := fun r letMuts => ⌜(r = true ↔ l.Nodup) ∧ l.Nodup⌝) (onContinue :=
fun xs letMuts => ⌜xs.prefix = [] ∧ letMuts = seen✝ xs.suffix = [] ∧ l.Nodup⌝)
-/
#guard_msgs (info) in
theorem polyNodup_suggest_invariant [Monad m] [WPMonad m ps] (l : List Int) : ⦃⌜True⌝⦄ @polyNodup m _ l ⦃⇓r => ⌜r ↔ l.Nodup⌝⦄ := by
mvcgen [polyNodup] invariants?
all_goals admit
def fast_expo (x n : Nat) : Nat := Id.run do
let mut x := x
let mut y := 1
let mut e := n
for _ in [:n] do -- simulating a while loop running at most n times
if e = 0 then break
if e % 2 = 1 then
y := x * y
e := e - 1
else
x := x*x
e := e/2
return y
open Std.Do
/--
info: Try this:
[apply] invariants
· ⇓⟨xs, letMuts⟩ => ⌜xs.prefix = [] ∧ letMuts = (x, 1, n) xs.suffix = [] ∧ letMuts.snd.fst = x ^ n⌝
-/
#guard_msgs (info) in
theorem fast_expo_correct (x n : Nat) : fast_expo x n = x^n := by
generalize h : fast_expo x n = r
apply Id.of_wp_run_eq h
mvcgen invariants?
all_goals admit
namespace FreshBounded
structure Supply where
counter : Nat
limit : Nat
property : counter ≤ limit
def mkFresh : EStateM String Supply Nat := do
let supply ← get
if h : supply.counter = supply.limit then
throw s!"Supply exhausted: {supply.counter} = {supply.limit}"
else
let n := supply.counter
have := supply.property
set {supply with counter := n + 1, property := by omega}
pure n
def mkFreshN (n : Nat) : ExceptT Char (EStateM String Supply) (List Nat) := do
let mut acc := #[]
for _ in [:n] do
acc := acc.push (← mkFresh)
pure acc.toList
@[spec]
theorem mkFresh_spec (c : Nat) :
⦃fun state => ⌜state.counter = c⌝⦄
mkFresh
⦃post⟨fun r state => ⌜r = c ∧ c < state.counter⌝, fun _msg state => ⌜c = state.counter ∧ c = state.limit⌝⟩⦄ := by
mvcgen [mkFresh]
all_goals grind
/--
info: Try this:
[apply] invariants
·
post⟨fun ⟨xs, letMuts⟩ => ⌜xs.prefix = [] ∧ letMuts = acc✝ xs.suffix = [] ∧ letMuts.toList.Nodup⌝, fun x =>
⌜False⌝, fun _msg state => ⌜state.counter = state.limit⌝⟩
-/
#guard_msgs (info) in
theorem mkFreshN_spec (n : Nat) :
⦃⌜True⌝⦄
mkFreshN n
⦃post⟨fun r => ⌜r.Nodup⌝, fun _ => ⌜False⌝, fun _msg state => ⌜state.counter = state.limit⌝⟩⦄ := by
mvcgen [mkFreshN] invariants?
-- The full invariant is:
-- · post⟨fun ⟨xs, acc⟩ state => ⌜(∀ n ∈ acc, n < state.counter) ∧ acc.toList.Nodup⌝,
-- fun _ => ⌜False⌝,
-- fun _msg state => ⌜state.counter = state.limit⌝⟩
all_goals admit
end FreshBounded