Zulip Chat Archive
Stream: lean4
Topic: Proving termination in nested inductives
Daniel Fabian (Feb 11 2022 at 14:56):
I'm working on taking inspiration from some Coq code and implementing something similar in Lean. And I hit some trouble proving termination for something that coq seems to have no trouble at all.
The context is, that I've got an n-ary tree to work with. Here's a MWE:
Inductive nelist (A : Type) : Type :=
| nil : A -> nelist A
| cons : A -> nelist A -> nelist A.
Section f.
Variable A: Type.
Variable B: Type.
Variable f: A -> B.
Variable g: B -> B -> B.
Fixpoint foldMap (l : nelist A) : B :=
match l with
| nil _ x => f x
| cons _ x xs => g (f x) (foldMap xs)
end.
End f.
Inductive tree : Type :=
| node : nelist tree -> tree
| leaf : tree.
Fixpoint count (t : tree) : nat :=
match t with
| node xs => foldMap _ _ count (fun x y => x + y) xs
| leaf => 1
end.
inductive nelist (α : Type u) : Type u
| nil : α → nelist α
| cons : α → nelist α → nelist α
def foldMap (f : α → β) (g : β → β → β) : nelist α → β
| nelist.nil a => f a
| nelist.cons x xs => g (f x) (foldMap f g xs)
inductive tree
| node : nelist tree → tree
| leaf : tree
def count : tree → Nat
| tree.node xs => foldMap count Nat.add xs
| tree.leaf => 1
and when I try using well-founded recursion I just get an unexpected occurrence of recursive application
.
Any idea how to prove these nicely?
Sebastian Ullrich (Feb 11 2022 at 15:13):
The generic solution is to equip foldMap
's fold function with a proof that the passed element is part of the given list, from which you can derive that its sizeOf
size is smaller. The simpler solution is to specialize foldMap
to count
, yielding mutually recursive definitions. I pushed an example of the latter a few days ago https://github.com/leanprover/lean4/blob/master/tests/lean/run/mut_ind_wf.lean
Leonardo de Moura (Feb 11 2022 at 15:20):
And I hit some trouble proving termination for something that coq seems to have no trouble at all.
Recall that Lean does not have a termination checker in the kernel.
In Lean, we have to justifiy the termination using structural or well founded recursion.
This kind of example is quite verbose right now because of missing features:
lineararith
is missing- Heuristics for guessing good well founded relations is missing
- Better support for combinators such as map and fold is missing.
Daniel Fabian (Feb 11 2022 at 15:25):
yeah I see that:
tactic 'assumption' failed,
xs : nelist tree
⊢ sizeOf xs < 1 + sizeOf xs
Daniel Fabian (Feb 11 2022 at 15:26):
in fact all of the proofs would go away with a bit of linear arithmetic.
Leonardo de Moura (Feb 11 2022 at 15:28):
We are working on that as we described in our last dev meeting that included you :stuck_out_tongue_wink:
Daniel Fabian (Feb 11 2022 at 15:30):
I know, I know. ;-) I'm working on the other feature and am kinda forced to prove termination. FWIW, the termination_by
experience doesn't even compare to Lean 3 in how convenient and much less confusing it is.
Daniel Fabian (Feb 12 2022 at 09:19):
working on this I found something I don't understand. It looks like a bug to me, but maybe I'm misusing it somehow.
The general idea is to define a definitional reduction to reify objects. And rfl
need help in a way that makes absolutely no sense to me. calling #reduce
behaves as I'd hope, but rfl
needs what looks to me like an unnecessary simp
step just to unfold the definition. I even tried marking the def
as @[reducible]
, but I don't think it should be needed to begin with.
Here's the most hatchety job I could do, deleting most of the code:
def arrow (α : Type u) : Nat → Type u
| 0 => α
| n+1 => α → arrow α n
structure Symbol (α : Type u) where
arity : Nat
value : arrow α arity
inductive NonEmptyList (α : Type u)
| nil (a : α)
| cons (a : α) (as : NonEmptyList α)
syntax "![" term ", " sepBy(term, ", ") "]" : term
syntax "![" term "]" : term
def NonEmptyList.ofList : α → List α → NonEmptyList α
| a, [] => NonEmptyList.nil a
| a, b::bs => NonEmptyList.cons a (ofList b bs)
macro_rules
| `(![ $elem, $elems,* ]) => `(NonEmptyList.ofList $elem [ $elems,* ])
| `(![ $elem ]) => `(NonEmptyList.nil $elem)
infixr:67 " !:: " => NonEmptyList.cons
def NonEmptyList.get! : NonEmptyList α → Nat → α
| ![a], _ => a
| _ !:: as, n+1 => as.get! n
| a !:: _, 0 => a
structure Context (α : Type u) where
symbols : NonEmptyList $ Symbol α
arbitrary : α
inductive Expr
| Sym (idx : Nat) (args : List Expr)
def eval {α : Type u} (ctx : Context α) : Expr → α
| Expr.Sym idx args =>
let sym := ctx.symbols.get! idx
have : sizeOf args < 1 + idx + sizeOf args := by
rw [Nat.add_assoc, Nat.add_comm 1]
apply Nat.lt_succ_of_le
apply Nat.le_add_left
reduceSym sym args
where
reduceSym : Symbol α → List Expr → α
| ⟨0, f⟩, [] => f
| ⟨n+1, f⟩, a::as =>
have : sizeOf a < 1 + sizeOf a + sizeOf as := by
rw [Nat.add_assoc, Nat.add_comm 1]
apply Nat.lt_succ_of_le
apply Nat.le_add_right
have : sizeOf as < 1 + sizeOf a + sizeOf as := by
rw [Nat.add_assoc, Nat.add_comm 1]
apply Nat.lt_succ_of_le
apply Nat.le_add_left
reduceSym ⟨n, f (eval ctx a)⟩ as
| _, _ => ctx.arbitrary
termination_by
eval e => sizeOf e
reduceSym _ xs => sizeOf xs
def myCtx (x y z : Nat) : Context Nat := {
symbols := ![
{ arity := 0, value := (x : Nat) },
{ arity := 0, value := (y : Nat) },
{ arity := 0, value := (z : Nat) }
]
arbitrary := 42
}
example (x y z : Nat) : eval (myCtx x y z) (Expr.Sym 0 []) = x := by
--simp [myCtx]
--simp [NonEmptyList.get!]
--simp [NonEmptyList.ofList]
simp [eval] -- !!!!!!! commenting this out breaks. The previous simps are just there to prettify the goal
rfl
example (x y z) : (myCtx x y z).symbols.get! 0 = ⟨0, x⟩ := rfl
constant x : Nat
constant y : Nat
constant z : Nat
#reduce eval (myCtx x y z) (Expr.Sym 0 []) -- !!!!!!! this here happily reduces to x
Sebastian Ullrich (Feb 12 2022 at 10:03):
Likely the same issue as in https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/mathport.3Awhnf-power - the (elaborator) unifier is very conservative about unfolding well-founded recursion
Daniel Fabian (Feb 12 2022 at 10:10):
ok... so how can I make progress then? The functions I'm defining are unlikely going to work as pure structural recursion. And if well-founded recursion is not unfolded automatically, how can I produce a proof automatically? The idea is to take variables from a local ctx and pass them into these objects, let lean perform a computation to verify the objects are equal modulo AAC. Then, in the end we can reify the proof into the terms the user wanted.
In fact, when I tried a simp [eval]
on the actual definition it fails, claiming the motive is not type correct.
Leonardo de Moura (Feb 12 2022 at 14:26):
@Daniel Fabian I am assuming you are trying to "proof by reflection", correct?
If yes, then you should not use well-found recursion anyway since they are very expensive to reduce in the kernel.
Another reason is that, as Sebastian pointed out, the elaborator unifier is conservative about them.
ok... so how can I make progress then?
If the goal is proof by reflection, you can always use structural recursion + Nat fuel argument, and then set the argument with a big enough value.
Daniel Fabian (Feb 12 2022 at 16:45):
yes, correct on all accounts. We can basically make a fake structural with enough fuel. Just need to give a upper bound on the steps. I noticed it when looking at the existing sample code. I still do want to use n-ary trees, though. They should make other things easier.
Last updated: Dec 20 2023 at 11:08 UTC