lean4-htt/src/Lean/Meta/Tactic/LibrarySearch.lean
Kim Morrison 975a81cdb8
feat: filter out deprecated lemmas from suggestions in exact?/rw? (#11918)
This PR filters deprecated lemmas from `exact?` and `rw?` suggestions.

Previously, both tactics would suggest deprecated lemmas, which could be
confusing for users since using the suggestion would trigger a
deprecation warning.

Now, lemmas marked with `@[deprecated]` are filtered out in the
`addImport` functions that populate the discrimination trees used by
these tactics.

**Example (before this PR):**
```lean
import Mathlib.Logic.Basic

example (h : ∃ n : Nat, n > 0) : True := by
  choose (n : Nat) (hn : n > 0 + 0) using h
  guard_hyp hn : n > 0  -- `rw?` would suggest `Eq.rec_eq_cast` which is deprecated
```

Zulip discussion:
https://leanprover.zulipchat.com/#narrow/channel/287929-mathlib4/topic/deprecated.20lemma.20from.20rw.3F/near/554106870

🤖 Prepared with Claude Code

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 23:45:04 +00:00

437 lines
16 KiB
Text
Raw 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.

/-
Copyright (c) 2021-2023 Gabriel Ebner and Lean FRO. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Gabriel Ebner, Joe Hendrix, Kim Morrison
-/
module
prelude
public import Lean.Meta.LazyDiscrTree
public import Lean.Meta.Tactic.SolveByElim
public import Lean.Meta.Tactic.Grind.Main
public import Lean.Util.Heartbeats
import Init.Grind.Util
import Init.Try
import Lean.Elab.Tactic.Basic
import Lean.Linter.Deprecated
public section
/-!
# Library search
This file defines tactics `exact?` and `apply?`,
(formerly known as `library_search`)
and a term elaborator `exact?%`
that tries to find a lemma
solving the current goal
(subgoals are solved using `solveByElim`).
```
example : x < x + 1 := exact?%
example : Nat := by exact?
```
-/
namespace Lean.Meta.LibrarySearch
builtin_initialize registerTraceClass `Tactic.librarySearch
builtin_initialize registerTraceClass `Tactic.librarySearch.lemmas
open SolveByElim
/--
A discharger that tries `grind` on the goal.
The proof is wrapped in `Grind.Marker` so that suggestions display `(by grind)`
instead of the complex grind proof term.
Returns `some []` if grind succeeds, `none` otherwise (leaving the goal as a subgoal).
-/
def grindDischarger (mvarId : MVarId) : MetaM (Option (List MVarId)) := do
try
-- Apply the marker wrapper, creating a subgoal for grind to solve
let type ← mvarId.getType
let u ← getLevel type
let markerExpr := mkApp (.const ``Lean.Grind.Marker [u]) type
let [subgoal] ← mvarId.apply markerExpr
| return none
-- Solve the subgoal with grind
let params ← Grind.mkDefaultParams {}
let result ← Grind.main subgoal params
if result.hasFailed then
return none
else
return some []
catch _ =>
return none
/--
A discharger that tries `try?` on the goal.
The proof is wrapped in `Try.Marker` so that suggestions display `(by try?)`
instead of the complex proof term.
Returns `some []` if try? succeeds, `none` otherwise (leaving the goal as a subgoal).
-/
def tryDischarger (mvarId : MVarId) : MetaM (Option (List MVarId)) := do
try
-- Apply the marker wrapper, creating a subgoal for try? to solve
let type ← mvarId.getType
let u ← getLevel type
let markerExpr := mkApp (.const ``Lean.Try.Marker [u]) type
let [subgoal] ← mvarId.apply markerExpr
| return none
-- Run try? via TacticM to solve the subgoal
-- We suppress the "Try this" messages since we're using try? as a discharger
let tacStx ← `(tactic| try?)
let remainingGoals ← Elab.Term.TermElabM.run' <| Elab.Tactic.run subgoal do
-- Suppress info messages from try?
let initialLog ← Core.getMessageLog
Elab.Tactic.evalTactic tacStx
Core.setMessageLog initialLog
if remainingGoals.isEmpty then
return some []
else
return none
catch _ =>
return none
/--
Wrapper for calling `Lean.Meta.SolveByElim.solveByElim with
appropriate arguments for library search.
If `grind` is true, `grind` will be used as a fallback discharger for subgoals
that cannot be closed by other means.
If `try?` is true, `try?` will be used as a fallback discharger (via grind internally).
-/
def solveByElim (required : List Expr) (exfalso : Bool) (goals : List MVarId)
(maxDepth : Nat) (grind : Bool := false) (try? : Bool := false) := do
let cfg : SolveByElimConfig :=
{ maxDepth, exfalso := exfalso, symm := true, commitIndependentGoals := true,
transparency := ← getTransparency,
-- `constructor` has been observed to significantly slow down `exact?` in Mathlib.
constructor := false }
-- Add grind or try? as a fallback discharger (tried after intro/constructor fail)
let cfg := if try? then cfg.withDischarge tryDischarger
else if grind then cfg.withDischarge grindDischarger
else cfg
let ⟨lemmas, ctx⟩ ← SolveByElim.mkAssumptionSet false false [] [] #[]
let cfg := if !required.isEmpty then cfg.requireUsingAll required else cfg
SolveByElim.solveByElim cfg lemmas ctx goals
/--
A "modifier" for a declaration.
* `none` indicates the original declaration,
* `mp` indicates that (possibly after binders) the declaration is an `↔`,
and we want to consider the forward direction,
* `mpr` similarly, but for the backward direction.
-/
inductive DeclMod
| /-- the original declaration -/ none
| /-- the forward direction of an `iff` -/ mp
| /-- the backward direction of an `iff` -/ mpr
deriving DecidableEq, Inhabited, Ord, Hashable
/--
LibrarySearch has an extension mechanism for replacing the function used
to find candidate lemmas.
-/
@[reducible, expose] def CandidateFinder := Expr → MetaM (Array (Name × DeclMod))
open LazyDiscrTree (InitEntry findMatches)
private def addImport (name : Name) (constInfo : ConstantInfo) :
MetaM (Array (InitEntry (Name × DeclMod))) := do
-- Don't report deprecated lemmas.
if Linter.isDeprecated (← getEnv) name then return #[]
-- Don't report lemmas from metaprogramming namespaces.
if name.isMetaprogramming then return #[] else
forallTelescope constInfo.type fun _ type => do
let e ← InitEntry.fromExpr type (name, DeclMod.none)
let a := #[e]
if e.key == .const ``Iff 2 then
let a := a.push (← e.mkSubEntry 0 (name, DeclMod.mp))
let a := a.push (← e.mkSubEntry 1 (name, DeclMod.mpr))
pure a
else
pure a
/-- Stores import discrimination tree. -/
private def LibSearchState := IO.Ref (Option (LazyDiscrTree (Name × DeclMod)))
private builtin_initialize defaultLibSearchState : IO.Ref (Option (LazyDiscrTree (Name × DeclMod))) ← do
IO.mkRef .none
private instance : Inhabited LibSearchState where
default := defaultLibSearchState
private builtin_initialize ext : EnvExtension LibSearchState ←
registerEnvExtension (IO.mkRef .none)
/--
We drop `.star` and `Eq * * *` from the discriminator trees because
they match too much.
-/
def droppedKeys : List (List LazyDiscrTree.Key) := [[.star], [.const `Eq 3, .star, .star, .star]]
/--
The maximum number of constants an individual task may perform.
The value was picked because it roughly corresponded to 50ms of work on the
machine this was developed on. Smaller numbers did not seem to improve
performance when importing Std and larger numbers (<10k) seemed to degrade
initialization performance.
-/
private def constantsPerImportTask : Nat := 6500
/-- Environment extension for caching star-indexed lemmas.
Used for fallback when primary search finds nothing for fvar-headed goals. -/
private builtin_initialize starLemmasExt : EnvExtension (IO.Ref (Option (Array (Name × DeclMod)))) ←
registerEnvExtension (IO.mkRef .none)
/-- Create function for finding relevant declarations.
Also captures dropped entries in starLemmasExt for fallback search. -/
def libSearchFindDecls (ty : Expr) : MetaM (Array (Name × DeclMod)) := do
let _ : Inhabited (IO.Ref (Option (Array (Name × DeclMod)))) := ⟨← IO.mkRef none⟩
let droppedRef := starLemmasExt.getState (←getEnv)
findMatches ext addImport
(droppedKeys := droppedKeys)
(constantsPerTask := constantsPerImportTask)
(droppedEntriesRef := some droppedRef)
ty
/-- Get star-indexed lemmas (lazily computed during tree initialization). -/
def getStarLemmas : MetaM (Array (Name × DeclMod)) := do
let _ : Inhabited (IO.Ref (Option (Array (Name × DeclMod)))) := ⟨← IO.mkRef none⟩
let ref := starLemmasExt.getState (←getEnv)
match ←ref.get with
| some lemmas => return lemmas
| none =>
-- If star lemmas aren't cached yet, trigger tree initialization by searching for a dummy type
-- This will populate starLemmasExt as a side effect
let _ ← libSearchFindDecls (mkConst ``True)
pure ((←ref.get).getD #[])
/--
Return an action that returns true when the remaining heartbeats is less
than the currently remaining heartbeats * leavePercent / 100.
-/
def mkHeartbeatCheck (leavePercent : Nat) : MetaM (MetaM Bool) := do
let maxHB ← getMaxHeartbeats
let hbThreshold := (← getRemainingHeartbeats) * leavePercent / 100
-- Return true if we should stop
pure $
if maxHB = 0 then
pure false
else do
return (← getRemainingHeartbeats) < hbThreshold
private def librarySearchEmoji : Except ε (Option α) → String
| .error _ => bombEmoji
| .ok (some _) => crossEmoji
| .ok none => checkEmoji
/--
Interleave x y interleaves the elements of x and y until one is empty and then returns
final elements in other list.
-/
def interleaveWith {α β γ} (f : αγ) (x : Array α) (g : β → γ) (y : Array β) : Array γ :=
Id.run do
let mut res := Array.mkEmpty (x.size + y.size)
let n := min x.size y.size
for h : i in *...n do
have p : i < min x.size y.size := Std.Rio.lt_upper_of_mem h
res := res.push (f x[i])
res := res.push (g y[i])
let last :=
if x.size > n then
(x.extract n x.size).map f
else
(y.extract n y.size).map g
pure $ res ++ last
/--
An exception ID that indicates further speculation on candidate lemmas should stop
and current results should be returned.
-/
private builtin_initialize abortSpeculationId : InternalExceptionId ←
registerInternalExceptionId `Lean.Meta.LibrarySearch.abortSpeculation
/--
Called to abort speculative execution in library search.
-/
def abortSpeculation [MonadExcept Exception m] : m α :=
throw (Exception.internal abortSpeculationId {})
/-- Returns true if this is an abort speculation exception. -/
def isAbortSpeculation : Exception → Bool
| .internal id _ => id == abortSpeculationId
| _ => false
section LibrarySearch
/--
A library search candidate using symmetry includes the goal to solve, the metavar
context for that goal, and the name and orientation of a rule to try using with goal.
-/
@[reducible, expose] def Candidate := (MVarId × MetavarContext) × (Name × DeclMod)
/--
Run `searchFn` on both the goal and `symm` applied to the goal.
-/
def librarySearchSymm (searchFn : CandidateFinder) (goal : MVarId) : MetaM (Array Candidate) := do
let tgt ← goal.getType
let l1 ← searchFn tgt
let coreMCtx ← getMCtx
let coreGoalCtx := (goal, coreMCtx)
if let some symmGoal ← observing? goal.applySymm then
let newType ← instantiateMVars (← symmGoal.getType)
let l2 ← searchFn newType
let symmMCtx ← getMCtx
let symmGoalCtx := (symmGoal, symmMCtx)
setMCtx coreMCtx
pure $ interleaveWith (coreGoalCtx, ·) l1 (symmGoalCtx, ·) l2
else
pure $ l1.map (coreGoalCtx, ·)
private def emoji (e : Except ε α) := if e.toBool then checkEmoji else crossEmoji
/-- Create lemma from name and mod. -/
def mkLibrarySearchLemma (lem : Name) (mod : DeclMod) : MetaM Expr := do
let lem ← mkConstWithFreshMVarLevels lem
match mod with
| .none => pure lem
| .mp => mapForallTelescope (fun e => mkAppM ``Iff.mp #[e]) lem
| .mpr => mapForallTelescope (fun e => mkAppM ``Iff.mpr #[e]) lem
private def isVar (e : Expr) : Bool :=
match e with
| .bvar _ => true
| .fvar _ => true
| .mvar _ => true
| _ => false
/--
Tries to apply the given lemma (with symmetry modifier) to the goal,
then tries to close subsequent goals using `solveByElim`.
If `solveByElim` succeeds, `[]` is returned as the list of new subgoals,
otherwise the full list of subgoals is returned.
-/
private def librarySearchLemma (cfg : ApplyConfig) (act : List MVarId → MetaM (List MVarId))
(allowFailure : MVarId → MetaM Bool) (cand : Candidate) : MetaM (List MVarId) := do
let ((goal, mctx), (name, mod)) := cand
let ppMod (mod : DeclMod) : MessageData :=
match mod with | .none => "" | .mp => " with mp" | .mpr => " with mpr"
withTraceNode `Tactic.librarySearch (return m!"{emoji ·} trying {name}{ppMod mod} ") do
setMCtx mctx
let lem ← mkLibrarySearchLemma name mod
let newGoals ← goal.apply lem cfg
try
act newGoals
catch _ =>
if ← allowFailure goal then
pure newGoals
else
failure
/--
Sequentially invokes a tactic `act` on each value in candidates on the current state.
The tactic `act` should return a list of meta-variables that still need to be resolved.
If this list is empty, then no variables remain to be solved, and `tryOnEach` returns
`none` with the environment set so each goal is resolved (unless `collectAll` is true,
in which case it continues searching and collects complete solutions in the array).
If the action throws an internal exception with the `abortSpeculationId` id then
further computation is stopped and intermediate results returned. If any other
exception is thrown, then it is silently discarded.
-/
def tryOnEach
(act : Candidate → MetaM (List MVarId))
(candidates : Array Candidate)
(collectAll : Bool := false) :
MetaM (Option (Array (List MVarId × MetavarContext))) := do
let mut a := #[]
let s ← saveState
for c in candidates do
match ← (tryCatch (Except.ok <$> act c) (pure ∘ Except.error)) with
| .error e =>
restoreState s
if isAbortSpeculation e then
break
| .ok remaining =>
if remaining.isEmpty && !collectAll then
return none
let ctx ← getMCtx
restoreState s
a := a.push (remaining, ctx)
return (.some a)
private def librarySearch' (goal : MVarId)
(tactic : List MVarId → MetaM (List MVarId))
(allowFailure : MVarId → MetaM Bool)
(leavePercentHeartbeats : Nat)
(includeStar : Bool := true)
(collectAll : Bool := false) :
MetaM (Option (Array (List MVarId × MetavarContext))) := do
withTraceNode `Tactic.librarySearch (return m!"{librarySearchEmoji ·} {← goal.getType}") do
profileitM Exception "librarySearch" (← getOptions) do
let cfg : ApplyConfig := { allowSynthFailures := true }
let shouldAbort ← mkHeartbeatCheck leavePercentHeartbeats
let act := fun cand => do
if ←shouldAbort then
abortSpeculation
librarySearchLemma cfg tactic allowFailure cand
-- First pass: search with droppedKeys (excludes star-indexed lemmas)
let candidates ← librarySearchSymm libSearchFindDecls goal
match ← tryOnEach act candidates collectAll with
| none => return none -- Found a complete solution (only when collectAll = false)
| some results =>
-- Only do star fallback if:
-- 1. No complete solutions from primary search (when collectAll, check for empty remaining)
-- 2. includeStar is true
-- When collectAll = false, any result means we should skip star fallback (return partial).
-- When collectAll = true, we continue to star lemmas unless we have a complete solution.
let hasCompleteSolution := results.any (·.1.isEmpty)
if hasCompleteSolution || (!collectAll && !results.isEmpty) || !includeStar then
return some results
-- Second pass: try star-indexed lemmas (those with [*] or [Eq,*,*,*] keys)
-- No need for librarySearchSymm since getStarLemmas ignores the goal type
let starLemmas ← getStarLemmas
if starLemmas.isEmpty then return some results
let mctx ← getMCtx
let starCandidates := starLemmas.map ((goal, mctx), ·)
match ← tryOnEach act starCandidates collectAll with
| none => return none -- Found complete solution from star lemmas
| some starResults => return some (results ++ starResults)
/--
Tries to solve the goal by applying a library lemma
then calling `tactic` on the resulting goals.
Typically here `tactic` is `solveByElim`.
If it successfully closes the goal, returns `none` (unless `collectAll` is true).
Otherwise, it returns `some a`, where `a : Array (List MVarId × MetavarContext)`,
with an entry for each library lemma which was successfully applied,
containing a list of the subsidiary goals, and the metavariable context after the application.
When `collectAll` is true, complete solutions (with empty remaining goals) are also included
in the array instead of returning early.
(Always succeeds, and the metavariable context stored in the monad is reverted,
unless the goal was completely solved.)
(Note that if `solveByElim` solves some but not all subsidiary goals,
this is not currently tracked.)
-/
def librarySearch (goal : MVarId)
(tactic : List MVarId → MetaM (List MVarId) :=
fun g => solveByElim [] (maxDepth := 6) (exfalso := false) g)
(allowFailure : MVarId → MetaM Bool := fun _ => pure true)
(leavePercentHeartbeats : Nat := 10)
(includeStar : Bool := true)
(collectAll : Bool := false) :
MetaM (Option (Array (List MVarId × MetavarContext))) := do
librarySearch' goal tactic allowFailure leavePercentHeartbeats includeStar collectAll
end LibrarySearch
end Lean.Meta.LibrarySearch