Zulip Chat Archive

Stream: lean4

Topic: Failed to show termination


Brandon Brown (May 30 2021 at 20:19):

I'm making my own int type derived from the built-in Nat type. I believe I have made a correct subtraction function and it seems obvious to me that it should terminate, because in the recursive calls the second parameter will eventually become either pos zero or neg zero but the termination checker isn't recognizing this and says it failed to show termination. If I mark it with unsafe I can check that it appears to be computing as expected nonetheless. How do I convince the termination checker?
MWE :

inductive int : Type where
| pos : Nat  int
| neg : Nat  int

open int

protected def int.decrement (a : int) : int :=
  match a with
  | pos 0 => neg 1
  | pos (b+1) => pos b
  | neg b => neg (b+1)

#reduce int.decrement $ pos $ 0

protected def int.increment (a : int) : int :=
  match a with
  | pos 0 => pos 1
  | neg 0 => pos 1
  | pos b => pos b.succ
  | neg (b+1) => neg b

#reduce int.increment $ neg $ 2

protected unsafe def int.sub (a b : int) : int :=
  match a,b with
  | pos c, pos 0 => a
  | neg c, pos 0 => a
  | neg c, neg 0 => a
  | pos c, neg 0 => a
  | pos c, pos (d+1) => int.sub (int.decrement a) (pos d)
  | neg c, pos (d+1) => int.sub (int.decrement a) (pos d)
  | neg c, neg (d+1) => int.sub (int.increment a) (neg d)
  | pos c, neg (d+1) => int.sub (int.increment a) (neg d)

-- (-1) - (-2) = +1
#reduce int.sub (neg 1) (neg 2)
-- (-2) - 3 = -5
#reduce int.sub (neg 2) (pos 3)
-- 5 - 3 = +2
#reduce int.sub (pos 5) (pos 3)
-- 3 - (-2) = +5
#reduce int.sub (pos 3) (neg 2)

Kevin Buzzard (May 30 2021 at 20:38):

My complete guess is that Lean might be looking at the first factor first, and here it is not at all obvious that what you write terminates, because increment and decrement can make terms more complex. Why not try a version of the definition where the first factor gets simpler? In Lean 3 you can write your own code to prove termination and get the equation compiler to use it but I don't know if this functionality is available yet in Lean 4.

Ryan Lahfa (May 31 2021 at 04:32):

You can define int.sub by well foundness with an appropriate measure (it looks like a + b is obviously fine but I might be wrong when you will get to the actual implementation)

Daniel Fabian (May 31 2021 at 06:43):

We have structural recursion on arbitrary arguments for types and soon for predicates. This doesn't cover mutually recursive definitions for now. And we don't have well-founded recursion yet.

Mario Carneiro (May 31 2021 at 07:35):

The usual trick here is to have auxiliary definitions that are structural recursive, and that works in this case too:

protected def int.sub (a : int) : int  int
| pos b => subNat a b
| neg b => addNat a b
where
  subNat (a : int) : Nat  int
  | 0 => a
  | d+1 => subNat (int.decrement a) d
  addNat (a : int) : Nat  int
  | 0 => a
  | d+1 => addNat (int.increment a) d

Brandon Brown (May 31 2021 at 15:19):

Cool, I didn't know you could have nested functions

Michael Jam (Aug 30 2022 at 15:11):

Considering this MWE

def f : Nat  Nat  Nat
  | .zero, .zero => .zero
  | .zero, .succ .. => .zero
  | .succ .., .zero  => .zero
  | .succ a, .succ b =>
      f b .zero

inductive N
| zero : N
| succ : N  N

def g : N  N  N
  | .zero, .zero => .zero
  | .zero, .succ .. => .zero
  | .succ .., .zero  => .zero
  | .succ a, .succ b =>
      g b .zero

On 08-27 nightly, lean fails to show termination for g but not for f which is really odd given that N is defined just like Nat. This looks like a bug to me. Thanks a lot for fixing this issue / explaining why it doesn't work.

Andrés Goens (Aug 30 2022 at 15:13):

@Michael Jam it looks a lot like Nat, but it's not Nat. Lean has special machinery there for Nat, see e.g.: https://github.com/leanprover/lean4/blob/78927542b7f8222887b0277d52ed433d6a5f8e30/stage0/src/Init/WF.lean#L151

Michael Jam (Aug 30 2022 at 15:16):

But here, if my understanding is correct, the proof should be by structural recursion and not by WF recursion so I am not sure this is relevant

Michael Jam (Aug 30 2022 at 15:17):

It works by the way, if you replace b by a on the last line

Sebastian Ullrich (Aug 30 2022 at 15:18):

No, Lean fails to dispatch f via structural recursion just like for g. If you dig through f's ultimate definition using #print, you can see the well-founded recursion.

Michael Jam (Aug 30 2022 at 15:19):

Ah so its just fortunate that it works for f because a Nat wf relation is defined behind the scenes

Michael Jam (Aug 30 2022 at 15:22):

Will structural recursion be able to handle such cases in the future? I really think of that example structurally or am I wrong to think like that?

Andrés Goens (Aug 30 2022 at 15:25):

Maybe it's not because of the currying? In a sense you're doing the recursion on N -> N, not on N there. I don't know the details of the type theory behind there though, so maybe I'm totally wrong there as well

Sebastian Ullrich (Aug 30 2022 at 15:27):

It should be technically feasible, but implementing it could be quite tough. You could try it yourself for this example if you want, you'll need course-of-values recursion via Nat.brecOn to jump straight from .succ b to .zero

Michael Jam (Aug 30 2022 at 16:03):

I think I was wrong to think it's structural because actually b changed of position as an argument sorry for wasting time.


Last updated: Dec 20 2023 at 11:08 UTC