Zulip Chat Archive

Stream: lean4

Topic: Termination works in Lean 3, but fails in Lean 4


Patrick Johnson (Jan 25 2022 at 18:00):

Lean 3 (works):

def f : nat  nat  nat  nat
| 0 x y := x + y
| (n+1) x 0 := x
| (n+1) x (y+1) := f n (f (n+1) x y) (f (n+1) x y + y + 1)

Lean 4 (fails):

def f : Nat  Nat  Nat  Nat
| 0, x, y => x + y
| (n+1), x, 0 => x
| (n+1), x, (y+1) => f n (f (n+1) x y) (f (n+1) x y + y + 1)

Is this expected? My guess is that Lean 3 termination prover can pull some useful facts from mathlib, while Lean4 can't.

Reid Barton (Jan 25 2022 at 18:18):

I don't know what the reason is but it's not anything related to mathlib, because it works in Lean 3 without any imports.

Arthur Paulino (Jan 25 2022 at 18:37):

I was trying to bake a simpler example and I found out that this fails both in Lean 3 and Lean 4:

def g :      -- fails in Lean 3
| (n+1) x := g n (g (n+1) x)
| _ _ := 0

def g : Nat  Nat  Nat -- fails in Lean 4
| (n+1), x => g n (g (n+1) x)
| _, _ => 0

Alice Laroche (Jan 25 2022 at 18:41):

Well this one can be expended infinitly so it's normal
You have :
g (n+1) x => g n (g (n+1) x)) => g n (g n (g (n+1) x))) => g n (g n (g n (g (n+1) x)))) => etc

Patrick Johnson (Jan 25 2022 at 18:53):

The example I posted is the Sudan function.

Arthur Paulino (Jan 25 2022 at 18:59):

Sorry, I think I got it this time:

def f : nat  nat  nat -- works on Lean 3
| (n + 1) (y + 1) := f n (f (n + 1) 0)
| _       _       := 0

def f : Nat  Nat  Nat -- fails on Lean 4
| (n + 1), (y + 1) => f n (f (n + 1) 0)
| _,       _       => 0

Alice Laroche (Jan 25 2022 at 19:11):

Maybe Lean 3 is able to infer that f (n + 1) 0 = 0 so it inline it in the first equation and then solve it ?

Alice Laroche (Jan 25 2022 at 19:14):

This hypothesis doesn't explain why Sudan work in Lean 3 and not in Lean 4 tho, since i don't see such inlining

Mario Carneiro (Jan 26 2022 at 06:10):

The function works in lean 3 because it's decreasing lexicographically on the arguments. It is compiled to a well founded recursion, and the default well order is the lexicographic one. The default decreasing tactic has just enough smarts to try each of the lex constructors and prove n < n+1 which is what is needed to prove this is decreasing

Mario Carneiro (Jan 26 2022 at 06:12):

in lean 4, it appears not to guess a termination measure anymore, it only tries structural recursion if the error message is to be believed. However you can give it the lex order pretty easily, and it goes through without issue:

def f : Nat  Nat  Nat  Nat
| 0, x, y => x + y
| (n+1), x, 0 => x
| (n+1), x, (y+1) => f n (f (n+1) x y) (f (n+1) x y + y + 1)
termination_by _ n x y => (n, x, y)

Mario Carneiro (Jan 26 2022 at 06:20):

For Arthur's test, the obvious thing doesn't work:

def f : Nat  Nat  Nat -- fails on Lean 4
| (n + 1), (y + 1) => f n (f (n + 1) 0)
| _,       _       => 0
termination_by _ n y => (n, y)
tactic 'assumption' failed,
case h
n y : Nat
 0 < Nat.succ y

Lean 3's default decreasing tactic has a small collection of built in theorems about natural numbers like x < x.succ and x <= x + y which are useful for proving things about the autogenerated sizeof function. It was always a bit ad hoc, and we tried to reduce it down to just assumption in lean#288. It appears that lean 4's default decreasing tactic is also weak, although I don't know whether this is a deliberate attempt to downsize or simply something the devs haven't gotten to yet.

Arthur Paulino (Jan 26 2022 at 15:14):

Thanks for the thoughtful answer Mario!


Last updated: Dec 20 2023 at 11:08 UTC