Zulip Chat Archive

Stream: lean4

Topic: Proof of program


Patrick Massot (May 18 2023 at 07:46):

Very locally I'm interested in proving programs. But I have no idea how to get started. Say I want to complete:

def sum (n : Nat) := Id.run do
  let mut r := 0
  for i in [0:n] do
    r := r + i
  return r

theorem sum_eq (n : Nat) : sum (n + 1) = n*(n+1)/2 := sorry

How do I even get started? I can guess induction n, but then what?

Eric Wieser (May 18 2023 at 07:48):

That's false, right? sum n = n there.

Patrick Massot (May 18 2023 at 07:51):

Fixed. I changed my mind half-way through and ended up in an inconsistent state, sorry.

Kevin Buzzard (May 18 2023 at 07:53):

I would get rid of the division, by multiplying both sides by 2 before you state anything but I can't say anything else coherent about this question

Eric Wieser (May 18 2023 at 07:55):

I think the crux of the question is maybe "how do I prove things about docs4#Std.Range.forIn", which I think I've seen threads about before

Eric Wieser (May 18 2023 at 07:55):

See for instance this message by Mario

Patrick Massot (May 18 2023 at 07:56):

Yes, that's one crux. The other one is let mut

Kevin Buzzard (May 18 2023 at 07:56):

