Skip to content

Commit eef4537

Browse files
nomeataluisacicolini
authored andcommitted
feat: fun_induction foo (no arguments) (leanprover#7101)
This PR implements `fun_induction foo`, which is like `fun_induction foo x y z`, only that it picks the arguments to use from a unique suitable call to `foo` in the goal.
1 parent 3092aaa commit eef4537

File tree

5 files changed

+464
-8
lines changed

5 files changed

+464
-8
lines changed

Diff for: src/Init/Tactics.lean

+16
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,14 @@ induction y₁, ... yₘ using f.induct x₁ ... xₙ
917917
where the arguments of `f` are used as arguments to `f.induct` or targets of the induction, as
918918
appropriate.
919919
920+
The form
921+
```
922+
fun_induction f
923+
```
924+
(with no arguments to `f`) searches the goal for an unique eligible application of `f`, and uses
925+
these arguments. An application of `f` is eligible if it is saturated and the arguments that will
926+
become targets are free variables.
927+
920928
The forms `fun_induction f x y generalizing z₁ ... zₙ` and
921929
`fun_induction f x y with | case1 => tac₁ | case2 x' ih => tac₂` work like with `induction.`
922930
-/
@@ -938,6 +946,14 @@ cases y, ... using f.fun_cases x ...
938946
where the arguments of `f` are used as arguments to `f.fun_cases` or targets of the case analysis, as
939947
appropriate.
940948
949+
The form
950+
```
951+
fun_cases f
952+
```
953+
(with no arguments to `f`) searches the goal for an unique eligible application of `f`, and uses
954+
these arguments. An application of `f` is eligible if it is saturated and the arguments that will
955+
become targets are free variables.
956+
941957
The form `fun_cases f x y with | case1 => tac₁ | case2 x' ih => tac₂` works like with `cases`.
942958
-/
943959
syntax (name := funCases) "fun_cases " term (inductionAlts)? : tactic

Diff for: src/Lean/Elab/Tactic/Induction.lean

+23-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Lean.Meta.Tactic.ElimInfo
1313
import Lean.Meta.Tactic.FunIndInfo
1414
import Lean.Meta.Tactic.Induction
1515
import Lean.Meta.Tactic.Cases
16+
import Lean.Meta.Tactic.FunIndCollect
1617
import Lean.Meta.GeneralizeVars
1718
import Lean.Elab.App
1819
import Lean.Elab.Tactic.ElabTerm
@@ -819,12 +820,33 @@ def evalInduction : Tactic := fun stx =>
819820
let targets ← withMainContext <| addImplicitTargets elimInfo targets
820821
evalInductionCore stx elimInfo targets toTag
821822

823+
824+
/--
825+
Elaborates the `foo args` of `fun_induction` or `fun_cases`, inferring the args if omitted from the goal
826+
-/
827+
def elabFunTargetCall (cases : Bool) (stx : Syntax) : TacticM Expr := do
828+
match stx with
829+
| `($id:ident) =>
830+
let fnName ← realizeGlobalConstNoOverload id
831+
let some _ ← getFunIndInfo? cases fnName |
832+
let theoremKind := if cases then "induction" else "cases"
833+
throwError "no functional {theoremKind} theorem for '{.ofConstName fnName}', or function is mutually recursive "
834+
let candidates ← FunInd.collect fnName (← getMainGoal)
835+
if candidates.isEmpty then
836+
throwError "could not find suitable call of '{.ofConstName fnName}' in the goal"
837+
if candidates.size > 1 then
838+
throwError "found more than one suitable call of '{.ofConstName fnName}' in the goal. \
839+
Please include the desired arguments."
840+
pure candidates[0]!
841+
| _ =>
842+
elabTerm stx none
843+
822844
/--
823845
Elaborates the `foo args` of `fun_induction` or `fun_cases`, returning the `ElabInfo` and targets.
824846
-/
825847
private def elabFunTarget (cases : Bool) (stx : Syntax) : TacticM (ElimInfo × Array Expr) := do
826848
withRef stx <| withMainContext do
827-
let funCall ← elabTerm stx none
849+
let funCall ← elabFunTargetCall cases stx
828850
funCall.withApp fun fn funArgs => do
829851
let .const fnName fnUs := fn |
830852
throwError "expected application headed by a function constant"

Diff for: src/Lean/Meta/Tactic/FunIndCollect.lean

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/-
2+
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Joachim Breitner
5+
-/
6+
7+
prelude
8+
import Lean.Meta.Tactic.Util
9+
import Lean.Meta.Tactic.FunIndInfo
10+
11+
/-!
12+
Support for collecting function calls that could be used for `fun_induction` or `fun_cases`.
13+
Used by `fun_induction foo`, and the `Calls` structure is also used by `try?`.
14+
-/
15+
16+
namespace Lean.Meta.FunInd
17+
18+
structure Call where
19+
/- The full function call -/
20+
expr : Expr
21+
/- Used to avoid adding calls that differ only in dropped arguments -/
22+
relevantArgs : Expr
23+
deriving Hashable, BEq
24+
25+
structure SeenCalls where
26+
/-- the full calls -/
27+
calls : Array Expr
28+
/-- only relevant arguments -/
29+
seen : Std.HashSet (Array Expr)
30+
31+
instance : EmptyCollection SeenCalls where
32+
emptyCollection := ⟨#[], {}⟩
33+
34+
def SeenCalls.push (e : Expr) (declName : Name) (args : Array Expr) (calls : SeenCalls) :
35+
MetaM SeenCalls := do
36+
let some funIndInfo ← getFunIndInfo? (cases := false) declName | return calls
37+
if funIndInfo.params.size != args.size then return calls
38+
let mut keys := #[]
39+
for arg in args, kind in funIndInfo.params do
40+
if kind matches .target then
41+
if !arg.isFVar then return calls
42+
unless kind matches .dropped do
43+
keys := keys.push arg
44+
if calls.seen.contains keys then return calls
45+
return { calls := calls.calls.push e, seen := calls.seen.insert keys }
46+
47+
namespace Collector
48+
49+
abbrev M := ReaderT Name <| StateRefT SeenCalls MetaM
50+
51+
def saveFunInd (e : Expr) (declName : Name) (args : Array Expr) : M Unit := do
52+
set (← (← get).push e declName args)
53+
54+
def visitApp (e : Expr) (declName : Name) (args : Array Expr) : M Unit := do
55+
saveFunInd e declName args
56+
57+
unsafe abbrev Cache := PtrSet Expr
58+
59+
unsafe def visit (e : Expr) : StateRefT Cache M Unit := do
60+
unless (← get).contains e do
61+
modify fun s => s.insert e
62+
match e with
63+
| .const _ _ => pure ()
64+
| .forallE _ d b _ => visit d; visit b
65+
| .lam _ d b _ => visit d; visit b
66+
| .mdata _ b => visit b
67+
| .letE _ t v b _ => visit t; visit v; visit b
68+
| .app .. => e.withApp fun f args => do
69+
if let .const declName _ := f then
70+
if declName = (← read) then
71+
unless e.hasLooseBVars do -- TODO: We can allow them in `.dropped` arguments
72+
visitApp e declName args
73+
else
74+
visit f
75+
args.forM visit
76+
| .proj _ _ b => visit b
77+
| _ => return ()
78+
79+
unsafe def main (needle : Name) (mvarId : MVarId) : MetaM (Array Expr) := mvarId.withContext do
80+
let (_, s) ← go |>.run mkPtrSet |>.run needle |>.run {}
81+
return s.calls
82+
where
83+
go : StateRefT Cache M Unit := do
84+
for localDecl in (← getLCtx) do
85+
unless localDecl.isAuxDecl do
86+
if let some val := localDecl.value? then
87+
visit val
88+
else
89+
visit localDecl.type
90+
visit (← mvarId.getType)
91+
92+
end Collector
93+
94+
def collect (needle : Name) (mvarId : MVarId) : MetaM (Array Expr) := do
95+
unsafe Collector.main needle mvarId
96+
97+
end Lean.Meta.FunInd

Diff for: tests/lean/run/funInduction.lean

-7
Original file line numberDiff line numberDiff line change
@@ -168,13 +168,6 @@ error: Expected fully applied application of 'ackermann' with 2 arguments, but f
168168
example : P (ackermann n m) := by
169169
fun_induction ackermann n
170170

171-
/--
172-
error: Expected fully applied application of 'ackermann' with 2 arguments, but found 0 arguments
173-
-/
174-
#guard_msgs in
175-
example : P (ackermann n m) := by
176-
fun_induction ackermann
177-
178171
end Ex2
179172

180173
namespace Ex3

0 commit comments

Comments
 (0)