feat: functional cases theorem for non-recursive functions (#6261)

This PR adds `foo.fun_cases`, an automatically generated theorem that
splits the goal according to the branching structure of `foo`, much like
the Functional Induction Principle, but for all functions (not just
recursive ones), and without providing inductive hypotheses.

The design isn't quite final yet as to which function parameters should
become targets of the motive, and which parameters of the theorem, but
the current version is already proven to be useful, so start with this
and iterate later.
This commit is contained in:
Joachim Breitner 2025-01-21 11:16:42 +01:00 committed by GitHub
parent 7b813d4f5d
commit 3569797377
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 108 additions and 1 deletions

View file

@ -1085,6 +1085,63 @@ def deriveInductionStructural (names : Array Name) (numFixed : Nat) : MetaM Unit
if names.size > 1 then
projectMutualInduct names inductName
/--
For non-recursive (and recursive functions) functions we derive a “functional case splitting theorem”. This is very similar
than the functional induction theorem. It splits the goal, but does not give you inductive hyptheses.
For these, it is not really clear which parameters should be “targets” of the motive, as there is
no “fixed prefix” to guide this decision. All? None? Some?
We tried none, but that did not work well. Right now it's all parameters, and it seems to work well.
In the future, we might post-process the theorem (or run the code below iteratively) and remove
targets that are unchanged in each case, so simplify applying the lemma when these “fixed” parameters
are not variables, to avoid having to generalize them.
-/
def deriveCases (name : Name) : MetaM Unit := do
mapError (f := (m!"Cannot derive functional cases principle (please report this issue)\n{indentD ·}")) do
let info ← getConstInfo name
let value ←
if let some unfoldEqnName ← getUnfoldEqnFor? (nonRec := false) name then
let eqInfo ← getConstInfo unfoldEqnName
forallTelescope eqInfo.type fun xs body => do
let some (_, _, rhs) := body.eq?
| throwError "Type of {unfoldEqnName} not an equality: {body}"
mkLambdaFVars xs rhs
else if let some value := info.value? then
pure value
else
throwError "'{name}' does not have an unfold theorem nor a value"
let motiveType ← lambdaTelescope value fun xs _body => do
mkForallFVars xs (.sort 0)
let e' ← withLocalDeclD `motive motiveType fun motive => do
lambdaTelescope value fun xs body => do
let (e',mvars) ← M2.run do
let goal := mkAppN motive xs
-- We bring an unused FVars into scope to pass as `oldIH` and `newIH`. These do not appear anywhere
-- so `buildInductionBody` should just do the right thing
withLocalDeclD `fakeIH (mkConst ``Unit) fun fakeIH =>
let isRecCall := fun _ => none
buildInductionBody #[fakeIH.fvarId!] goal fakeIH.fvarId! fakeIH.fvarId! isRecCall body
let e' ← mkLambdaFVars xs e'
let e' ← abstractIndependentMVars mvars (← motive.fvarId!.getDecl).index e'
let e' ← mkLambdaFVars #[motive] e'
pure e'
unless (← isTypeCorrect e') do
logError m!"constructed functional cases principle is not type correct:{indentExpr e'}"
check e'
let eTyp ← inferType e'
let eTyp ← elimOptParam eTyp
-- logInfo m!"eTyp: {eTyp}"
let params := (collectLevelParams {} eTyp).params
-- Prune unused level parameters, preserving the original order
let us := info.levelParams.filter (params.contains ·)
addDecl <| Declaration.thmDecl
{ name := info.name ++ `fun_cases, levelParams := us, type := eTyp, value := e' }
/--
Given a recursively defined function `foo`, derives `foo.induct`. See the module doc for details.
-/
@ -1097,7 +1154,7 @@ def deriveInduction (name : Name) : MetaM Unit := do
else if let some eqnInfo := Structural.eqnInfoExt.find? (← getEnv) name then
deriveInductionStructural eqnInfo.declNames eqnInfo.numFixed
else
throwError "{name} is not defined by structural or well-founded recursion"
throwError "constant '{name}' is not structurally or well-founded recursive"
def isFunInductName (env : Environment) (name : Name) : Bool := Id.run do
let .str p s := name | return false
@ -1117,14 +1174,32 @@ def isFunInductName (env : Environment) (name : Name) : Bool := Id.run do
return false
| _ => return false
def isFunCasesName (env : Environment) (name : Name) : Bool := Id.run do
let .str p s := name | return false
match s with
| "fun_cases" =>
if (WF.eqnInfoExt.find? env p).isSome then return true
if (Structural.eqnInfoExt.find? env p).isSome then return true
if let some ci := env.find? p then
if ci.hasValue then
return true
return false
| _ => return false
builtin_initialize
registerReservedNamePredicate isFunInductName
registerReservedNamePredicate isFunCasesName
registerReservedNameAction fun name => do
if isFunInductName (← getEnv) name then
let .str p _ := name | return false
MetaM.run' <| deriveInduction p
return true
if isFunCasesName (← getEnv) name then
let .str p _ := name | return false
MetaM.run' <| deriveCases p
return true
return false
end Lean.Tactic.FunInd

View file

@ -0,0 +1,32 @@
/--
info: Option.map.fun_cases.{u_1, u_2} (motive : {α : Type u_1} → {β : Type u_2} → (α → β) → Option α → Prop)
(case1 : ∀ {α : Type u_1} {β : Type u_2} (f : α → β) (x : α), motive f (some x))
(case2 : ∀ {α : Type u_1} {β : Type u_2} (f : α → β), motive f none) {α : Type u_1} {β : Type u_2} (f : α → β)
(x✝ : Option α) : motive f x✝
-/
#guard_msgs in
#check Option.map.fun_cases
example (x : Option Nat) (f : Nat → Nat) : (x.map f).isSome = x.isSome := by
cases f, x using Option.map.fun_cases
case case1 x => simp [-Option.isSome_map']
case case2 => simp [-Option.isSome_map']
/--
info: List.map.fun_cases.{u, v} (motive : {α : Type u} → {β : Type v} → (α → β) → List α → Prop)
(case1 : ∀ {α : Type u} {β : Type v} (f : α → β), motive f [])
(case2 : ∀ {α : Type u} {β : Type v} (f : α → β) (a : α) (as : List α), motive f (a :: as)) {α : Type u} {β : Type v}
(f : α → β) (x✝ : List α) : motive f x✝
-/
#guard_msgs in
#check List.map.fun_cases
/--
info: List.find?.fun_cases.{u} (motive : {α : Type u} → (α → Bool) → List α → Prop)
(case1 : ∀ {α : Type u} (p : α → Bool), motive p [])
(case2 : ∀ {α : Type u} (p : α → Bool) (a : α) (as : List α), p a = true → motive p (a :: as))
(case3 : ∀ {α : Type u} (p : α → Bool) (a : α) (as : List α), p a = false → motive p (a :: as)) {α : Type u}
(p : α → Bool) (x✝ : List α) : motive p x✝
-/
#guard_msgs in
#check List.find?.fun_cases