Your real question is how to relate sum(n+1) to sum(n) (unless there's another way?) but yeah this suddenly looks terrifying :-)

Eric Wieser (May 18 2023 at 07:58):

I don't think the let mut is a crux; if you rw [sum] to start the proof you see that it desugars immediately

Eric Wieser (May 18 2023 at 07:59):

Running simp then gets you to

Id.run (forIn { start := 0, stop := n + 1, step := 1 } 0 fun i r => ForInStep.yield (r + i)) = ...

which looks in the realm of "basic missing API for forIn"

Siddhartha Gadgil (May 18 2023 at 10:12):

What I would do is define the function differently, as a recursive definition. Then a proof should be easier.

Patrick Massot (May 18 2023 at 10:20):

I specifically don't want to write in an openly functional way.

Patrick Massot (May 18 2023 at 10:21):

The question is: can you write a program normally and still prove it is correct?

Siddhartha Gadgil (May 18 2023 at 10:32):

There is a paper of @Sebastian Ullrich and @Leonardo de Moura about proving things in Monads. One of them can probably explain (to me using mutable variables is not the normal way :smile: ).

Eric Wieser (May 18 2023 at 10:34):

mutable variables (and likely even monads) are irrelevant here, consider

def sum := Id.run do
  let mut r := 0
  r := r + 2
  r := r + 3
  return r

theorem sum_eq : sum = 5 := rfl -- no problem

The problem here is Std.Range.forIn not having API

Patrick Massot (May 18 2023 at 10:43):

Siddhartha, are sure this paper exists? I know about the great https://leanprover.github.io/papers/do.pdf which is mostly about writing normal programs in a functional programming language. It has a short section about proofs, but only devoted to translating between do notation and openly functional style, not directly about proving programs. Is there another paper?

Siddhartha Gadgil (May 18 2023 at 10:50):

Patrick Massot said:

Siddhartha, are sure this paper exists? I know about the great https://leanprover.github.io/papers/do.pdf which is mostly about writing normal programs in a functional programming language. It has a short section about proofs, but only devoted to translating between do notation and openly functional style, not directly about proving programs. Is there another paper?

I meant that paper. I had wrongly assumed that it had such proofs.

Kyle Miller (May 18 2023 at 11:31):

@Patrick Massot I proved a few forIn lemmas to make some progress here.

All that's left is this theorem about natural numbers, and it's sort of gross because the loop is summing upwards rather than downwards:

theorem main :
    acc + start + (start + 1 + n) * (start + n) / 2 - (start + 1) * start / 2 =
    acc + (start + 1 + n) * (start + n) / 2 - start * (start - 1) / 2 := by
  sorry

The code:

import Mathlib.Tactic

def sum (n : Nat) := Id.run do
  let mut r := 0
  for i in [0:n] do
    r := r + i
  return r

section forIn_thms

theorem forIn_range_done1 [Monad m] {f : Nat  β  m (ForInStep β)}
    {fuel start stop step : Nat} (h : stop  start) :
    Std.Range.forIn.loop f fuel start stop step b = pure b := by
  cases fuel <;> simp [Std.Range.forIn.loop, h]

theorem forIn_range_fuel_boost1 [Monad m] (f : Nat  β  m (ForInStep β))
    (fuel start stop n : Nat) (b : β) (hf : stop  start + fuel) :
    Std.Range.forIn.loop f fuel start stop 1 b =
      Std.Range.forIn.loop f (fuel + n) start stop 1 b :=
  match fuel with
  | 0 => by
    simp at hf
    simp [forIn_range_done1 hf]
  | fuel + 1 => by
    rw [Nat.add_assoc, Nat.add_comm 1,  Nat.add_assoc]
    simp [Std.Range.forIn.loop]
    congr! 5 with _ h' s b'
    simp at h'
    have hf' : stop  start + 1 + fuel := by rwa [Nat.add_assoc, Nat.add_comm 1]
    exact forIn_range_fuel_boost1 f fuel (start + 1) stop n b' hf'

theorem forIn_range_1_aux [Monad m] (f : Nat  β  m (ForInStep β))
  (fuel start stop : Nat) (h : stop  start + fuel) :
  Std.Range.forIn.loop f fuel start stop 1 b =
    if start < stop then
      f start b >>= fun r =>
      match r with
      | ForInStep.done b  => pure b
      | ForInStep.yield b => Std.Range.forIn.loop f (fuel - 1) (start + 1) stop 1 b
    else
      pure b := by
  split
  next h' =>
    cases fuel
    · simp at h
      have := trans h h'
      exact False.elim <| Nat.lt_le_antisymm h' h
    next fuel =>
      rw [Std.Range.forIn.loop]
      have : ¬ start  stop := by simp [h']
      simp [this]
      rfl
  next h' =>
    simp at h'
    cases fuel
    · rw [Std.Range.forIn.loop]
      simp [h']
    next fuel =>
      rw [Std.Range.forIn.loop]
      simp [h']

theorem forIn_range_1 [Monad m] (f : Nat  β  m (ForInStep β)) :
    forIn [start:stop] b f =
      if start < stop then
        f start b >>= fun r =>
        match r with
        | ForInStep.done b  => pure b
        | ForInStep.yield b => forIn [start+1 : stop] b f
      else
        pure b := by
  simp [forIn, Std.Range.forIn]
  split
  next h =>
    rw [forIn_range_1_aux]
    simp [h]
    have : stop - 1 + 1 = stop := by cases stop <;> simp at h <;> simp
    congr! 4
    · rw [forIn_range_fuel_boost1 f (stop - 1) (start + 1) stop 1]
      · simp [this]
      · rw [Nat.add_assoc, Nat.add_comm 1, this]
        simp
    · simp
  next h =>
    simp at h
    simp [forIn_range_done1, h]

theorem forIn_range' [Monad m] (f : Nat  β  m (ForInStep β))
    (h : start < stop) :
    forIn [start:stop] b f =
      f start b >>= fun r =>
      match r with
      | ForInStep.done b  => pure b
      | ForInStep.yield b => forIn [start+1 : stop] b f := by
  rw [forIn_range_1]
  simp [h]

end forIn_thms

theorem main :
    acc + start + (start + 1 + n) * (start + n) / 2 - (start + 1) * start / 2 =
    acc + (start + 1 + n) * (start + n) / 2 - start * (start - 1) / 2 := by
  sorry

theorem sum_eq_aux (n start acc : Nat) :
    Id.run (forIn [start : start + n] acc fun i r  ForInStep.yield (r + i)) =
      acc + (start + n) * (start + n - 1) / 2 - start * (start - 1) / 2 :=
  match n with
  | 0 => by
    rw [forIn_range_1]
    simp
    rfl
  | n + 1 => by
    rw [forIn_range']
    case h => simp
    simp
    rw [add_comm n,  add_assoc]
    rw [sum_eq_aux n (start + 1)]
    simp only [ge_iff_le, Nat.succ_add_sub_one, add_le_iff_nonpos_left, nonpos_iff_eq_zero, add_tsub_cancel_right]
    apply main

theorem sum_eq (n : Nat) : sum (n + 1) = n*(n+1)/2 := by
  rw [sum]
  simp
  have := sum_eq_aux (n + 1) 0 0
  simp at this
  rw [this, Nat.mul_comm]

Kyle Miller (May 18 2023 at 11:32):

The mathlib congr! tactic is pretty good at descending into equalities between monad expressions, ifs, and lambdas, and this is used a couple of times here.

Patrick Massot (May 18 2023 at 11:58):

Thanks a lot Kyle!

Henrik Böving (May 18 2023 at 12:03):

I think generally speaking a theorem of the form

for x in [0:n] do
  action

is eqiuvalent to

action
for x in [1:n]
  action

and

for x in [0:n+1]
  action

is equivalent to

for x in [0:n]
   action
action

unfolded to the forIn definition of Std.Range could ease these proofs a lot.

Henrik Böving (May 18 2023 at 12:04):

Hm, actually that theorem doesnt hold if action as a break /o\

Kyle Miller (May 18 2023 at 12:24):

@Henrik Böving Are you suggesting something that's different from, for example, forIn_range'?

Kyle Miller (May 18 2023 at 12:24):

Most of the complexity is from reasoning about the fuel after you unfold the forIn definition.

Kyle Miller (May 18 2023 at 12:25):

Regarding inducting on the stop, I was thinking about how to formulate that, but you need a version of forIn that catches when there's a break to make it easy to state (I guess like Python's for ... else ... loops)

Kevin Buzzard (May 18 2023 at 12:34):

Wow so this is crazy complicated compared to the inductive def of sum. Even the statements are kinda awful. If you multiply both sides by 2 then main is just ring, you've moved the division difficulties elsewhere (but they're not hard to solve here I guess).

Patrick Massot (May 18 2023 at 12:44):

Kevin, this really isn't about Nat division this time. I simply tried to get a program that does something small but non-zero. In my real example there is no Nat division.

Kyle Miller (May 18 2023 at 13:09):

@Kevin Buzzard If Lean has the ability to write imperative programs like this, then I think we should be able to prove things about them. This is a pretty standard program to compute triangle numbers, and you shouldn't have to change the program just because it's not mathematically the most convenient.

That goal isn't hard to prove, though it's not the most fun. Here's something without thinking too hard:

theorem key' (n : Int) : 2  n * (n + 1) := by
  rw [Int.dvd_iff_emod_eq_zero,  Int.even_iff]
  apply Int.even_mul_succ_self

theorem key (n : Nat) : (n + 1) * (n + 2) / 2 = n * (n + 1) / 2 + n + 1 := by
  qify [key' n]
  rw [Int.cast_div]
  simp
  ring
  apply key'
  norm_num

theorem key'' (a b c d : Nat) : a + (b + c) - (d + c) = a + b - d := by
  induction c
  · rfl
  simp [Nat.add_succ, *]

theorem main :
    acc + start + (start + 1 + n) * (start + n) / 2 - (start + 1) * start / 2 =
    acc + (start + 1 + n) * (start + n) / 2 - start * (start - 1) / 2 := by
  cases start
  · simp
  next start =>
    simp [Nat.succ_eq_add_one]
    have := key start
    rw [mul_comm] at this
    simp [this]
    rw [add_assoc _ start]
    rw [add_assoc acc, add_comm (start + 1)]
    simp [key'']
    ring_nf
    trivial

Kyle Miller (May 18 2023 at 13:12):

I'm sure the proofs of sum_eq, sum_eq_aux, and main could be improved, but they're not so long that it suggests that it's practically impossible to prove things about for

Eric Wieser (May 18 2023 at 13:55):

Eric Wieser said:

See for instance this message by Mario

One suggestion in this thread was to redefine forIn to avoid a fuel argument, because then we get useful equation lemmas

Eric Wieser (May 18 2023 at 13:56):

(with the speculation being that it only exists in the first place because termination_by didn't exist yet)

Mario Carneiro (May 18 2023 at 15:06):

I did write some API for dealing with these loops, including a bind-like operator docs4#ForInStep.bind like Kyle suggested, but I was mainly aiming for for loops over lists, not Range.

Mario Carneiro (May 18 2023 at 15:09):

see docs4#List.forIn_eq_bindList

Patrick Massot (May 18 2023 at 15:12):

It looks like this is again relating forIn with functional stuff, but surely this isn't how we hope that proofs of programs in Lean 4 will look like, right? I guess we want tactics generating goals where the local context is the same as what you see inside the loop while programming, without seeing any forIn or monadic stuff, right? I'm aware this is a very vague specification, but does it make some sense?

Mario Carneiro (May 18 2023 at 15:32):

Sure, that is definitely on the agenda (we have been holding some meetings at CMU on this topic). It's a major endeavor though, and not likely to seamlessly interact with do notation as it currently exists (which was quite clearly not written with proving in mind). The approach Std is taking is much more pragmatic and similar to where other such goals pop up in mathlib: break everything down into functional stuff and have lemmas for everything

Sebastian Ullrich (May 18 2023 at 16:26):

The proofs in the do paper were based on forM plus exception monads, not forIn (because they use a different implementation of do notation) https://kha.github.io/do-supplement/For.lean.html

Shreyas Srinivas (May 18 2023 at 16:54):

Mario Carneiro said:

Sure, that is definitely on the agenda (we have been holding some meetings at CMU on this topic). It's a major endeavor though, and not likely to seamlessly interact with do notation as it currently exists (which was quite clearly not written with proving in mind). The approach Std is taking is much more pragmatic and similar to where other such goals pop up in mathlib: break everything down into functional stuff and have lemmas for everything

How do we get this for our programs?

Mario Carneiro (May 18 2023 at 16:55):

what do you mean?

Shreyas Srinivas (May 18 2023 at 17:04):

How do we automatically convert our imperative programs to functional versions?

Kyle Miller (May 18 2023 at 17:08):

I think Std's approach is that there is a team of volunteers that do it (unless you're asking what they're talking about at the meetings)

Shreyas Srinivas (May 18 2023 at 17:13):

delete previous message okay so it is done by hand. I am curious to know what the solutions are, as discussed in these meetings of course.

Mario Carneiro (May 18 2023 at 17:39):

the meetings are about the kind of thing Patrick was talking about: having a fancy proof context which works with the imperative program, in a style similar to dafny. One thing I can say is that it would almost certainly be an alternative to the do notation, not a proof about it

Mario Carneiro (May 18 2023 at 17:41):

It is still very much in the design stage though, if you want to prove things about do notation programs you have to do it the old fashioned way

Jeremy Avigad (May 18 2023 at 17:43):

The idea is to modify the "do unchained" approach and devise syntax so that there are places to write loop invariants and proofs. Ideally, the proofs can be filled in automatically, making Lean look more like Dafny or Why3 or F* in that sense. But users will also be able to write interactive proofs by hand.

There is a toy proof-of-concept example here: https://github.com/avigad/verification_demo/blob/main/Demo/Demo.lean#L94-L103.

Mario Carneiro (May 18 2023 at 21:42):

With some new theorems in Std, the proof is now down to this:

import Std.Data.Range.Lemmas
import Std.Data.List.Lemmas

def sum (n : Nat) := Id.run do
  let mut r := 0
  for i in [0:n] do
    r := r + i
  return r

theorem sum_eq (n : Nat) : sum (n + 1) = n*(n+1)/2 := by
  suffices  l i acc r, l + i = n  i * (i + 1) = 2 * acc 
      Id.run (forIn (List.range' (i+1) l) acc fun i r => .yield (r + i)) = r 
      n * (n + 1) = 2 * r by
    rw [sum]
    simp [Std.Range.forIn_eq_forIn_range', Std.Range.numElems_step_1]
    refine (Nat.div_eq_of_eq_mul_right (by decide) ?_).symm
    exact this _ _ _ _ (by rfl) (by rfl) rfl
  intro l i acc r e ih hr
  induction l generalizing i acc
  · simp [ e, ih,  hr, Id.run]
  · next l IH =>
    rw [Nat.succ_add] at e
    refine IH i.succ _ e ?_ hr
    rw [Nat.mul_add 2,  ih,  Nat.add_mul, Nat.mul_comm]

You can probably golf it some more using mathlib stuff

Patrick Massot (May 19 2023 at 07:58):

Great! Could you also do the forIn' versions?

Shreyas Srinivas (May 19 2023 at 12:53):

I am interested in working towards a solution to this problem because this will make algorithmic verification so much less painful than it is (even in the isabelle world).

Shreyas Srinivas (May 19 2023 at 13:04):

my initial idea was to replace all the loops with folds, because they seem like a general way to represent what loops seem to be doing (fold on a list that carries the relevant program state).

Shreyas Srinivas (May 19 2023 at 13:08):

I don't have a proof that it works in all cases. The idea is based on how forM_ and mapM_ work in haskell

Jannis Limperg (May 19 2023 at 13:16):

forIn, which the for syntax sugar boils down to, is just a monadic fold. It's a slight generalisation of foldlM that also allows early exit.

Shreyas Srinivas (May 19 2023 at 13:26):

The lean version is. But foldl doesn't need to be defined monadically

Jannis Limperg (May 19 2023 at 14:14):

In that case I don't understand the idea. Do you intend to convert the monadic folds to non-monadic ones?

Shreyas Srinivas (May 19 2023 at 14:19):

Jannis Limperg said:

In that case I don't understand the idea. Do you intend to convert the monadic folds to non-monadic ones?

It was an initial idea. I essentially rewrote my own list so that Lean doesn't use some Lean.Internal.coeM and I wrote my fold operation on it. Now I am trying to write an induction rule on this version of fold

Mario Carneiro (May 19 2023 at 17:00):

I essentially rewrote my own list

Don't reinvent List, it will multiply your work 10x. There is a reason String and Array both use List for the model instead of reimplementing it, it has a huge API, convenient recursion principles and a lot of lemmas.


Last updated: Dec 20 2023 at 11:08 UTC