Zulip Chat Archive

Stream: lean4

Topic: Proving termination (yet again...)


Alexander Fasching (Mar 14 2024 at 14:05):

I'm trying to formalize Epistemic Action Logic, and constantly run into some issues with termination.
Without posting the mess I've experimented with, I think I can resolve most of it if I know how to show termination for the examples below.

Here, height terminates:

/- Prove termination automatically. -/
namespace Inferred

inductive Tree (α : Type u) where
  | nil
  | node (pre : α  Tree α) (val : α)

def height : Tree α  Nat
  | .nil => 0
  | .node pre val => 1 + height (pre val)

end Inferred

But not in the next two examples, although they are basically the same:

/- Definition with mutual inductives. -/
namespace Mutual

mutual
  inductive Node (α : Type u) where
    | mk (pre : α  Tree α) (val : α)

  inductive Tree (α : Type u) where
    | nil
    | node (n : Node α)
end

def Node.pre : Node α  (α  Tree α) | .mk pre _ => pre
def Node.val : Node α  α            | .mk _ val => val

def height : Tree α  Nat
  | .nil => 0
  | .node n => 1 + height (n.pre n.val)

end Mutual
/- Definition with parametrized structures. -/
namespace Structure

structure Node (α : Type u) (β : Type v) where
  pre : α  β
  val : α

inductive Tree (α : Type u) where
  | nil
  | node (n : Node α (Tree α))

def height : Tree α  Nat
  | .nil => 0
  | .node n => 1 + height (n.pre n.val)

end Structure

How can I prove termination for the latter two examples?

Joachim Breitner (Mar 14 2024 at 14:32):

Lean does not support structural recursion, like in the first example, with mutually recursive data types (yet).

So the other way of proving termination is with well-founded recursion, e.g. using termination_by t => height t. Of course this doesn't help when you are trying to define height itself…
It seems that there is a sizeOf function defined automatically for your Tree/Node pair, but it seems to ignore function types (not unexpected, hardly possible to assign them a size measured in Nat):

def Mutual.Node._sizeOf_2.{u} : {α : Type u}  [inst : SizeOf α]  Tree α  Nat :=
fun {α} [SizeOf α] t => Tree.rec (fun pre val pre_ih => 1 + sizeOf val) 1 (fun n n_ih => 1 + n_ih) t

If you only want to prove stuff about it, but not compile or #eval, you can useTree.rec manually:

noncomputable
def height : Tree α  Nat := Tree.rec
  (mk := fun pre val ih => 1 + ih val)
  (nil := 0)
  (node := fun n ih => 1 + ih)

#reduce height (Tree.node (Node.mk (fun () => .nil) ()))

Joachim Breitner (Mar 14 2024 at 14:40):

With that non-computable height you can actually define a computable version:

noncomputable
def height' : Tree α  Nat := Tree.rec
  (mk := fun pre val ih => ih val)
  (nil := 0)
  (node := fun n ih => 1 + ih)

def height : Tree α  Nat
  | .nil => 0
  | .node n => 1 + height (n.pre n.val)
termination_by t => height' t
decreasing_by
  simp_wf
  cases n
  simp [Node.pre, Node.val, height']
  omega

Joachim Breitner (Mar 14 2024 at 14:41):

The example with a parametrized structure is not much different from the mutual one, because Lean has do implement nested inductive types using mutual types.

Alexander Fasching (Mar 14 2024 at 16:38):

Thanks!
I think I can work with this.

Mario Carneiro (Mar 14 2024 at 23:03):

Joachim Breitner said:

If you only want to prove stuff about it, but not compile or #eval, you can useTree.rec manually:

You can get the best of all worlds (except the one where you don't have to do this dance) by defining a partial computable version and use @[implemented_by] to the one defined using Tree.rec

Joachim Breitner (Mar 14 2024 at 23:15):

Except that implemented_by is untrusted, so if you care about that, you can define one with Tree.rec, then one using termination_by, then prove them equivalent, and then slap the csimp attribute on that proof. Then you have the best of all worlds, besides having had to far too much for a very simple function :-)


Last updated: May 02 2025 at 03:31 UTC