Zulip Chat Archive
Stream: general
Topic: new monadic program verification framework
Asei Inoue (Jul 22 2025 at 23:42):
@Sebastian Graf
I would like to know how I can use https://github.com/sgraf812/mpl, which has been upstreamed into Lean. I read the README, but I couldn’t understand it very well. For example, with your framework, is it possible to prove the following?
def peasantMul (x y : Nat) : Nat := Id.run do
let mut x := x
let mut y := y
let mut prod := 0
while x > 0 do
if x % 2 = 1 then
prod := prod + y
x := x / 2
y := y * 2
return prod
example (x y : Nat) : peasantMul x y = x * y := by
sorry
Eric Wieser (Jul 22 2025 at 23:48):
I think that's unprovable, because while is partial / docs#Lean.Loop.forIn.loop is opaque
Asei Inoue (Jul 23 2025 at 00:02):
@Eric Wieser Thank you! "I'll give a different example.
how about this?
def Nat.isPrime (n : Nat) : Bool := Id.run do
if n ≤ 1 then
return false
for d in [2:n] do
if n % d = 0 then
return false
if d ^ 2 > n then
break
return true
#guard Nat.isPrime 2
#guard Nat.isPrime 3
#guard Nat.isPrime <$> [4, 5, 6, 7] == [false, true, false, true]
#guard Nat.isPrime 103
example (n d : Nat) (h : Nat.isPrime n) (h1 : 2 ≤ d) (h2 : d < n) : ¬ d ∣ n := by
sorry
Bhavik Mehta (Jul 23 2025 at 05:47):
Sebastian Graf (Jul 23 2025 at 07:34):
There's some material in .
Note that the lack of documentation is due to Std.Do still being a pre-release.
Regarding example code, there's Markus' blog post giving Std.Do a try and the kitchen sink test file I've been using.
Here's a solution using Std.Do for the isPrime variant without the d^2 > n test. The version including the test is doable as well, but it would take me quite a bit of time to recast the wlog d^2 <= n and use that fact to reason about ranges. It's likely that you would need to tweak the loop invariant to
case inv => exact (⇓ (⟨x, _⟩, xs) => ⌜xs.suff = [] ∧ x = .some false ∨ x = .none ∧ (∀ d, d ∈ xs.pref → d^2 ≤ n ∧ ¬ d ∣ n)⌝)
(The d^2 <= n is new.)
Perhaps someone would like to do that and in the process come up with nice grind theorems that the API around Std.List.Zipper is currently lacking?
Patrick Massot (Jul 23 2025 at 10:06):
Was this blog post advertised before and I missed it? I think this is a fantastic read (both because the post is nice and the documented feature is awesome) and everybody here should read it, even mathematicians with very little interest in programming.
Sebastian Graf (Jul 23 2025 at 10:08):
I don't think it was more widely announced than in and then referred to a couple of times in discussions
Patrick Massot (Jul 23 2025 at 10:10):
That answers my question: I was not aware of the Program verification channel (and not subscribed to it).
Patrick Massot (Jul 23 2025 at 10:11):
I guess we’ll have a proper announcement when the framework is ready to use and documented, but that’s already very exciting.
Patrick Massot (Jul 23 2025 at 10:22):
Are there plans to make the tactic states less frightening for cases where people want to provide manual proofs?
Sebastian Graf (Jul 23 2025 at 10:36):
Thanks for giving it a try! So far, UX for mvcgen is top priority. In the small experiments I did so far (and also in the medium-sized one that @Rish Vaishnav is conducting), it appears that the most comfortable way to prove things is by immediately leaving the SPred proof mode and then using grind/the regular arsenal of Lean tactics :) The simplest way to "leave the proof mode" is
simp_all only [Std.Do.SPred.entails_cons, Std.Do.SPred.entails_nil, Std.Do.SPred.and_cons, Std.Do.SPred.and_nil, Std.Do.SVal.curry_cons, Std.Do.SVal.curry_nil, Std.Do.SVal.uncurry_cons, Std.Do.SVal.uncurry_nil, Std.Do.SVal.getThe_here, Std.Do.SVal.getThe_there, and_imp, true_implies]
for which I implemented a tactic mleave in nightly. (This is only an option if your monad stack can be completely interpreted away, i.e., there's no polymorphic base layer such as in StateT Nat m Nat.)
Other than that, I agree that the SPred proof mode (which is very much inspired by Iris' proof mode) is a little wonky and perhaps scary to use. Using simp will immediately destroy the metadata for working in that proof mode, so if you want to stay in the proof mode you have to be quite careful.
I'm tempted to get rid of the proof mode entirely, but that would mean you will be having a hard time proving things about StateT Nat m Nat.
TLDR; future support for the proof mode unclear.
In the meantime, if you can show me what exactly frightens you, then maybe I can try and improve?
Patrick Massot (Jul 23 2025 at 11:50):
I’m sorry my message was super vague. I was simply playing with Markus’s example and looking at the tactic state:
5 goals
l : List Int
b : MProd (Option Bool) (HashSet Int)
rpref : List Int
x : Int
suff : List Int
h✝ : l = rpref.reverse ++ x :: suff
h : (fun x =>
match x with
| (⟨earlyReturn?, seen⟩, traversalState) =>
earlyReturn? = none ∧
(∀ (x : Int), x ∈ seen ↔ x ∈ traversalState.rpref) ∧
¬List.ExistsPair (fun a b => a + b = 0) traversalState.pref ∨
earlyReturn? = some true ∧ List.ExistsPair (fun a b => a + b = 0) l ∧ l = traversalState.pref,
()).fst
(b, { rpref := rpref, suff := x :: suff, property := ⋯ })
a✝ : -x ∈ b.snd
⊢
h✝ : ⌜True⌝
⊢ₛ
match ForInStep.done ⟨some true, b.snd⟩ with
| ForInStep.yield b' =>
(fun x =>
match x with
| (⟨earlyReturn?, seen⟩, traversalState) =>
earlyReturn? = none ∧
(∀ (x : Int), x ∈ seen ↔ x ∈ traversalState.rpref) ∧
¬List.ExistsPair (fun a b => a + b = 0) traversalState.pref ∨
earlyReturn? = some true ∧ List.ExistsPair (fun a b => a + b = 0) l ∧ l = traversalState.pref,
()).fst
(b', { rpref := x :: rpref, suff := suff, property := ⋯ })
| ForInStep.done b' =>
(fun x =>
match x with
| (⟨earlyReturn?, seen⟩, traversalState) =>
earlyReturn? = none ∧
(∀ (x : Int), x ∈ seen ↔ x ∈ traversalState.rpref) ∧
¬List.ExistsPair (fun a b => a + b = 0) traversalState.pref ∨
earlyReturn? = some true ∧ List.ExistsPair (fun a b => a + b = 0) l ∧ l = traversalState.pref,
()).fst
(b', { rpref := l.reverse, suff := [], property := ⋯ })
l : List Int
b : MProd (Option Bool) (HashSet Int)
rpref : List Int
x : Int
suff : List Int
h✝ : l = rpref.reverse ++ x :: suff
h : (fun x =>
match x with
| (⟨earlyReturn?, seen⟩, traversalState) =>
earlyReturn? = none ∧
(∀ (x : Int), x ∈ seen ↔ x ∈ traversalState.rpref) ∧
¬List.ExistsPair (fun a b => a + b = 0) traversalState.pref ∨
earlyReturn? = some true ∧ List.ExistsPair (fun a b => a + b = 0) l ∧ l = traversalState.pref,
()).fst
(b, { rpref := rpref, suff := x :: suff, property := ⋯ })
a✝ : ¬-x ∈ b.snd
⊢
h✝ : ⌜True⌝
⊢ₛ
match ForInStep.yield ⟨none, b.snd.insert x⟩ with
| ForInStep.yield b' =>
(fun x =>
match x with
| (⟨earlyReturn?, seen⟩, traversalState) =>
earlyReturn? = none ∧
(∀ (x : Int), x ∈ seen ↔ x ∈ traversalState.rpref) ∧
¬List.ExistsPair (fun a b => a + b = 0) traversalState.pref ∨
earlyReturn? = some true ∧ List.ExistsPair (fun a b => a + b = 0) l ∧ l = traversalState.pref,
()).fst
(b', { rpref := x :: rpref, suff := suff, property := ⋯ })
| ForInStep.done b' =>
(fun x =>
match x with
| (⟨earlyReturn?, seen⟩, traversalState) =>
earlyReturn? = none ∧
(∀ (x : Int), x ∈ seen ↔ x ∈ traversalState.rpref) ∧
¬List.ExistsPair (fun a b => a + b = 0) traversalState.pref ∨
earlyReturn? = some true ∧ List.ExistsPair (fun a b => a + b = 0) l ∧ l = traversalState.pref,
()).fst
(b', { rpref := l.reverse, suff := [], property := ⋯ })
case pre1
l : List Int
⊢
h✝ : ⌜True⌝
⊢ₛ
(fun x =>
match x with
| (⟨earlyReturn?, seen⟩, traversalState) =>
earlyReturn? = none ∧
(∀ (x : Int), x ∈ seen ↔ x ∈ traversalState.rpref) ∧
¬List.ExistsPair (fun a b => a + b = 0) traversalState.pref ∨
earlyReturn? = some true ∧ List.ExistsPair (fun a b => a + b = 0) l ∧ l = traversalState.pref,
()).fst
(⟨none, ∅⟩, { rpref := [], suff := l, property := ⋯ })
case h_1
l : List Int
r✝ : MProd (Option Bool) (HashSet Int)
x✝ : Option Bool
heq✝ : r✝.fst = none
h : (fun x =>
match x with
| (⟨earlyReturn?, seen⟩, traversalState) =>
earlyReturn? = none ∧
(∀ (x : Int), x ∈ seen ↔ x ∈ traversalState.rpref) ∧
¬List.ExistsPair (fun a b => a + b = 0) traversalState.pref ∨
earlyReturn? = some true ∧ List.ExistsPair (fun a b => a + b = 0) l ∧ l = traversalState.pref,
()).fst
(r✝, { rpref := l.reverse, suff := [], property := ⋯ })
⊢
⊢ₛ false = true ↔ List.ExistsPair (fun a b => a + b = 0) l
case h_2
l : List Int
r✝ : MProd (Option Bool) (HashSet Int)
x✝ : Option Bool
a✝ : Bool
heq✝ : r✝.fst = some a✝
h : (fun x =>
match x with
| (⟨earlyReturn?, seen⟩, traversalState) =>
earlyReturn? = none ∧
(∀ (x : Int), x ∈ seen ↔ x ∈ traversalState.rpref) ∧
¬List.ExistsPair (fun a b => a + b = 0) traversalState.pref ∨
earlyReturn? = some true ∧ List.ExistsPair (fun a b => a + b = 0) l ∧ l = traversalState.pref,
()).fst
(r✝, { rpref := l.reverse, suff := [], property := ⋯ })
⊢
⊢ₛ a✝ = true ↔ List.ExistsPair (fun a b => a + b = 0) l
Patrick Massot (Jul 23 2025 at 11:53):
Note this is showing 5 goals, but each one of them looks rather frightening to me.
Patrick Massot (Jul 23 2025 at 11:53):
However keep in mind I know nothing about program verification, I’m a mathematician. So maybe this is simply unavoidable.
Markus Himmel (Jul 23 2025 at 11:59):
I think this is just because mvcgen doesn't do any kind of simp at the end; after all_goals simp the goals become much more manageable.
Markus Himmel (Jul 23 2025 at 12:02):
To give an anecdotal data point: in my first version I didn't use grind at all, and I didn't have any trouble understanding the goals and finishing the proofs using the usual Lean tactics.
Asei Inoue (Jul 23 2025 at 12:03):
@Bhavik Mehta Thank you!! great work!!
Asei Inoue (Jul 23 2025 at 12:07):
@Sebastian Graf Thank you for answering my question. I’m going to take my time and carefully read the material you shared with me. Thanks again.
Patrick Massot (Jul 23 2025 at 12:22):
Indeed all_goals simp at * after the mvcgen does wonders. Maybe it would make sense to do this (probably with a curated simp set) and then also some rcases to nicely split the invariant?
Patrick Massot (Jul 23 2025 at 12:26):
In Markus example, the splitting thing could be try rcases h with ⟨h₁, h₂, h₃⟩|⟨h₁, h₂, h₃⟩ based and the shape of the invariant.
Patrick Massot (Jul 23 2025 at 12:27):
And that could be opt-in, with something like mvcgen!.
Sebastian Graf (Jul 23 2025 at 12:35):
I think this is just because
mvcgendoesn't do any kind ofsimpat the end; afterall_goals simpthe goals become much more manageable.
Indeed. I'm beginning to think that the default for mvcgen should be to run simp on the subgoals it produces after all... The worry here is performance, of course, but something like mvcgen -simp should provide an escape hatch.
some
rcasesto nicely split the invariant
I think this is something that @Rish Vaishnav also requested in https://github.com/leanprover/lean4/issues/9364.
- I'm unsure how to do this reliably... taking the hyp names from the invariant syntax reeks a bit unhygienic to me, but maybe that worry is unfounded.
- (It is not always necessary to split Ors and doing so duplicates goals.)
- I would prefer if this issue had a solution that is orthogonal to
mvcgen, but I'm having trouble seeing how that could work. What makes loop invariants so special compared to e.g., induction hypotheses? Can we find a solution that handles both?
Patrick Massot (Jul 23 2025 at 12:39):
I think doing a full simp would be confusing in addition to being potentially slow. Actually in Markus’s example, doing dsimp only at * is already clearing the frightening part.
Patrick Massot (Jul 23 2025 at 12:42):
Probably dsimp only at * is a bit extreme, you could also use basic logic lemmas to get rid of stuff like True → … or turn `true = false ↔ …$ into $¬ …$ etc.
Sebastian Graf (Jul 23 2025 at 12:43):
Indeed. I did mean simp only; what lemmas to include is a good question. Rish argues that we might want to do mleave (the simp set in ) by default. (This set includes currently includes true_implies and and_imp.) I'm yet undecided.
Patrick Massot (Jul 23 2025 at 12:44):
About hygiene, the standard solution for all other tactics is to let users provide names. That would be someing like mvcgen [pairsSumToZero] with ⟨h₁, h₂, h₃⟩|⟨h₁, h₂, h₃⟩, except it needs a way to provide the invariant somewhere.
Patrick Massot (Jul 23 2025 at 12:45):
But don’t take my messages too seriously. I don’t know anything about program verification and I played only with Markus’s example. I mostly wanted to say I’m enthusiastic and I’d like to show this to people, but without frightening them.
Sebastian Graf (Jul 23 2025 at 12:47):
What makes matters more complicated is that the invariant is actually a subgoal produced by mvcgen and is specified by the user after using mvcgen. Also there might be many different invariants to specify.
Perhaps a specialized tactic provide_invariant inv => ... that works like case inv => by exact ... with a few extra goodies is the solution here. That tactic could investigate the remaining goals after instantiating inv and rcases accordingly. I think I like this idea.
Ruben Van de Velde (Jul 23 2025 at 12:58):
Fwiw, I had the same reaction, and I proved the first subgoal manually (with lots of simp?) and found it somewhat enlightening
Ruben Van de Velde (Jul 23 2025 at 12:59):
It did turn out that importing Mathlib.Tactic broke mvcgen
Sebastian Graf (Jul 23 2025 at 13:06):
Ruben Van de Velde said:
It did turn out that importing
Mathlib.Tacticbrokemvcgen
Yes, I already heard that from another user. MWE was
import Std.Tactic.Do
import Lean.Elab.Tactic.Do.VCGen -- comment this out to get the expected "failed to synthesize ..." error
example : True := by mvcgen
It's pretty strange. I must be doing something wrong because it doesn't happen for bv_decide. Will investigate, but unfortunately it doesn't reproduce in Lean's testsuite.
Robin Arnez (Jul 23 2025 at 14:14):
You redefine the syntax in Lean.Elab.Tactic.Do.VCGen:
syntax (name := mvcgen_step) "mvcgen_step" optConfig
(num)? (" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "]")? : tactic
syntax (name := mvcgen_no_trivial) "mvcgen_no_trivial" optConfig
(" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "]")? : tactic
syntax (name := mvcgen) "mvcgen" optConfig
(" [" withoutPosition((simpStar <|> simpErase <|> simpLemma),*,?) "]")? : tactic
The first two should probably be moved to Init.Tactics and in general all of them removed from Lean.Elab.Tactic.Do.VCGen.
Robin Arnez (Jul 23 2025 at 14:15):
Actually no they are already in Std.Tactic.Do.Syntax, just remove them from Lean.Elab.Tactic.Do.VCGen then.
Sebastian Graf (Jul 23 2025 at 14:15):
Huh, thanks for checking! That would explain why I can't reproduce it, I've already removed these vestiges in my dev branch.
Bhavik Mehta (Jul 23 2025 at 15:03):
Sebastian Graf said:
Here's a solution using
Std.Dofor theisPrimevariant without thed^2 > ntest.
For the sake of comparison, here's the solution for the simpler version in the old setup. The two important points to note are that it's about double the length of Sebastian's solution, but about a third of the length of my solution to the original
Bhavik Mehta (Jul 23 2025 at 17:55):
Is there a easy workaround for the mathlib import breaking mvcgen? I'd like to use both in my example
Sebastian Graf (Jul 24 2025 at 07:06):
Unfortunately I don't think there is. I pushed a fix (#9505 lean4#9505), but it appears you'll have to wait for the next RC to use it with Mathlib.
Bhavik Mehta (Jul 24 2025 at 11:58):
Bhavik Mehta (Jul 29 2025 at 16:02):
Patrick Massot said:
I mostly wanted to say I’m enthusiastic and I’d like to show this to people, but without frightening them.
Here's another basic example with some explanation, which might be useful for your purpose!
Kim Morrison (Jul 30 2025 at 03:51):
lean#9622 cleans up two rough edges you noticed here
Patrick Massot (Jul 31 2025 at 11:20):
Thinking again about this, I started fantasizing teaching my old intro to effective algebra class using this. And I remember the first thing I taught to explain the relevance of algorithm was:
import Std.Tactic.Do
import Std.Do.Triple.SpecLemmas
def naive_expo (x n : Nat) : Nat := Id.run do
let mut y := 1
for _ in [:n] do
y := y*x
return y
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 % 2 = 1 then
y := x * y
e := e - 1
else
x := x*x
e := e/2
if e = 0 then break
return y
Does anyone want want to take up the challenge of proving those function both compute x^n? Changing the implementation is ok as long as the algorithms are recognizable.
Kim Morrison (Jul 31 2025 at 11:53):
import Std.Tactic.Do
import Std.Do.Triple.SpecLemmas
open Std.Do
def naive_expo (x n : Nat) : Id Nat := do
let mut y := 1
for _ in [:n] do
y := y*x
return y
theorem naive_expo_correct (x n : Nat) : naive_expo x n = x^n := by
generalize h : naive_expo x n = r
apply Id.by_wp h
mvcgen
case inv => exact ⇓⟨r, xs⟩ => ⌜r = x^xs.pref.length⌝
case step ih =>
simp at ih ⊢
simp [Nat.pow_add_one, ih]
case a.pre1 =>
simp
case a.post.success h =>
simpa using h
Kim Morrison (Jul 31 2025 at 11:53):
(desperately cribbing from Bhavik's example above :-)
Kim Morrison (Jul 31 2025 at 12:05):
and I claim/hope that all the sorries in
theorem fast_expo_correct (x n : Nat) : fast_expo x n = x^n := by
generalize h : fast_expo x n = r
apply Id.by_wp h
mvcgen
case inv => exact ⇓⟨⟨e, x', y⟩, xs⟩ => ⌜y * x'^e = x^n ∧ e ≤ n - xs.pref.length⌝
case step.isTrue ih =>
simp at ih ⊢
mvcgen
case isTrue h' =>
simp
sorry
case isFalse h' =>
simp
sorry
case step.isFalse ih =>
simp at ih ⊢
mvcgen
case isTrue h' =>
simp
sorry
case isFalse h' =>
simp
sorry
case a.pre1 =>
simp
case a.post.success h =>
simp at h ⊢
sorry
are entirely conventional. :-)
Kim Morrison (Jul 31 2025 at 12:07):
(i.e. all I have done here is written down my guess as to the relevant loop invariant, and then used the not-yet-existing code action that takes us from the monadic verification world to the normal world)
Kim Morrison (Jul 31 2025 at 12:07):
The nested calls to mvcgen are perhaps the wrong way to drive this machine, @Sebastian Graf will know better.
Patrick Massot (Jul 31 2025 at 12:21):
Which version of Lean are you using here? Your code doesn’t seem to work in the playground.
Patrick Massot (Jul 31 2025 at 12:22):
And indeed it works in the playground if I force latest nightly.
Bhavik Mehta (Jul 31 2025 at 12:22):
Kim Morrison said:
The nested calls to
mvcgenare perhaps the wrong way to drive this machine, Sebastian Graf will know better.
I'm using split there instead of mvcgen, it looks to me like there's a missing [spec] for ite. But as you say, Sebastian will know better
Bhavik Mehta (Jul 31 2025 at 12:27):
here's a proof for both, where I edited the function slightly to change the position of the break
import Std.Tactic.Do
import Std.Do.Triple.SpecLemmas
def naive_expo (x n : Nat) : Nat := Id.run do
let mut y := 1
for _ in [:n] do
y := y*x
return y
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
theorem naive_expo_correct (x n : Nat) : naive_expo x n = x^n := by
generalize h : naive_expo x n = r
apply Id.by_wp h
mvcgen
case inv => exact ⇓⟨y, xs⟩ => ⌜y = x ^ xs.pref.length⌝
all_goals simp_all [Nat.pow_add_one]
theorem fast_expo_correct (x n : Nat) : fast_expo x n = x^n := by
generalize h : fast_expo x n = r
apply Id.by_wp h
mvcgen
case inv => exact ⇓⟨⟨e, x', y⟩, xs⟩ => ⌜x' ^ e * y = x ^ n ∧ e ≤ n - xs.pref.length⌝
all_goals simp_all
case isFalse x _ _ ih =>
obtain ⟨e, y, x'⟩ := b
split
case isTrue =>
simp_all
constructor
· rw [← Nat.mul_assoc, ← Nat.pow_add_one, ← ih.1]
have : e - 1 + 1 = e := by grind
rw [this]
· grind
case isFalse =>
simp_all
constructor
· rw [← Nat.pow_two, ← Nat.pow_mul]
grind
· grind
case a.post b ih =>
obtain ⟨e, y, x'⟩ := b
dsimp at ih ⊢
rw [← ih.1, ih.2, Nat.pow_zero, Nat.one_mul]
theorem same_func (x n : Nat) : fast_expo x n = naive_expo x n := by
rw [naive_expo_correct, fast_expo_correct]
Bhavik Mehta (Jul 31 2025 at 12:28):
I'm surprised by the block
have : e - 1 + 1 = e := by grind
rw [this]
I expected just grind to solve that goal...
Aaron Liu (Jul 31 2025 at 12:34):
I tried
import Std.Tactic.Do
import Std.Do.Triple.SpecLemmas
def fastExpo (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 % 2 = 1 then
y := x * y
e := e - 1
else
x := x*x
e := e/2
if e = 0 then break
return y
open Std.Do
@[simp]
theorem Id.pure_run {α : Type u} (x : Id α) : pure x.run = x := rfl
theorem fastExpo_spec (x n : Nat) : fastExpo x n = x ^ n := by
rw [← Id.run_pure (fastExpo x n)]
refine Id.by_wp (Eq.refl (Id.run (pure (fastExpo x n)))) (· = x ^ n) ?_
rw [fastExpo, Id.pure_run]
mvcgen
case inv =>
exact ⇓⟨⟨e, x', y⟩, xs⟩ => ⌜y * x'^e = x^n ∧ e + xs.pref.length ≤ n⌝
case ifTrue h₁ h₂=>
obtain ⟨e, x', y⟩ := b
match e with
| 0 => simp at h₁
| 1 => simpa [Nat.mul_comm] using h.left
case ifFalse h₁ h₂ =>
obtain ⟨e, x', y⟩ := b
match e with
| 0
| 1 => simp at h₂
| 2 => simp at h₁
| e + 1 + 2 =>
simp only [Nat.add_mod_right] at h₁
obtain he | he := e.mod_two_eq_zero_or_one.symm
· rw [Nat.add_mod, he] at h₁
simp at h₁
rw [← Nat.dvd_iff_mod_eq_zero] at he
obtain ⟨k, rfl⟩ : ∃ k, 2 * k = e := ⟨e / 2, Nat.mul_div_cancel' he⟩
simp only [List.length_reverse, SVal.curry_nil, Nat.add_one_sub_one, List.length_cons,
SPred.entails_nil, forall_const] at h ⊢
constructor
· rw [← h.left]
simp only [Nat.pow_succ]
ac_rfl
· refine le_of_eq_of_le ?_ h.right
ac_rfl
case ifTrue h₁ h₂ =>
obtain ⟨e, x', y⟩ := b
match e with
| 0 => simpa using h.left
| 1 => simp at h₁
| n + 2 => simp at h₂
case ifFalse h₁ h₂ =>
obtain ⟨e, x', y⟩ := b
simp only [List.length_reverse, SVal.curry_nil, List.length_cons, SPred.entails_nil,
forall_const] at h ⊢
constructor
· rw [← h.left]
rw [Nat.mod_two_not_eq_one, ← Nat.dvd_iff_mod_eq_zero] at h₁
rw [← Nat.pow_two, ← Nat.pow_mul, Nat.mul_div_cancel' h₁]
· have := h.right
simp only [Nat.div_eq_zero_iff, reduceCtorEq, false_or, Nat.not_lt] at h₂
omega
case pre1 => simp
case post.success r =>
obtain ⟨e, x', y⟩ := r
obtain rfl : e = 0 := by
simp only [List.length_reverse, List.length_range', Nat.sub_zero, Nat.add_one_sub_one,
Nat.div_one, SVal.curry_nil] at h
omega
simpa using h.left
Patrick Massot (Jul 31 2025 at 12:37):
Nice!
Sebastian Graf (Jul 31 2025 at 12:37):
Great work! Another nice example.
Strange. Generally, whenever you need to do mvcgen twice, there's a bug in mvcgen. In the case above, the internal use of the split tactic fails. I added this test case to my kitchen sink test file on my dev branch and there it appears I've already fixed the issue. On my dev branch I tell the split tactic which if to split rather than letting it figure out from the shape of the goal (where it first tries to split on the if in a stateful hypothesis ...)
Patrick Massot (Jul 31 2025 at 12:37):
Using xs.pref.length seems a bit artificial. Isn’t there simply a way to access the current value of the loop variable?
Bhavik Mehta (Jul 31 2025 at 12:55):
I think "current value" is a bit tricky, since it needs to make sense both before the loop starts and after it ends: xs.pref.length takes n+1 values, but the loop variable only takes n values
Patrick Massot (Jul 31 2025 at 13:34):
I’m not sure I understand this explanation, but the important point is that Sebastian can take notes that could be useful when all this will be documented.
Shreyas Srinivas (Jul 31 2025 at 13:53):
can these two lines by automated?
generalize h : <definition = generalized result variable>
apply Id.by_wp h
mvcgen
Sebastian Graf (Jul 31 2025 at 14:04):
Good question. The generalize is telling the system "focus on this subexpression, it represents the computation". That is similar to conv at focusOnThisSubexpression. Alas, the by_wp theorems are very specific to the particular monad. But maybe we can somehow maintain a database of by_wp theorems per monad and shorten it to
by_wp at fast_expo x n
mvcgen
where by_wp is a new tactic. Is that one saved line worth explaining what by_wp does? Hard to tell. Note that you will only need to do this once per functional correctness property. Larger proofs are likely specifying properties in terms of Hoare triples rather than through pure Props.
Shreyas Srinivas (Jul 31 2025 at 14:06):
Thanks to @Bhavik Mehta's examples I am beginning to learn how this framework functions.
Shreyas Srinivas (Jul 31 2025 at 14:06):
I have another feedback
Shreyas Srinivas (Jul 31 2025 at 14:06):
For some reason the mvcgen tactic decides to output stuff with no notation
Shreyas Srinivas (Jul 31 2025 at 14:07):
From discussions in iris, I know that the infoview doesn't support complex formatting, but I would have hoped it could at least use the correct notation
Bhavik Mehta (Jul 31 2025 at 14:07):
Which notation are you hoping to see? Something which resembles the imperative syntax? For me, the tactic does display notation, so presumably the notation you want is different
Shreyas Srinivas (Jul 31 2025 at 14:07):
Yeah, but I also think it would be nice to have the verification conditions appear in a different colour than the code in between
Bhavik Mehta (Jul 31 2025 at 14:08):
I think that's not specific to the tactic, that's either a notation printing change or an infoview change
Shreyas Srinivas (Jul 31 2025 at 14:09):
Yes but I know this isn't supported by the infoview. We have similar problems in iris-lean
Bhavik Mehta (Jul 31 2025 at 14:09):
But I agree that the goal state after running the tactic could be quite a lot more readable
Shreyas Srinivas (Jul 31 2025 at 14:10):
It might be better to change the way the tactic behaves by having one tactic show the goal in {{Pre-condition}} def {{Post Condition}} format with suitable indentation and syntax highlighting, and then do the case split in a separate tactic
Shreyas Srinivas (Jul 31 2025 at 14:10):
Currently I get these four cases, out of which 3 have metavariables (in Bhavik's second example)
Sebastian Graf (Jul 31 2025 at 14:10):
To be fair, all examples so far used Id. If you use StateM you'll begin to see actual stateful hypothesis to the left of |-s. Often these aren't terribly useful and all tactics try to frame as many pure facts as possible into the regular, local Lean context. In Id, every hypothesis can and will be framed into the local context.
Shreyas Srinivas (Jul 31 2025 at 14:12):
For what it is worth, I think the infoview needs to support some custom formatting. In Iris we have this weird situation where the Iris hyps and goals are essentially just rendered as one big blob with no highlighting
Shreyas Srinivas (Jul 31 2025 at 14:12):
We'll run into this same situation in CSLib when people start formulating different logical theories
Shreyas Srinivas (Jul 31 2025 at 14:13):
CC : @Marc Huisinga
EDIT : The other thread : for reference
Sebastian Graf (Jul 31 2025 at 14:15):
In Iris we have this weird situation where the Iris hyps and goals are essentially just rendered as one big blob with no highlighting
Yeah, the pretty-printing in Std.Do is basically a copy of that. See the examples in https://github.com/leanprover/lean4/blob/master/tests/lean/run/spredProofMode.lean. Why is missing highlighting problematic? Do you have a concrete complaint?
show the goal in
{{Pre-condition}} def {{Post Condition}}format
This won't support multiple named stateful hypotheses, hence the chosen formatting of the proof mode
Shreyas Srinivas (Jul 31 2025 at 14:19):
Sebastian Graf said:
In Iris we have this weird situation where the Iris hyps and goals are essentially just rendered as one big blob with no highlighting
Yeah, the pretty-printing in
Std.Dois basically a copy of that. See the examples in https://github.com/leanprover/lean4/blob/master/tests/lean/run/spredProofMode.lean. Why is missing highlighting problematic? Do you have a concrete complaint?
Readability and mapping it to more traditional notation as a beginner.
Shreyas Srinivas (Jul 31 2025 at 14:20):
For example:
PostCond (Nat × Std.List.Zipper { stop := n, step_pos := Nat.zero_lt_one }.toList) PostShape.pure
this would be easier to read with notation
Shreyas Srinivas (Jul 31 2025 at 14:21):
Screenshot from 2025-07-31 16-21-38.png
This would be more readable with indentation
Shreyas Srinivas (Jul 31 2025 at 14:22):
to be fair some of this is just complaining about how infoview renders anything in general
Shreyas Srinivas (Jul 31 2025 at 14:22):
but they stand out here because of the size of the goals
Sebastian Graf (Jul 31 2025 at 14:23):
Readability and mapping it to more traditional notation as a beginner.
Is there a traditional notation you have in mind that supports giving names to multiple stateful hypotheses? Perhaps similar to {{P}} x {{Q}} as you suggest, but that would look weird with one line per stateful hyp in P.
Shreyas Srinivas said:
For example:
PostCond (Nat × Std.List.Zipper { stop := n, step_pos := Nat.zero_lt_one }.toList) PostShape.purethis would be easier to read with notation
What kind of notation do you have in mind?
Shreyas Srinivas (Jul 31 2025 at 14:26):
Sebastian Graf said:
Readability and mapping it to more traditional notation as a beginner.
Is there a traditional notation you have in mind that supports giving names to multiple stateful hypotheses? Perhaps similar to
{{P}} x {{Q}}as you suggest, but that would look weird with one line per stateful hyp inP.
- One line per stateful hypothesis. Iris already does something similar.
- Colours for hypothesis names, maybe even different from regular hypotheses to keep the two separate visually (which we also miss in Iris, a bigger rendering of the entails symbol). This requires better ability to control how infoview is rendered as Mario mentioned in the other thread
- Indented rendering of the structure type itself, but this is a lean-wide issue.
- EDIT : PostCond doesn't need to appear as a word. Some bracketing notation would look better.
Shreyas Srinivas said:
For example:
PostCond (Nat × Std.List.Zipper { stop := n, step_pos := Nat.zero_lt_one }.toList) PostShape.purethis would be easier to read with notation
What kind of notation do you have in mind?
Basically along the general ideas I described above.
Shreyas Srinivas (Jul 31 2025 at 14:34):
Also, the effects in the PostShape could be rendered separately
Shreyas Srinivas (Jul 31 2025 at 14:35):
I understand that this is purely determined by the monad, so there is no need to include the PostShape.pure rendering inside the goal
Shreyas Srinivas (Jul 31 2025 at 14:35):
It's extraneous in some sense. It's part of and determined by the (monadic) "context" rather than the goal.
Sebastian Graf (Aug 10 2025 at 11:29):
In the reference manual, I want to demonstrate that proving stuff about for loops with early return is somewhat frustrating without mvcgen. I came up with the following example:
import Std
def nodup (l : List Int) : Bool := Id.run do
let mut seen : Std.HashSet Int := ∅
for x in l do
if x ∈ seen then
return false
seen := seen.insert x
return true
@[simp, grind]
theorem Std.HashSet.Nodup_toList [Hashable α] [BEq α] [LawfulHashable α] [EquivBEq α] [LawfulBEq α] (m : Std.HashSet α) :
m.toList.Nodup := by
simpa using Std.HashMap.distinct_keys (m := m.inner)
@[simp, grind]
theorem Std.HashSet.Nodup_append_toList_insert [Hashable α] [BEq α] [LawfulHashable α] [EquivBEq α] [LawfulBEq α] (m : Std.HashSet α) (x : α) (h : x ∉ m) :
((m.insert x).toList ++ tl).Nodup ↔ (m.toList ++ x :: tl).Nodup := by
grind
theorem nodup_correct_directly (l : List Int) : nodup l ↔ l.Nodup := by
simp only [nodup, bind_pure_comp, map_pure, Id.run_bind]
suffices h :
∀ seen,
match MProd.fst (Id.run (forIn (β := MProd (Option Bool) (Std.HashSet Int)) l ⟨none, seen⟩ (fun x ⟨_, seen⟩ =>
if x ∈ seen then pure (ForInStep.done (α := MProd (Option Bool) (Std.HashSet Int)) ⟨some false, seen⟩)
else pure (ForInStep.yield (α := MProd (Option Bool) (Std.HashSet Int)) ⟨none, seen.insert x⟩)))) with
| some true => False
| some false => ¬(seen.toList ++ l).Nodup
| none => (seen.toList ++ l).Nodup by
replace h := h ∅
split
· simp only [Id.run_pure]; grind
· cases ‹Bool› <;> simp_all only [Id.run_pure]; grind
induction l
case nil => simp
case cons hd tl ih =>
intro seen
replace ih := ih (seen.insert hd)
simp only [List.forIn_cons]
if hif : hd ∈ seen
then simp_all only [↓reduceIte, pure_bind, Id.run_pure]; grind
else split <;> simp_all
Which is indeed quite painful. But perhaps someone can make this proof more succinct so that I don't oversell mvcgen? I tried to golf a bit, but the inability of grind to prove
example : Id.run (pure true) = true := by
grind [Id.run_pure]
is getting in the way. (Have to reach out and see whether this is a bug.)
Alfredo Moreira-Rosa (Aug 10 2025 at 15:20):
and whith mvcgen ?
Sebastian Graf (Aug 10 2025 at 15:22):
On my branch (soon master) the proof is
theorem nodup_correct (l : List Int) : nodup l ↔ l.Nodup := by
generalize h : nodup l = r
apply Id.by_wp h
mvcgen
case inv =>
exact Invariant.withEarlyReturn
(onReturn := fun ret seen => ⌜ret = false ∧ ¬l.Nodup⌝)
(onContinue := fun xs seen => ⌜(∀ x, x ∈ seen ↔ x ∈ xs.pref) ∧ xs.pref.Nodup⌝)
all_goals mleave; grind
(Without the additional simp and grind lemmas.)
Bhavik Mehta (Aug 12 2025 at 01:58):
here's what I got:
theorem nodup_correct_directly (l : List Int) : nodup l ↔ l.Nodup := by
rw [nodup]
generalize hl' : (∅ : Std.HashSet Int) = l'
convert_to _ ↔ l.Nodup ∧ ∀ x ∈ l, x ∉ l'
· simp [← hl']
clear hl'
simp only [bind_pure_comp, map_pure, Id.run_bind]
induction l generalizing l'
case nil => simp_all
case cons x _ _ => by_cases x ∈ l' <;> aesop (add simp [forall_and])
Bhavik Mehta (Aug 12 2025 at 01:59):
It's relatively short, but has the cost of importing tactics from mathlib
Bhavik Mehta (Aug 12 2025 at 02:05):
while this example doesn't make it clear, I believe the case where mvcgen really shines over the older approaches is with nested loops or more subtle control flows
Kim Morrison (Aug 12 2025 at 02:08):
Sebastian Graf said:
On my branch (soon master) the proof is
theorem nodup_correct (l : List Int) : nodup l ↔ l.Nodup := by generalize h : nodup l = r apply Id.by_wp h mvcgen case inv => exact Invariant.withEarlyReturn (onReturn := fun ret seen => ⌜ret = false ∧ ¬l.Nodup⌝) (onContinue := fun xs seen => ⌜(∀ x, x ∈ seen ↔ x ∈ xs.pref) ∧ xs.pref.Nodup⌝) all_goals mleave; grind(Without the additional
simpandgrindlemmas.)
It would be lovely if there was some way for a tactic for postpone itself, and say "leave goal 1 for the user. goal 2 will depend on via a metavariable; as soon as this metavariable is assign, resume with the following tactics".
This would allow omitting all_goals mleave; grind, perhaps.
Kim Morrison (Aug 12 2025 at 02:09):
I wonder if
mvcgen
case inv =>
exact Invariant.withEarlyReturn
could be turned into a single step, i.e. specifying the invariant as part of the mvcgen call, so it could take care of cleanup steps itself.
Sebastian Graf (Aug 12 2025 at 08:19):
Sebastian Graf said:
In the reference manual, I want to demonstrate that proving stuff about
forloops with early return is somewhat frustrating withoutmvcgen.
Latest master has a fix for the grind bug and thanks to Bhavik's template I could shorten the proof to
theorem nodup_correct_directly (l : List Int) : nodup l ↔ l.Nodup := by
rw [nodup]
generalize hseen : (∅ : Std.HashSet Int) = seen
change ?lhs ↔ l.Nodup
suffices h : ?lhs ↔ l.Nodup ∧ ∀ x ∈ l, x ∉ seen by grind
clear hseen
induction l generalizing seen with grind [Id.run_pure, Id.run_bind]
with only Core tactics. I think this is as short as it gets. Observations:
- We are relying on
grindto do the case splits on control flow (probably fine). - We are relying on
grindbeing able to reason about monadic programs using lemmasId.run_pureandId.run_bind.Id.run_seqLeftfor example is not an e-matchable lemma, andI imagine that it won't be able to reason about(Needs testing. TheStateMeither.StateT.run_*lemmas are certainly sufficiently first-order.) - The previous two points are important, because as soon as
grindfails to reason about a single construct in the chain (likeId.run (pure ...)in the previous post), we have to fall back to manual splitting and taking apart the control flow. So it's a separation of concerns argument:mvcgenfocuses just on breaking down control flow and monadic computations, so that the remaining pure VCs can be reliably discharged bygrindwithout knowing aboutId.run_pure, etc. - We are relying on
change C[?lhs]to match out sub-program?lhsin a contextC. This could be tedious and brittle whenCis large, although I'm not sure whether that would be a problem in practice.
Sebastian Graf (Aug 12 2025 at 08:30):
Kim Morrison said:
It would be lovely if there was some way for a tactic for postpone itself, and say "leave goal 1 for the user. goal 2 will depend on via a metavariable; as soon as this metavariable is assign, resume with the following tactics".
So mvcgen currently generates roughly two kinds of goals: (1) inv<n> invariants (of type Std.Do.Invariant), which I think corresponds to your "goal 1", and (2) vc<n> goals which is everything else. I imagine that at some point (probably next Q) I'll add syntax like
mvcgen [...] using
invariants 1 => Invariant.withEarlyReturn
(onReturn := fun ret seen => ⌜ret = false ∧ ¬l.Nodup⌝)
(onContinue := fun xs seen => ⌜(∀ x, x ∈ seen ↔ x ∈ xs.pref) ∧ xs.pref.Nodup⌝)
with grind
which expands to something like
mvcgen -leave [...]
case inv1 1 => by mleave; exact Invariant.withEarlyReturn
(onReturn := fun ret seen => ⌜ret = false ∧ ¬l.Nodup⌝)
(onContinue := fun xs seen => ⌜(∀ x, x ∈ seen ↔ x ∈ xs.pref) ∧ xs.pref.Nodup⌝)
all_goals mleave; grind
In particular, note that this instantiates the invariant before doing mleave (which is simp only [...] at *, done automatically on every subgoal as part of mvcgen +leave), so that grind is immediately applicable. Not sure if its worth saving the keystrokes for with grind.
specifying the invariant as part of the mvcgen call
Yes, exactly :)
Sebastian Graf (Aug 12 2025 at 17:35):
Here's where I'm drafting a reference manual entry/tutorial: https://hackmd.io/@sg-fro/BJRlurP_xg
Sebastian Graf (Aug 14 2025 at 16:46):
The linked tutorial should be "complete enough" now. As always, I'm open for feedback! Though I'll be away for a month now, so I can't really incorporate feedback until after that.
Sebastian Graf (Aug 15 2025 at 11:48):
lean4#9927 implements the syntax I was alluding to earlier. This syntax will not yet make it into 4.23. Full syntax example:
mvcgen
using invariants
| 1 => Invariant.withEarlyReturn
(onReturn := fun ret seen => ⌜ret = false ∧ ¬l.Nodup⌝)
(onContinue := fun traversalState seen =>
⌜(∀ x, x ∈ seen ↔ x ∈ traversalState.prefix) ∧ traversalState.prefix.Nodup⌝)
with mleave -- mleave is a no-op here, but we are just testing the grammar
| vc1 => grind
| vc2 => grind
| vc3 => grind
| vc4 => grind
| vc5 => grind
or in short
mvcgen
using invariants
| 1 => Invariant.withEarlyReturn
(onReturn := fun ret seen => ⌜ret = false ∧ ¬l.Nodup⌝)
(onContinue := fun traversalState seen =>
⌜(∀ x, x ∈ seen ↔ x ∈ traversalState.prefix) ∧ traversalState.prefix.Nodup⌝)
with grind
More examples in the test file.
I'm very much open to bike-shedding suggestions!
- Note that the
using invariants | <n> =>syntax might feel a little... "unleanish". Maybe somebody has a better idea. - The
with $tac | $lhs => $rhshas the same semantics asall_goals (try $tac); case $lhs => $rhs, so I think that's far more "leanish".
Ruben Van de Velde (Aug 15 2025 at 12:26):
I like it :)
Ruben Van de Velde (Aug 15 2025 at 12:27):
Can the using invariants bit go on the same line as mvcgen as well?
Sebastian Graf (Aug 15 2025 at 17:50):
Yes!
Asei Inoue (Sep 14 2025 at 02:29):
@Markus Himmel
Thank you for writing nice article! but your code does not work!
Asei Inoue (Sep 14 2025 at 03:58):
@Sebastian Graf
markdown layout looks broken (code block does not work)
Sebastian Graf (Sep 15 2025 at 07:45):
Asei Inoue said:
Thank you for writing nice article! but your code does not work!
That's the unfortunate consequence of working with pre-releases :)
I enshrined Markus' code as a test case here: https://github.com/leanprover/lean4/blob/master/tests/lean/run/pairsSumToZero.lean
Asei Inoue said:
markdown layout looks broken (code block does not work)
Thanks, fixed!
Frederick Pu (Sep 15 2025 at 23:04):
i think user should be able to annotate invariants and hints in a dafney/verus style way
import Mathlib
import Std
set_option pp.letVarTypes true
/-
simple tail assertion `have : Even y`
-/
def double_proof (x : Nat) : Nat := Id.run <| do
let y := 2 * x;
have : Even y := by
use x
ring
return y
def double (x : Nat) : Nat := Id.run <| do
let y := 2 * x;
return y
theorem even_double : ∀ x, Even (double x) := by
intro x
have : double = double_proof := rfl
rw [this]
unfold double_proof
extract_lets y this
(expose_names; exact this)
example : ∀ n : ℤ, 2 ∣ n * (n + 1) := by
exact fun n => Int.two_dvd_mul_add_one n
/-
loop invariant example
-/
def sumN_proof (n : Nat) : Nat := Id.run <| do
let mut out := 0
have : (out : Nat) = ((0:ℕ) - 1 : ℤ) * 0 / 2 := rfl
for i in List.range n do
let prev := out
out := out + i
let new := out
have : prev = (i-1 : ℤ) * i / 2 → new = (i + 1 - 1 : ℤ) * ((i + 1)) / 2 := by
intro h
have : new = prev + i := by tauto
zify at this
rw [this, h]
qify
rw [Int.cast_div, Int.cast_div]
field_simp
push_cast
ring
convert Int.two_dvd_mul_add_one (↑i + 1 - 1 : ℤ)
ring
norm_num
convert Int.two_dvd_mul_add_one (↑i - 1 : ℤ)
ring
norm_num
return out
def sumN (n : Nat) : Nat := Id.run <| do
let mut out := 0
for i in List.range n do
out := out + i
return out
theorem sumN_spec (n : Nat) : ⦃⌜True⌝⦄ sumN n ⦃⇓r => r = n * (n + 1) / 2⦄ := by
have : sumN = sumN_proof := by rfl
intro n
rw [this]
unfold sumN_proof
/-
this : sumN = sumN_proof
n : ℕ
⊢ (let out : ℕ := 0;
have this : ↑out = (↑0 - 1) * 0 / 2 := sumN_proof._proof_1;
do
let r ←
forIn (List.range n) out fun i r =>
let out : ℕ := r;
let prev : ℕ := out;
let out : ℕ := out + i;
let new : ℕ := out;
have this : ↑prev = (↑i - 1) * ↑i / 2 → ↑new = (↑i + 1 - 1) * (↑i + 1) / 2 := ⋯;
do
pure PUnit.unit
pure (ForInStep.yield out)
have out : ℕ := r
pure out).run =
n * (n + 1) / 2
-/
and have proof automation such as extract_lets unify to the prove the goal. a lot of people already are doing this with a subtype style pattern. I think this approach + the mvcgen could allow this trick to generalize to for loops too
Frederick Pu (Sep 15 2025 at 23:45):
how would i construct the loop invariant?
/-
loop invariant example
-/
def sumN_proof (n : Nat) : Id Nat := do
let mut out := 0
have : (out : Nat) = ((0:ℕ) - 1 : ℤ) * 0 / 2 := rfl
for i in List.range n do
let prev := out
out := out + i
let new := out
have : prev = (i-1 : ℤ) * i / 2 → new = (i + 1 - 1 : ℤ) * ((i + 1)) / 2 := by
intro h
have : new = prev + i := by tauto
zify at this
rw [this, h]
qify
rw [Int.cast_div, Int.cast_div]
field_simp
push_cast
ring
convert Int.two_dvd_mul_add_one (↑i + 1 - 1 : ℤ)
ring
norm_num
convert Int.two_dvd_mul_add_one (↑i - 1 : ℤ)
ring
norm_num
return out
def sumN (n : Nat) : Id Nat := do
let mut out := 0
for i in List.range n do
out := out + i
return out
open Std Do
#check Invariant.withEarlyReturn
#check PostCond
theorem sumN_spec (n : Nat) :
sumN n = n * (n + 1) / 2:= by
/-
unexpected token '⌜'; expected '_' or identifier
-/
have : sumN = sumN_proof := by rfl
rw [this]
unfold sumN_proof
generalize h : sumN n = r
apply Id.of_wp_run_eq h
mvcgen
/-
n : ℕ
this : sumN = sumN_proof
r : Id ℕ
h : sumN n = r
⊢ Invariant (List.range n) ℕ PostShape.pure
-/
Asei Inoue (Sep 17 2025 at 03:56):
@Sebastian Graf
When is the official release of Std.Do?
Sebastian Graf (Sep 19 2025 at 14:27):
Frederick Pu said:
i think user should be able to annotate invariants and hints in a dafney/verus style way
Interesting. Why maintain the erased variant double besides the verified variant double_proof? Why not just double_proof?
Incidentally, in my prototype library mpl I implemented an extension to def elaboration that implements Dafny/Verus-style intrinsic verification syntax, exploiting definitional equivalence between an erased and refined definition of sumN in much the same way. Here's an example of that: https://github.com/sgraf812/mpl/blob/1dbf6bc75ff5398f59b77ae320f1f0a0781b721e/MPL/Experimental/Do.lean#L426-L437
I haven't implemented that syntax in Lean yet because we wanted to rewrite do elaboration first. But something like that is on our agenda.
Frederick Pu said:
how would i construct the loop invariant?
Here you go. In the next RC, you will be able to write mvcgen invariant? to get a suggestion to write mvcgen invariant\n · ⇓⟨xs, out⟩ => _.
Below you still need to prove the non-linear inductive step though:
import Std
def sumN (n : Nat) : Id Nat := do
let mut out := 0
for i in List.range n do
out := out + i
return out
open Std Do
#check Invariant.withEarlyReturn
#check PostCond
theorem sumN_spec (n : Nat) :
sumN n = n * (n + 1) / 2:= by
generalize h : sumN n = r
apply Id.of_wp_run_eq h
rw [List.range_eq_range']
mvcgen invariants
· ⇓⟨xs, out⟩ => ⌜out = xs.prefix.length * (xs.prefix.length + 1) / 2⌝
with grind -- TODO
Sebastian Graf (Sep 19 2025 at 14:29):
Asei Inoue said:
Sebastian Graf
When is the official release of Std.Do?
If all goes well, then the next RC will properly release Std.Do. It's my first day after a month of parental leave, though, and I need to ascertain that there are no remaining big TODOs.
Frederick Pu (Sep 26 2025 at 06:13):
ideally the invariant keyword could bind the for loop variables directly. something like
macro_rules
| `(doElem| assert $xs* => $p) => `(doElem| assertGadget (fun $xs* => $p))
| `(doElem| assert => $p) => `(doElem| assertGadget $p)
| `(doElem| invariant $xs* => $p
for $decls:doForDecl,* do $body) => `(doElem|
for $decls:doForDecl,* do $body)
but i think that would require the ability to create expr mdata during the do elaboration process. Idk if that's alerady a feature or not. But having a mdata:doElem would be really handy for extending do notation without having to interact with the core do elaboration procedure
Frederick Pu (Sep 26 2025 at 17:08):
something like annotate (e : term) (elem : doElem) : doElem so that it behaves exactly like doElem except whatever doElem becomes in the final expr gets wrapped with the meta data value of e (using a function def annotateDo {elemType : Type*} {alpha : elemType -> Type*} (elem : elemType) (e : alpha elem) := elem) where where elemType is the type of term that (elem: doElem becomes).
Frederick Pu (Sep 26 2025 at 17:56):
then you could have something like
syntax "invariant " term "by " term : doElem
macrorules
| `(invariant $inv by $proof
for $forDecl do $forBody) =>
`(annotate $inv (annotateDo $forBody $proof))
for $forDecl do annotate $proof $forBody)
obviously this still won't work because the $forBody will not be correct elaborated form of that do block but if there were a way efficiently getting that elaboration result this would make implement refinement style tings much nicer and allow for usable unification hints for invariants
Asei Inoue (Oct 17 2025 at 15:25):
Hi. Thank you for writing nice document: https://hackmd.io/@sg-fro/BJRlurP_xg
I have read this!
I want to write some proof by myself but I don't go further anymore.
How to fill this sorry?
import Std.Tactic.Do
import Lean
set_option mvcgen.warning false
open Std.Do
def peasantMul (x y : Nat) : Nat := Id.run do
let mut cur_x := x
let mut cur_y := y
let mut prod := 0
for _ in [0: x] do
if cur_x = 0 then
break
if cur_x % 2 = 1 then
prod := prod + cur_y
cur_x := cur_x / 2
cur_y := cur_y * 2
return prod
#guard peasantMul 3 4 = 12
#guard peasantMul 6 7 = 42
#guard peasantMul 12 100 = 1200
example (x y : Nat) : peasantMul x y = x * y := by
generalize h : peasantMul x y = s
apply Id.of_wp_run_eq h
mvcgen
case inv1 => exact ⇓⟨_, ⟨cur_x, cur_y, prod⟩⟩ => ⌜cur_x * cur_y + prod = x * y⌝
all_goals mleave
all_goals sorry
Asei Inoue (Oct 18 2025 at 10:48):
uhhh... :(
import Std.Tactic.Do
import Lean
set_option mvcgen.warning false
open Std.Do
def peasantMul (x y : Nat) : Nat := Id.run do
let mut cur_x := x
let mut cur_y := y
let mut prod := 0
for _ in [0: x] do
if cur_x = 0 then
break
if cur_x % 2 = 1 then
prod := prod + cur_y
cur_x := cur_x / 2
cur_y := cur_y * 2
return prod
#guard peasantMul 3 4 = 12
#guard peasantMul 6 7 = 42
#guard peasantMul 12 100 = 1200
example (x y : Nat) : peasantMul x y = x * y := by
generalize h : peasantMul x y = s
apply Id.of_wp_run_eq h
mvcgen +leave
case inv1 =>
exact Invariant.withEarlyReturn
(onReturn := fun ret cur_x cur_y prod => ⌜ret = prod ∧ cur_x = 0⌝)
/- Application type mismatch: The argument
fun xs cur_x cur_y prod => ⌜cur_x * cur_y + prod = x * y⌝
has type
(xs : List.Cursor ?m.599) → (cur_x cur_y prod : Nat) → SPred (?m.620 xs cur_x cur_y prod)
but is expected to have type
List.Cursor ?m.599 → Nat → Assertion ?m.597
in the application
@Invariant.withEarlyReturn ?m.597 ?m.598 ?m.599 Nat ?m.601 fun xs cur_x cur_y prod => ⌜cur_x * cur_y + prod = x * y⌝ -/
(onContinue := fun xs cur_x cur_y prod => ⌜cur_x * cur_y + prod = x * y⌝)
all_goals grind
pandaman (Oct 18 2025 at 14:41):
The issue is that the invariant doesn't have information about how many times the loop has run, so it's not strong enough to conclude the postcondition after the loop is completed. In this case, we can track the current value of cur_x in the invariant:
Solution (without early termination)
Sebastian Graf (Oct 20 2025 at 07:18):
@pandaman is spot on. Invariant.withEarlyReturn does not apply to your use case because there is no early return in the loop. Also, you need to keep fun ret (cur_x, cur_y, prod) => ... tupled up in the parameter list. In the next release, there will be an alias abbrev cursor.pos := cursor.prefix.length.
Asei Inoue (Oct 20 2025 at 07:19):
Thank you. I understand early break is not early return!
Sebastian Graf (Oct 20 2025 at 07:22):
Indeed; break is handled by simply returning ForInStep.done; return x additionally adds a new optional phantom let mut variable for x. Details in the docstring for Invariant.withEarlyReturn.
Asei Inoue (Nov 08 2025 at 16:00):
@Sebastian Graf
I succeeded in proving the specification in the case where an early return is used!
Thank you.
However, I feel that this function should really use break instead of return.
If I want to use break, how should I modify the proof?
import Lean
set_option mvcgen.warning false
open Std.Do
def peasantMul (x y : Nat) : Nat := Id.run do
let mut curX := x
let mut curY := y
let mut prod := 0
for _ in [0:x] do
if curX = 0 then
return prod
if curX % 2 = 1 then
prod := prod + curY
curX := curX / 2
curY := curY * 2
return prod
attribute [grind =] Nat.div_div_eq_div_mul
@[grind ->]
theorem div2_mul2_odd (x : Nat) (odd : x % 2 = 1) : x = (x / 2) * 2 + 1 := by
grind
@[grind ->]
theorem div2_mul2_even (x : Nat) (even : x % 2 = 0) : x = (x / 2) * 2 := by
grind
@[grind =, simp]
theorem div_pow_self_two_eq_zero (n : Nat) : n / 2 ^ n = 0 := by
have : n < 2 ^ n := Nat.lt_two_pow_self
simp_all
example (x y : Nat) : peasantMul x y = x * y := by
generalize h : peasantMul x y = s
apply Id.of_wp_run_eq h
mvcgen invariants
· Invariant.withEarlyReturn
(onContinue := fun cursor ⟨curX, curY, prod⟩ =>
⌜curX = x / 2 ^ cursor.prefix.length ∧ curX * curY + prod = x * y⌝)
(onReturn := fun ret ⟨curX, curY, prod⟩ => ⌜ret = prod ∧ prod = x * y⌝)
with (simp_all <;> grind)
Sebastian Graf (Nov 10 2025 at 11:09):
Here you go. I think there's a nonconfluence problem between the simp and grind lemmas that I didn't try to resolve:
import Lean
set_option mvcgen.warning false
open Std.Do
def peasantMul (x y : Nat) : Nat := Id.run do
let mut curX := x
let mut curY := y
let mut prod := 0
for _ in [0:x] do
if curX = 0 then
break
if curX % 2 = 1 then
prod := prod + curY
curX := curX / 2
curY := curY * 2
return prod
attribute [grind =] Nat.div_div_eq_div_mul
@[grind ->]
theorem div2_mul2_odd (x : Nat) (odd : x % 2 = 1) : x = (x / 2) * 2 + 1 := by
grind
@[grind ->]
theorem div2_mul2_even (x : Nat) (even : x % 2 = 0) : x = (x / 2) * 2 := by
grind
@[grind =, simp]
theorem div_pow_self_two_eq_zero (n : Nat) : n / 2 ^ n = 0 := by
have : n < 2 ^ n := Nat.lt_two_pow_self
simp_all
example (x y : Nat) : peasantMul x y = x * y := by
generalize h : peasantMul x y = s
apply Id.of_wp_run_eq h
mvcgen invariants
· ⇓⟨cursor, curX, curY, prod⟩ =>
⌜curX = x / 2 ^ cursor.pos ∧ curX * curY + prod = x * y⌝
with (simp_all <;> grind)
grind
Asei Inoue (Nov 10 2025 at 11:20):
@Sebastian Graf Thank you!
Asei Inoue (Nov 10 2025 at 14:53):
I am thinking about how loop invariants should be formulated when a for loop can terminate via break.
It seems that we have to consider an invariant that holds both when the loop finishes normally and when it exits by a break.
I wonder if, in cases where the same logic could be written with a return, it is often easier to prove things using return instead.
In the following code example, I couldn’t find an invariant that holds “both when the loop ends by break and when it ends normally,” and thus the proof doesn’t go through.
import Lean
open Std.Do
def sumWhilePositive (xs : List Int) : Int := Id.run do
let mut out := 0
for x in xs do
if x ≤ 0 then
break
out := out + x
return out
set_option mvcgen.warning false
@[grind =, simp]
theorem takeWhile_simplify_false {α : Type} (P : α → Bool) (pref suff : List α) (cur : α) (h : P cur = false) :
List.takeWhile P (pref ++ cur :: suff) = List.takeWhile P pref := by
fun_induction List.takeWhile with simp_all
theorem sumWhilePositive_spec (xs : List Int) :
sumWhilePositive xs = (xs.takeWhile (· > 0)).sum := by
generalize h : sumWhilePositive xs = s
apply Id.of_wp_run_eq h
mvcgen invariants
· ⇓⟨cursor, out⟩ => ⌜cursor.prefix.all (· > 0) ∧ (cursor.prefix.takeWhile (· > 0)).sum = out⌝
with grind
case vc1 =>
expose_names
simp_all
-- impossible to prove (invariant is incorrect)
sorry
case vc2 =>
expose_names
sorry
Sebastian Graf (Nov 10 2025 at 15:35):
In general, if your loop looks like
let mut out := ...
for x in xs do
... return out ...
return out
it is equivalent to
let mut out := ...
for x in xs do
... break ...
return out
and you should be able to prove all the same lemmas about it using mvcgen. What does your proof look like if you use return? It should be easy to adapt it to work with break. Certainly, cursor.prefix.all (· > 0) won't hold for the early return case either, so I don't see why you would include it in your invariant.
Asei Inoue (Nov 11 2025 at 11:37):
@Sebastian Graf
and you should be able to prove all the same lemmas about it using
mvcgen. What does your proof look like if you usereturn?
Thank you. So, are you saying that we can use the same invariant for both the proof with return and the proof with break? However, as the following code shows, I thought that in general we need to modify the invariant.
Am I missing something?
import Lean
open Std.Do
def takeWhileReturn {α : Type} (p : α → Bool) (l : List α) : List α := Id.run do
let mut result := []
for x in l do
if ! p x then
return result
result := result ++ [x]
return result
set_option mvcgen.warning false
@[grind =, simp]
theorem takeWhile_simplify_false {α : Type} (P : α → Bool) (pref suff : List α) (cur : α) (h : P cur = false) :
List.takeWhile P (pref ++ cur :: suff) = List.takeWhile P pref := by
fun_induction List.takeWhile with simp_all
@[grind =, simp]
theorem takeWhile_all {α : Type} (P : α → Bool) (l : List α) (h : l.all P) :
List.takeWhile P l = l := by
fun_induction List.takeWhile with simp_all
theorem takeWhileReturn_spec {α : Type} (p : α → Bool) (l : List α) :
takeWhileReturn p l = l.takeWhile p := by
generalize h : takeWhileReturn p l = r
apply Id.of_wp_run_eq h
mvcgen invariants
· Invariant.withEarlyReturn
(onContinue := fun cursor result =>
⌜cursor.prefix.all p ∧ result = (cursor.prefix).takeWhile p⌝)
(onReturn := fun ret result => ⌜ret = result ∧ result = l.takeWhile p⌝)
with (simp at * <;> grind)
def takeWhileBreak {α : Type} (p : α → Bool) (l : List α) : List α := Id.run do
let mut result := []
for x in l do
if ! p x then
break
result := result ++ [x]
return result
theorem takeWhileBreak_spec {α : Type} (p : α → Bool) (l : List α) :
takeWhileBreak p l = l.takeWhile p := by
generalize h : takeWhileBreak p l = r
apply Id.of_wp_run_eq h
mvcgen invariants
· ⇓⟨cursor, result⟩ => ⌜cursor.prefix.all p ∧ result = (cursor.prefix).takeWhile p⌝
with (simp at * <;> grind)
case vc1 =>
expose_names
simp_all
-- impossible to show?
sorry
Sebastian Graf (Nov 11 2025 at 11:54):
Here's how you can make it work:
import Lean
open Std.Do
def takeWhileReturn {α : Type} (p : α → Bool) (l : List α) : List α := Id.run do
let mut result := []
for x in l do
if ! p x then
return result
result := result ++ [x]
return result
set_option mvcgen.warning false
@[grind =, simp]
theorem takeWhile_simplify_false {α : Type} (P : α → Bool) (pref suff : List α) (cur : α) (h : P cur = false) :
List.takeWhile P (pref ++ cur :: suff) = List.takeWhile P pref := by
fun_induction List.takeWhile with simp_all
@[grind =, simp]
theorem takeWhile_all {α : Type} (P : α → Bool) (l : List α) (h : l.all P) :
List.takeWhile P l = l := by
fun_induction List.takeWhile with simp_all
@[grind =, simp]
theorem takeWhile_all' {α : Type} (P : α → Bool) (l : List α) (h : List.takeWhile P l = l) :
l.all P := by
fun_induction List.takeWhile with simp_all
theorem takeWhileReturn_spec {α : Type} (p : α → Bool) (l : List α) :
takeWhileReturn p l = l.takeWhile p := by
generalize h : takeWhileReturn p l = r
apply Id.of_wp_run_eq h
mvcgen invariants
· Invariant.withEarlyReturn
(onContinue := fun cursor result =>
⌜result = cursor.prefix ∧ result = cursor.prefix.takeWhile p⌝)
(onReturn := fun ret result => ⌜ret = result ∧ result = l.takeWhile p⌝)
with (simp at * <;> grind)
def takeWhileBreak {α : Type} (p : α → Bool) (l : List α) : List α := Id.run do
let mut result := []
for x in l do
if ! p x then
break
result := result ++ [x]
return result
theorem takeWhileBreak_spec {α : Type} (p : α → Bool) (l : List α) :
takeWhileBreak p l = l.takeWhile p := by
generalize h : takeWhileBreak p l = r
apply Id.of_wp_run_eq h
mvcgen invariants
· ⇓⟨cursor, result⟩ => ⌜(cursor.suffix = [] ∨ result = cursor.prefix) ∧ result = cursor.prefix.takeWhile p⌝
with (simp at * <;> grind)
Alas, I see that coming up with cursor.suffix = [] ∨ result = cursor.prefix is non-obvious. It is the exact reason why I defined Invariant.withEarlyReturn. Perhaps it would be a good idea to define Invariant.withBreak as well.
However, I'm working on a revamped do elaborator this Q and it is likely that the current elaboration of for loops will change anyway. I hope that I can make it a bit easier to handle early return and break in this kind of proof.
Asei Inoue (Nov 11 2025 at 13:13):
Thank you for your reply!
Perhaps it would be a good idea to define
Invariant.withBreakas well.
I think so!
However, I'm working on a revamped
doelaborator this Q and it is likely that the current elaboration offorloops will change anyway. I hope that I can make it a bit easier to handle earlyreturnandbreakin this kind of proof.
Will the improvements to the do elaborator make Lean’s already impressive do syntax even more powerful, and further enhance the already excellent Std.Do framework? I’m really looking forward to the release!
Sebastian Graf (Nov 11 2025 at 13:28):
The plan is to make do elaborators user-definable and extensible in the first place and perhaps adding support for return, continue and break-at-label. Then it might finally be possible to implement invariant syntax and thus enable intrinsic verification of monadic functions, augmenting Std.Do, yes. Thanks for the praise :)
Asei Inoue (Nov 22 2025 at 11:20):
@Sebastian Graf
I'm sorry if you've already answered this somewhere, but I have one question.
As far as I understand, Lean currently makes functions opaque when they use a while loop, which prevents us from proving properties about them.
Is there any plan to allow proving properties—using tools like mvcgen—for functions that contain while loops, provided that we can prove that the while loop definitely terminates?
Sebastian Graf (Nov 22 2025 at 11:39):
Non-opaque while needs a change to WF preprocessing. We have sketched out a plan for what to do, but we haven't added it to our roadmap yet. We'll announce when we have more concrete plans.
Last updated: Dec 20 2025 at 21:32 UTC