lean4-htt/tests/elab/structuralMutual.lean
Joachim Breitner 26ad4d6972
feat: name the functional argument to brecOn in structural recursion (#12987)
This PR extracts the functional (lambda) passed to `brecOn` in
structural
recursion into a named `_f` helper definition (e.g. `foo._f`), similar
to
how well-founded recursion uses `._unary`. This way the functional shows
up
with a helpful name in kernel diagnostics rather than as an anonymous
lambda.

The `_f` definition is added with `.abbrev` kernel reducibility hints
and
the `@[reducible]` elaborator attribute, so the kernel unfolds it
eagerly
after `brecOn` iota-reduces. For inductive predicates, the previous
inline
lambda behavior is kept.

To ensure that parent definitions still get the correct reducibility
height
(since `getMaxHeight` ignores `.abbrev` definitions), each `_f`'s body
height is registered via a new `defHeightOverrideExt` environment
extension.
`getMaxHeight` checks this extension for all definitions, making the
height
computation transparent to the extraction.

This change improves code size (a bit). It may regress kernel reduction
times,
especially if a function defined by structural recursion is used in
kernel reduction
proofs on the hot path. Functions defined by structural recursion are
not particularly
fast to reduce anyways (due to the `.brecOn` construction), so already
now it may be
worth writing a kernel-reduction-friendly function manually (using the
recursor directly,
avoiding overloaded operations). This change will guide you in knowing
which function to
optimize.


🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 13:40:18 +00:00

713 lines
18 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

set_option guard_msgs.diff true
/-!
Mutual structural recursion.
In this file, we often attach a `termination_by structural` annotation to at least
one of the functions to force structural recursion. We don't want lean to resort to
well-founded recursion if structural recursion fails somehow.
-/
mutual
inductive A
| self : A → A
| other : B → A
| empty
inductive B
| self : B → B
| other : A → B
| empty
end
-- A simple mutually recursive function definition
mutual
def A.size : A → Nat
| .self a => a.size + 1
| .other b => b.size + 1
| .empty => 0
termination_by structural x => x
def B.size : B → Nat
| .self b => b.size + 1
| .other a => a.size + 1
| .empty => 0
end
-- And indeed all equationals hold definitionally
theorem A_size_eq1 (a : A) : (A.self a).size = a.size + 1 := rfl
theorem A_size_eq2 (b : B) : (A.other b).size = b.size + 1 := rfl
theorem A_size_eq3 : A.empty.size = 0 := rfl
theorem B_size_eq1 (b : B) : (B.self b).size = b.size + 1 := rfl
theorem B_size_eq2 (a : A) : (B.other a).size = a.size + 1 := rfl
theorem B_size_eq3 : B.empty.size = 0 := rfl
-- The expected equational theorems are produced
/-- info: A.size.eq_1 (a : A) : a.self.size = a.size + 1 -/
#guard_msgs in
#check A.size.eq_1
/-- info: A.size.eq_2 (b : B) : (A.other b).size = b.size + 1 -/
#guard_msgs in
#check A.size.eq_2
/-- info: A.size.eq_3 : A.empty.size = 0 -/
#guard_msgs in
#check A.size.eq_3
/-- info: B.size.eq_1 (b : B) : b.self.size = b.size + 1 -/
#guard_msgs in
#check B.size.eq_1
/-- info: B.size.eq_2 (a : A) : (B.other a).size = a.size + 1 -/
#guard_msgs in
#check B.size.eq_2
/-- info: B.size.eq_3 : B.empty.size = 0 -/
#guard_msgs in
#check B.size.eq_3
-- `_f` definitions show up in diagnostics
/--
info: 3
---
trace: [diag] Diagnostics
[reduction] unfolded declarations (max: 8, num: 4):
[reduction] A.rec ↦ 8
[reduction] Add.add ↦ 3
[reduction] HAdd.hAdd ↦ 3
[reduction] OfNat.ofNat ↦ 2
[reduction] unfolded reducible declarations (max: 4, num: 2):
[reduction] A.casesOn ↦ 4
[reduction] A.size._f ↦ 4
use `set_option diagnostics.threshold <num>` to control threshold for reporting counters
-/
#guard_msgs in
set_option diagnostics true in
set_option diagnostics.threshold 1 in
#reduce A.size (.self (.self (.self .empty)))
-- Smart unfolding works
/--
trace: a : A
h : (B.other a).size.add 0 = 1
⊢ a.size = 0
-/
#guard_msgs in
theorem ex1 (a : A) (h : (A.other (B.other a)).size = 2) : a.size = 0 := by
injection h with h
trace_state -- without smart unfolding the state would be a mess
dsimp [Nat.add_zero] at h
injection h with h
-- And it computes in type just fine
mutual
def A.subs : (a : A) → (Fin a.size → A ⊕ B)
| .self a => Fin.lastCases (.inl a) (a.subs)
| .other b => Fin.lastCases (.inr b) (b.subs)
| .empty => Fin.elim0
termination_by structural x => x
def B.subs : (b : B) → (Fin b.size → A ⊕ B)
| .self b => Fin.lastCases (.inr b) (b.subs)
| .other a => Fin.lastCases (.inl a) (a.subs)
| .empty => Fin.elim0
end
-- We can define mutually recursive theorems as well
mutual
def A.hasNoBEmpty : A → Prop
| .self a => a.hasNoBEmpty
| .other b => b.hasNoBEmpty
| .empty => True
termination_by structural x => x
def B.hasNoBEmpty : B → Prop
| .self b => b.hasNoBEmpty
| .other a => a.hasNoBEmpty
| .empty => False
end
-- Mixing Prop and Nat.
-- This works because both `Prop` and `Nat` are in the same universe (`Type`)
mutual
open Classical
noncomputable
def A.hasNoAEmpty : A → Prop
| .self a => a.hasNoAEmpty
| .other b => b.oddCount > 0
| .empty => False
termination_by structural x => x
noncomputable
def B.oddCount : B → Nat
| .self b => b.oddCount + 1
| .other a => if a.hasNoAEmpty then 0 else 1
| .empty => 0
end
-- Higher levels, but the same level `Type u`
mutual
open Classical
def A.type.{u} : A → Type u
| .self a => Unit × a.type
| .other b => Unit × b.type
| .empty => PUnit
termination_by structural x => x
def B.type.{u} : B → Type u
| .self b => PUnit × b.type
| .other a => PUnit × a.type
| .empty => PUnit
end
-- Mixed levels, should error
/--
error: invalid mutual definition, result types must be in the same universe level, resulting type for `A.strangeType` is
Type : Type 1
and for `B.odderCount` is
Nat : Type
-/
#guard_msgs in
mutual
open Classical
def A.strangeType : A → Type
| .self a => Unit × a.strangeType
| .other b => Fin b.odderCount
| .empty => Unit
termination_by structural x => x
def B.odderCount : B → Nat
| .self b => b.odderCount + 1
| .other a => if Nonempty a.strangeType then 0 else 1
| .empty => 0
end
namespace Index
/-! An example where the data type has parameters and indices -/
mutual
inductive A (m : Nat) : Nat → Type
| self : A m n → A m (n+m)
| other : B m n → A m (n+m)
| empty : A m 0
inductive B (m : Nat) : Nat → Type
| self : B m n → B m (n+m)
| other : A m n → B m (n+m)
| empty : B m 0
end
mutual
def A.size {m n : Nat} : A m n → Nat
| .self a => a.size + m
| .other b => b.size + m
| .empty => 0
termination_by structural x => x
def B.size {m n : Nat} : B m n → Nat
| .self b => b.size + m
| .other a => a.size + m
| .empty => 0
end
mutual
theorem A.size_eq_index (m n : Nat) : (a : A m n) → a.size = n
| .self a => by dsimp [A.size]; rw[ A.size_eq_index]
| .other b => by dsimp [A.size]; rw [B.size_eq_index]
| .empty => rfl
termination_by structural x => x
theorem B.size_eq_index (m n : Nat) : (b : B m n) → b.size = n
| .self b => by dsimp [B.size]; rw [B.size_eq_index]
| .other a => by dsimp [B.size]; rw [A.size_eq_index]
| .empty => rfl
end
end Index
namespace EvenOdd
-- Mutual structural recursion over a non-mutual inductive type
-- (The functions don't actually implement even/odd, but that isn't the point here.)
mutual
def Even : Nat → Prop
| 0 => True
| n+1 => ¬ Odd n
termination_by structural x => x
def Odd : Nat → Prop
| 0 => False
| n+1 => ¬ Even n
end
mutual
def isEven : Nat → Bool
| 0 => true
| n+1 => ! isOdd n
termination_by structural x => x
def isOdd : Nat → Bool
| 0 => false
| n+1 => ! isEven n
end
theorem isEven_eq_2 (n : Nat) : isEven (n+1) = ! isOdd n := rfl
/-- info: EvenOdd.isEven.eq_1 : isEven 0 = true -/
#guard_msgs in
#check isEven.eq_1
theorem eq_true_of_not_eq_false {b : Bool} : (! b) = false → b = true := by simp
theorem eq_false_of_not_eq_true {b : Bool} : (! b) = true → b = false := by simp
/--
trace: n : Nat
h : isOdd (n + 1) = false
⊢ isEven n = true
-/
#guard_msgs in
theorem ex1 (n : Nat) (h : isEven (n+2) = true) : isEven n = true := by
replace h := eq_false_of_not_eq_true h
trace_state -- without smart unfolding the state would be a mess
replace h := eq_true_of_not_eq_false h
exact h
end EvenOdd
namespace MutualIndNonMutualFun
mutual
inductive A
| self : A → A
| other : B → A
| empty
inductive B
| self : B → B
| other : A → B
| empty
end
-- Structural recursion ignoring some types of the mutual inductive
def A.self_size : A → Nat
| .self a => a.self_size + 1
| .other _ => 0
| .empty => 0
termination_by structural x => x
def B.self_size : B → Nat
| .self b => b.self_size + 1
| .other _ => 0
| .empty => 0
termination_by structural x => x
def A.self_size_with_param : Nat → A → Nat
| n, .self a => a.self_size_with_param n + n
| _, .other _ => 0
| _, .empty => 0
termination_by structural _ x => x
-- Structural recursion with more than one function per types of the mutual inductive
mutual
def A.weird_size1 : A → Nat
| .self a => a.weird_size2 + 1
| .other _ => 0
| .empty => 0
termination_by structural x => x
def A.weird_size2 : A → Nat
| .self a => a.weird_size3 + 1
| .other _ => 0
| .empty => 0
def A.weird_size3 : A → Nat
| .self a => a.weird_size1 + 1
| .other _ => 0
| .empty => 0
end
-- We have equality
theorem A.weird_size1_eq_1 (a : A) : (A.self a).weird_size1 = a.weird_size2 + 1 := rfl
-- And the right equational theorems
/--
info: MutualIndNonMutualFun.A.weird_size1.eq_1 (a : A) : a.self.weird_size1 = a.weird_size2 + 1
-/
#guard_msgs in
#check A.weird_size1.eq_1
end MutualIndNonMutualFun
namespace DifferentTypes
-- Check error message when argument types are not mutually recursive
inductive A
| self : A → A
| empty
/--
error: failed to infer structural recursion:
Skipping arguments of type A, as DifferentTypes.Nat.foo has no compatible argument.
Skipping arguments of type Nat, as DifferentTypes.A.with_nat has no compatible argument.
-/
#guard_msgs in
mutual
def A.with_nat : A → Nat
| .self a => a.with_nat + Nat.foo 1
| .empty => 0
termination_by structural x => x
def Nat.foo : Nat → Nat
| n+1 => Nat.foo n
| 0 => A.empty.with_nat
end
end DifferentTypes
namespace FixedIndex
/- Do we run into problems if one of the indices is part of the “fixed prefix”? -/
inductive T : Nat → Type
| a : T 37
| b : T n → T n
def T.size (n : Nat) (start : Nat) : T n → Nat
| a => start
| b t => 1 + T.size n start t
termination_by structural t => t
namespace Mutual
mutual
def T.size1 (n : Nat) (start : Nat) : T n → Nat
| .a => 0
| .b t => 1 + T.size2 n start t
termination_by structural t => t
def T.size2 (n : Nat) (start : Nat) : T n → Nat
| .a => 0
| .b t => 1 + T.size1 n start t
end
end Mutual
namespace Mutual2
mutual
inductive A : Nat → Type
| a : A n
| b : B → A n → A n
inductive B : Type
| a : ((n : Nat) → A n) → B
end
-- In this test A and B have `n start` as fixed prefix, but only
-- in `A` the `n` is an index
set_option linter.constructorNameAsVariable false in
mutual
def A.size (n : Nat) (start : Nat) : A n → Nat
| .a => 0
| .b b a => 1 + B.size n start b + A.size n start a
termination_by structural t => t
def B.size (n : Nat) (start : Nat) : B → Nat
| .a a => 1 + A.size n start (a n)
end
end Mutual2
namespace Mutual3
mutual
inductive A (n : Nat) : Type
| a : A n
| b : B n → A n
inductive B (n : Nat) : Type
| a : A n → B n
end
set_option linter.constructorNameAsVariable false in
mutual
def A.size (n : Nat) (m : Nat) : A n → Nat
| .a => 0
| .b b => 1 + B.size m n b
termination_by structural t => t
def B.size (n : Nat) (m : Nat) : B m → Nat
| .a a => 1 + A.size m n a
end
end Mutual3
/--
error: cannot use specified measure for structural recursion:
its type FixedIndex.T is an inductive family and indices are not variables
T 37
-/
#guard_msgs in
def T.size2 : T 37 → Nat
| a => 0
| b t => 1 + T.size2 t
termination_by structural t => t
end FixedIndex
namespace IndexIsParameter
-- Not actual mutual, but since I was at it
inductive T (n : Nat) : Nat → Type where
| z : T n n
| n : T n n → T n n
/--
error: failed to infer structural recursion:
Cannot use parameter #2:
its type is an inductive datatype and the datatype parameter
n
which cannot be fixed as it is an index or depends on an index, and indices cannot be fixed parameters when using structural recursion.
-/
#guard_msgs in
def T.a {n : Nat} : T n n → Nat
| .z => 0
| .n t => t.a + 1
termination_by structural t => t
end IndexIsParameter
namespace DifferentParameters
-- An attempt to make it fall over mutual recursion over the same type
-- but with different parameters.
inductive T (n : Nat) : Type where
| z : T n
| n : T n → T n
/--
error: failed to infer structural recursion:
Skipping arguments of type T 23, as DifferentParameters.T.b has no compatible argument.
Skipping arguments of type T 42, as DifferentParameters.T.a has no compatible argument.
-/
#guard_msgs in
mutual
def T.a : T 23 → Nat
| .z => 0
| .n t => t.a + 1 + T.b .z
termination_by structural t => t
def T.b : T 42 → Nat
| .z => 0
| .n t => t.b + 1 + T.a .z
end
end DifferentParameters
namespace ManyCombinations
-- A datatype with no size function, to avoid well-founded recursion
inductive Nattish
| zero
| cons : (Nat → Nattish) → Nattish
/--
error: fail to show termination for
ManyCombinations.f
ManyCombinations.g
with errors
failed to infer structural recursion:
Too many possible combinations of parameters of type Nattish (or please indicate the recursive argument explicitly using `termination_by structural`).
Could not find a decreasing measure.
The basic measures relate at each recursive call as follows:
(<, ≤, =: relation proved, ? all proofs failed, _: no proof attempted)
Call from ManyCombinations.f to ManyCombinations.g at 564:15-29:
#1 #2 #3 #4
#5 ? ? ? ?
#6 ? ? = ?
#7 ? ? ? =
#8 ? = ? ?
Call from ManyCombinations.g to ManyCombinations.f at 567:15-29:
#5 #6 #7 #8
#1 _ _ _ _
#2 _ _ _ ?
#3 _ ? _ _
#4 _ _ ? _
#1: sizeOf a
#2: sizeOf b
#3: sizeOf c
#4: sizeOf d
#5: sizeOf a
#6: sizeOf b
#7: sizeOf c
#8: sizeOf d
Please use `termination_by` to specify a decreasing measure.
-/
#guard_msgs in
mutual
def f (a b c d : Nattish) : Nat := match a with
| .zero => 0
| .cons n => g (n 23) c d b
def g (a b c d : Nattish) : Nat := match a with
| .zero => 0
| .cons n => f (n 42) b c d
end
-- specifying one `termination_by structural` helps
#guard_msgs in
mutual
def f' (a b c d : Nattish) : Nat := match a with
| .zero => 0
| .cons n => g' (n 23) b c d
termination_by structural a
def g' (a b c d : Nattish) : Nat := match a with
| .zero => 0
| .cons n => f' (n 42) b c d
end
end ManyCombinations
namespace WithTuple
inductive Tree (α : Type) where
| node : α → (Tree α × Tree α) → Tree α
mutual
def Tree.map (f : α → β) (x : Tree α): Tree β :=
match x with
| node x arrs => node (f x) $ Tree.map_tup f arrs
termination_by structural x
def Tree.map_tup (f : α → β) (x : Tree α × Tree α): (Tree β × Tree β) :=
match x with
| (t₁,t₂) => (Tree.map f t₁, Tree.map f t₂)
termination_by structural x
end
end WithTuple
namespace WithArray
inductive Tree (α : Type) where
| node : α → Array (Tree α) → Tree α
mutual
def Tree.map (f : α → β) (x : Tree α): Tree β :=
match x with
| node x arr₁ => node (f x) $ Tree.map_arr f arr₁
termination_by structural x
def Tree.map_arr (f : α → β) (x : Array (Tree α)): Array (Tree β) :=
match x with
| .mk arr₁ => .mk (Tree.map_list f arr₁)
termination_by structural x
def Tree.map_list (f : α → β) (x : List (Tree α)): List (Tree β) :=
match x with
| [] => []
| h₁::t₁ => (Tree.map f h₁)::Tree.map_list f t₁
termination_by structural x
end
end WithArray
namespace FunIndTests
-- FunInd does not handle mutual structural recursion yet, so make sure we error
-- out nicely
/--
info: A.size.induct (motive_1 : A → Prop) (motive_2 : B → Prop) (case1 : ∀ (a : A), motive_1 a → motive_1 a.self)
(case2 : ∀ (b : B), motive_2 b → motive_1 (A.other b)) (case3 : motive_1 A.empty)
(case4 : ∀ (b : B), motive_2 b → motive_2 b.self) (case5 : ∀ (a : A), motive_1 a → motive_2 (B.other a))
(case6 : motive_2 B.empty) (a✝ : A) : motive_1 a✝
-/
#guard_msgs in
#check A.size.induct
/--
info: A.subs.induct (motive_1 : A → Prop) (motive_2 : B → Prop) (case1 : ∀ (a : A), motive_1 a → motive_1 a.self)
(case2 : ∀ (b : B), motive_2 b → motive_1 (A.other b)) (case3 : motive_1 A.empty)
(case4 : ∀ (b : B), motive_2 b → motive_2 b.self) (case5 : ∀ (a : A), motive_1 a → motive_2 (B.other a))
(case6 : motive_2 B.empty) (a : A) : motive_1 a
-/
#guard_msgs in
#check A.subs.induct
/--
info: MutualIndNonMutualFun.A.self_size.induct (motive : MutualIndNonMutualFun.A → Prop)
(case1 : ∀ (a : MutualIndNonMutualFun.A), motive a → motive a.self)
(case2 : ∀ (a : MutualIndNonMutualFun.B), motive (MutualIndNonMutualFun.A.other a))
(case3 : motive MutualIndNonMutualFun.A.empty) (a✝ : MutualIndNonMutualFun.A) : motive a✝
-/
#guard_msgs in
#check MutualIndNonMutualFun.A.self_size.induct
/--
info: MutualIndNonMutualFun.A.self_size_with_param.induct (motive : Nat → MutualIndNonMutualFun.A → Prop)
(case1 : ∀ (n : Nat) (a : MutualIndNonMutualFun.A), motive n a → motive n a.self)
(case2 : ∀ (x : Nat) (a : MutualIndNonMutualFun.B), motive x (MutualIndNonMutualFun.A.other a))
(case3 : ∀ (x : Nat), motive x MutualIndNonMutualFun.A.empty) (a✝ : Nat) (a✝¹ : MutualIndNonMutualFun.A) :
motive a✝ a✝¹
-/
#guard_msgs in
#check MutualIndNonMutualFun.A.self_size_with_param.induct
/--
info: A.hasNoBEmpty.induct (motive_1 : A → Prop) (motive_2 : B → Prop) (case1 : ∀ (a : A), motive_1 a → motive_1 a.self)
(case2 : ∀ (b : B), motive_2 b → motive_1 (A.other b)) (case3 : motive_1 A.empty)
(case4 : ∀ (b : B), motive_2 b → motive_2 b.self) (case5 : ∀ (a : A), motive_1 a → motive_2 (B.other a))
(case6 : motive_2 B.empty) (a✝ : A) : motive_1 a✝
-/
#guard_msgs in
#check A.hasNoBEmpty.induct
/--
info: EvenOdd.isEven.induct (motive_1 motive_2 : Nat → Prop) (case1 : motive_1 0)
(case2 : ∀ (n : Nat), motive_2 n → motive_1 n.succ) (case3 : motive_2 0)
(case4 : ∀ (n : Nat), motive_1 n → motive_2 n.succ) (a✝ : Nat) : motive_1 a✝
-/
#guard_msgs in
#check EvenOdd.isEven.induct
/--
info: WithTuple.Tree.map.induct {α : Type} (motive_1 : WithTuple.Tree α → Prop)
(motive_2 : WithTuple.Tree α × WithTuple.Tree α → Prop)
(case1 :
∀ (x : α) (arrs : WithTuple.Tree α × WithTuple.Tree α), motive_2 arrs → motive_1 (WithTuple.Tree.node x arrs))
(case2 : ∀ (t₁ t₂ : WithTuple.Tree α), motive_1 t₁ → motive_1 t₂ → motive_2 (t₁, t₂)) (x : WithTuple.Tree α) :
motive_1 x
-/
#guard_msgs in
#check WithTuple.Tree.map.induct
/--
info: WithArray.Tree.map.induct {α : Type} (motive_1 : WithArray.Tree α → Prop) (motive_2 : Array (WithArray.Tree α) → Prop)
(motive_3 : List (WithArray.Tree α) → Prop)
(case1 : ∀ (x : α) (arr₁ : Array (WithArray.Tree α)), motive_2 arr₁ → motive_1 (WithArray.Tree.node x arr₁))
(case2 : ∀ (arr₁ : List (WithArray.Tree α)), motive_3 arr₁ → motive_2 { toList := arr₁ }) (case3 : motive_3 [])
(case4 : ∀ (h₁ : WithArray.Tree α) (t₁ : List (WithArray.Tree α)), motive_1 h₁ → motive_3 t₁ → motive_3 (h₁ :: t₁))
(x : WithArray.Tree α) : motive_1 x
-/
#guard_msgs in
#check WithArray.Tree.map.induct
end FunIndTests