Recursive theorems with `cases` vs `match`
Opened this issue · 3 comments
Prerequisites
Please put an X between the brackets as you perform the following steps:
- Check that your issue is not already filed:
https://github.com/leanprover/lean4/issues - Reduce the issue to a minimal, self-contained, reproducible test case.
Avoid dependencies to Mathlib or Batteries. - Test your test case against the latest nightly release, for example on
https://live.lean-lang.org/#project=lean-nightly
(You can also use the settings there to switch to “Lean nightly”)
Description
Consider:
inductive C : Nat → Type where
| C0 : C 0
| C1 : C n → C (1 + n)
| C2 : C n → C n → C n
open C
def id_C (c : C n) : C n :=
match c with
| C0 => C0
| C1 c => C1 (id_C c)
| C2 c₁ c₂ => C2 (id_C c₁) (id_C c₂)
We can prove the following theorem with match
or induction
:
theorem id_c_is_identity (c : C n) : id_C c = c := by
match c with
| C0 => simp [id_C]
| C1 inner => simp [id_C, id_c_is_identity inner]
| C2 lhs rhs => simp [id_C, id_c_is_identity lhs, id_c_is_identity rhs]
this will use the structural termination metric on c
as expected.
However the following:
theorem id_c_is_identity (c : C n) : id_C c = c := by
cases c with
| C0 => simp [id_C]
| C1 inner => simp [id_C, id_c_is_identity inner]
| C2 lhs rhs => simp [id_C, id_c_is_identity lhs, id_c_is_identity rhs]
Fails to prove termination, if we force a use of the structural metric:
theorem id_c_is_identity (c : C n) : id_C c = c := by
cases c with
| C0 => simp [id_C]
| C1 inner => simp [id_C, id_c_is_identity inner]
| C2 lhs rhs => simp [id_C, id_c_is_identity lhs, id_c_is_identity rhs]
termination_by structural c
We get
failed to infer structural recursion:
Cannot use parameter c:
failed to eliminate recursive application
id_c_is_identity lhs
Steps to Reproduce
Expected behavior: Given that it works with match
it should either also work with cases
or there should be a better error message explaining why that is not the case (haha).
Actual behavior: match
works, cases
doesn't.
Versions
"4.12.0-nightly-2024-10-13"
Impact
Add 👍 to issues you consider important. If others are impacted by this issue, please ask them to add 👍 to it.
What happens if you pass explicit arguments to the theorem before passing them to rw
? I.e. is the problem with rw
or with cases
?
Breaks in the same fashion, updated the issue to align the code.
Interestingly the issue is only with C2
, not with C1
.
Initial debugging with set_option trace.Elab.definition true
: This is the predefinition with cases
id_c_is_identity : ∀ {n : Nat} (c : C n), id_C c = c :=
fun {n} c =>
C.casesOn (motive := fun a t => n = a → HEq c t → id_C c = c) c
(fun h =>
Eq.ndrec (motive := fun {n} => ∀ (c : C n), HEq c C.C0 → id_C c = c)
(fun c h => Eq.symm (eq_of_heq h) ▸ of_eq_true (eq_self C.C0)) (Eq.symm h) c)
(fun {n_1} inner h =>
Eq.ndrec (motive := fun {n} => ∀ (c : C n), HEq c inner.C1 → id_C c = c)
(fun c h =>
Eq.symm (eq_of_heq h) ▸
of_eq_true
(Eq.trans (congrArg (fun x => x.C1 = inner.C1) (_root_.id_c_is_identity n_1 inner))
(eq_self inner.C1)))
(Eq.symm h) c)
(fun {n_1} lhs rhs h =>
Eq.ndrec (motive := fun {n_2} => ∀ (lhs rhs : C n_2), HEq c (lhs.C2 rhs) → id_C c = c)
(fun lhs rhs h =>
Eq.symm (eq_of_heq h) ▸
of_eq_true
(Eq.trans
(congrArg (fun x => x = lhs.C2 rhs)
(congr (congrArg C.C2 (_root_.id_c_is_identity n lhs)) (_root_.id_c_is_identity n rhs)))
(eq_self (lhs.C2 rhs))))
h lhs rhs)
(Eq.refl n) (HEq.refl c)
and this with match
:
id_c_is_identity : ∀ {n : Nat} (c : C n), id_C c = c :=
fun {n} c =>
match n, c with
| .(0), C.C0 => of_eq_true (eq_self C.C0)
| .(1 + n), inner.C1 =>
of_eq_true (Eq.trans (congrArg (fun x => x.C1 = inner.C1) (_root_.id_c_is_identity n inner)) (eq_self inner.C1))
| n, lhs.C2 rhs =>
of_eq_true
(Eq.trans
(congrArg (fun x => x = lhs.C2 rhs)
(congr (congrArg C.C2 (_root_.id_c_is_identity n lhs)) (_root_.id_c_is_identity n rhs)))
(eq_self (lhs.C2 rhs)))
Clearly the cases
construction, with C.casesOn
, is more complicated. There is still a chance that the transformation done for structural recursion (with the C.below
value pushed into the C.casesOn
could work here, but hard to tell without digging deeper (i.e. adding more tracing.)
Using well-founded recursion one gets
failed to prove termination, possible solutions:
- Use `have`-expressions to prove the remaining goals
- Use `termination_by` to specify a different well-founded relation
- Use `decreasing_by` to specify your own tactic for discharging this kind of goal
n✝ : Nat
c : C n✝
a✝² : ∀ (y : C n✝), (invImage (fun x => x) instWellFoundedRelationOfSizeOf).1 y c → id_C y = y
n : Nat
a✝¹ a✝ : C n
x✝ : ∀ (y : C n), (invImage (fun x => x) instWellFoundedRelationOfSizeOf).1 y (a✝¹.C2 a✝) → id_C y = y
h✝¹ : n✝ = n
lhs rhs : C n✝
h✝ : HEq c (lhs.C2 rhs)
⊢ sizeOf lhs < 1 + n + sizeOf a✝¹ + sizeOf a✝
and notices that there is a HEq
that isn't actually heterogenous at this point, so maybe that’s also a lead.
Anyways, this first investigation does not point to a quick fix, and there seems to be a work-around, so will not dig deeper until this happens more often or becomes a clear blocker.