Zulip Chat Archive
Stream: new members
Topic: mvcgen doesn't produce any invariant goals
Anthony Wang (Sep 29 2025 at 02:29):
Inspired by , I'm trying to verify the bubble sort implementation in that post using mvcgen in Lean v4.24.0-rc1. This is my code so far:
import Std.Tactic.Do
import Mathlib
def BubbleSort {α} [LT α] [DecidableLT α] (a: Array α) : Array α := Id.run do
let n := a.size
let mut a : Vector α n := a.toVector
for i in List.range (n - 1) do
for hj : j in List.range (n - i - 1) do
have := List.mem_range.mp hj
if a[j] > a[j + 1] then
a := a.swap j (j+1)
return a.toArray
open Std.Do
theorem BubbleSortSameElements [LT α] [DecidableLT α] (A : Array α) : Multiset.ofList A.toList = Multiset.ofList (BubbleSort A).toList := by
generalize h : BubbleSort A = x
apply Id.of_wp_run_eq h
mvcgen
However, mvcgen doesn't produce any invariants goals for me to assign loop invariants to, and instead changes the goal to this:
(wp⟦forIn (List.range (n✝ - 1)) a✝ fun i r => do
let r ←
forIn' (List.range (n✝ - i - 1)) r fun a m b =>
if b[a] > b[a + 1] then do
pure PUnit.unit
pure (ForInStep.yield (b.swap a (a + 1) ⋯ ⋯))
else do
pure PUnit.unit
pure (ForInStep.yield b)
pure PUnit.unit
pure (ForInStep.yield r)⟧
(fun a => wp⟦pure a.toArray⟧ (PostCond.noThrow fun a => { down := ↑A.toList = ↑a.toList }), ExceptConds.false)).down
Is this because of some limitation in mvcgen or am I using it incorrectly?
Asei Inoue (Oct 19 2025 at 02:54):
@Sebastian Graf may be right person to ask..?
Sebastian Graf (Oct 20 2025 at 14:50):
Thanks for pinging me. I just had a look and it seems like you found a bug. Will look for a fix tomorrow!
Sebastian Graf (Oct 21 2025 at 07:51):
Sigh. It's a universe polymorphism issue (it almost always is)! BubbleSort has a universe parameter for the type of α, and the specification for forIn requires this parameter to always be the same as that of the list element type Nat : Type, which is 0. I'll see if I can generalize the specification lemma. Meanwhile, you can keep exploring mvcgen by instantiating BubbleSort.{0} in the theorem statemtnt:
import Std.Tactic.Do
import Mathlib
def BubbleSort {α} [LT α] [DecidableLT α] (a: Array α) : Array α := Id.run do
let n := a.size
let mut a : Vector α n := a.toVector
for i in List.range (n - 1) do
for hj : j in List.range (n - i - 1) do
have := List.mem_range.mp hj
if a[j] > a[j + 1] then
a := a.swap j (j+1)
return a.toArray
open Std.Do
theorem BubbleSortSameElements [LT α] [DecidableLT α] (A : Array α) : Multiset.ofList A.toList = Multiset.ofList (BubbleSort.{0} A).toList := by
generalize h : BubbleSort A = x
apply Id.of_wp_run_eq h
mvcgen
Sebastian Graf (Oct 21 2025 at 08:55):
Fixed in https://github.com/leanprover/lean4/pull/10865.
Anthony Wang (Oct 22 2025 at 04:35):
Thanks @Sebastian Graf! mvcgen still wasn't producing the right invariant goals with your .{0} trick and Lean v4.24.0, but updating to v4.25.0-rc1 fixed the problem. I finally completed my proof of correctness for a sorting algorithm (not bubble sort but a very closely related algorithm)!
Asei Inoue (Oct 22 2025 at 04:46):
I want to see your sorting algorithm verification
Anthony Wang (Oct 22 2025 at 04:53):
Here's the code: https://git.unnamed.website/miscelleaneous/plain/Sort.lean
Anthony Wang (Oct 22 2025 at 04:54):
(deleted)
Sebastian Graf (Oct 22 2025 at 06:40):
Yes, in v4.25.0 I fixed a problem where the invariant of the inner loop got instantiated with the invariant of the outer loop. Good call trying v4.25.0 and congrats on finishing your proof :)
Asei Inoue (Nov 18 2025 at 14:30):
@Anthony Wang
Thank you for providing nice code example!
I removed Mathlib dependency from your example (Std has IsLinearOrder type class! :) )
import Std.Tactic.Do
import Batteries
open Std
variable {α : Type} (A : Array α)
variable [LT α] [DecidableLT α]
def ICan'tBelieveItCanSort := Id.run do
let N := A.size
let mut A := A.toVector
for hi : i in [:N] do
for hj : j in [:N] do
if A[i] < A[j] then
A := A.swap i j
return A.toArray
open Std.Do
set_option mvcgen.warning false
theorem ICan'tBelieveItCanSort_perm (arr : Array α) : Array.Perm (ICan'tBelieveItCanSort arr) arr := by
generalize h : ICan'tBelieveItCanSort arr = x
apply Id.of_wp_run_eq h
mvcgen invariants
· ⇓⟨_cursor, vec⟩ => ⌜Array.Perm arr vec.toArray⌝
· ⇓⟨_cursor, vec⟩ => ⌜Array.Perm arr vec.toArray⌝
with grind [Array.Perm.trans, Array.Perm.symm, Array.swap_perm]
@[simp, grind =]
theorem Vector.toArray_extract_size {α : Type} {n : Nat} (v : Vector α n) :
v.toArray.extract 0 n = v.toArray := by
grind
@[grind! =>]
theorem lemm (n : Nat) (xs : [0:n].toList.Cursor) : xs.pos ≤ n := by
have : xs.prefix.length + xs.suffix.length = n := by
simp [← List.length_append, xs.property]
grind
variable [LE α] [IsLinearOrder α] [LawfulOrderLT α]
theorem sorted : ICan'tBelieveItCanSort A |>.Pairwise (· ≤ ·) := by
generalize h : ICan'tBelieveItCanSort A = x
apply Id.of_wp_run_eq h
mvcgen invariants
· ⇓⟨xs, A'⟩ => ⌜A'.take xs.pos |>.toArray.Pairwise (· ≤ ·)⌝
· ⇓⟨xs, A'⟩ => by
expose_names
exact ⌜(A'.take cur |>.toArray.Pairwise (· ≤ ·)) ∧ ∀ i (_ : i < xs.pos), A'[i]'(by grind) ≤ A'[cur]'(by grind)⌝
with grind
case vc1.step.isTrue =>
expose_names
simp [Array.pairwise_iff_getElem] at *
refine ⟨?_, by grind⟩
grind
case vc4.step.post.success =>
simp_all
grind [= Array.pairwise_iff_getElem]
Asei Inoue (Nov 18 2025 at 14:35):
I tried to keep the code as concise as possible and arranged things so that the proof would finish with just grind, but I couldn’t push it any further than this.
Asei Inoue (Nov 23 2025 at 13:03):
done!!
import Std.Tactic.Do
import Batteries.Data.Array
open Std
variable {α : Type}
variable [LT α] [DecidableLT α]
def ICan'tBelieveItCanSort (arr : Array α) := Id.run do
let n := arr.size
let mut vec := arr.toVector
for hi : i in [0:n] do
for hj : j in [0:n] do
if vec[i] < vec[j] then
vec := vec.swap i j
return vec.toArray
open Std.Do
set_option mvcgen.warning false
theorem ICan'tBelieveItCanSort_perm (arr : Array α) : Array.Perm (ICan'tBelieveItCanSort arr) arr := by
generalize h : ICan'tBelieveItCanSort arr = x
apply Id.of_wp_run_eq h
mvcgen invariants
· ⇓⟨_cursor, vec⟩ => ⌜Array.Perm arr vec.toArray⌝
· ⇓⟨_cursor, vec⟩ => ⌜Array.Perm arr vec.toArray⌝
with grind [Array.Perm.trans, Array.Perm.symm, Array.swap_perm]
@[grind =]
theorem Vector.toArray_extract_size {α : Type} {n : Nat} (v : Vector α n) :
v.toArray.extract 0 n = v.toArray := by
grind
@[grind! =>]
theorem List.Cursor.pos_le_length (n : Nat) (xs : [0:n].toList.Cursor) : xs.pos ≤ n := by
have : xs.prefix.length + xs.suffix.length = n := by
simp [← List.length_append, xs.property]
grind only
theorem Array.sorted_take_swap {α : Type} [LE α] [IsPreorder α] {arr : Array α}
(s t : Nat) (hs : s < arr.size) (ht : t < arr.size)
(h : Array.Pairwise (· ≤ ·) (arr.take s))
(le1 : arr[s] ≤ arr[t])
(le2 : ∀ (i : Nat) (_ : i < t), arr[i] ≤ arr[s])
: Array.Pairwise (· ≤ ·) ((arr.swap s t).take s) := by
simp [Array.pairwise_iff_getElem] at h ⊢
grind
@[grind <=]
theorem Array.pairwise_take_swap {α : Type} {R : α → α → Prop} [Refl R] [Trans R R R] {arr : Array α}
(s t : Nat) (hs : s < arr.size) (ht : t < arr.size)
(h : Array.Pairwise R (arr.take s))
(le1 : R arr[s] arr[t])
(le2 : ∀ (i : Nat) (_ : i < t), R arr[i] arr[s])
: Array.Pairwise R ((arr.swap s t).take s) := by
let : LE α := { le := R }
have : IsPreorder α := {
le_refl := Refl.refl
le_trans _ _ _ := Trans.trans
}
apply Array.sorted_take_swap <;> assumption
@[grind <=]
theorem Array.pairwise_take_succ {α : Type} {R : α → α → Prop} {arr : Array α}
(k : Nat) (hk : k < arr.size)
(h : Array.Pairwise R (arr.take k))
(le : ∀ (i : Nat) (_ : i < arr.size), R arr[i] arr[k])
: Array.Pairwise R (arr.take (k + 1)) := by
simp [Array.pairwise_iff_getElem] at h ⊢
grind
variable [LE α] [IsLinearOrder α] [LawfulOrderLT α]
theorem ICan'tBelieveItCanSort_sorted (arr : Array α) : ICan'tBelieveItCanSort arr |>.Pairwise (· ≤ ·) := by
generalize h : ICan'tBelieveItCanSort arr = x
apply Id.of_wp_run_eq h
mvcgen invariants
· ⇓⟨cursor, vec⟩ => ⌜vec.take cursor.pos |>.toArray.Pairwise (· ≤ ·)⌝
· ⇓⟨cursor, vec⟩ => by
expose_names
exact ⌜(vec.take cur |>.toArray.Pairwise (· ≤ ·)) ∧
∀ i (_ : i < cursor.pos), vec[i]'(by grind) ≤ vec[cur]'(by grind)⌝
with (simp at *; grind)
Last updated: Dec 20 2025 at 21:32 UTC