lean4-htt/src/Lean/Elab/Tactic/Simp.lean
Leonardo de Moura 29545dcf10
feat: do not dsimp instances (#12195)
This PR ensures `dsimp` does not "simplify" instances by default. The
old behavior can be retrieved by using
```
set_option backward.dsimp.instances true
```
Applying `dsimp` to instances creates non-standard instances, and this
creates all sorts of problems in Mathlib.
This modification is similar to
```
set_option backward.dsimp.proofs true
```

---------

Co-authored-by: Kim Morrison <kim@tqft.net>
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-29 05:25:01 +00:00

762 lines
31 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) 2020 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
module
prelude
public import Lean.Meta.Tactic.Simp
public import Lean.Meta.Tactic.Simp.LoopProtection
public import Lean.Elab.BuiltinNotation
public import Lean.Elab.Tactic.Location
public section
namespace Lean.Elab.Tactic
open Meta
open TSyntax.Compat
declare_config_elab elabSimpConfigCore Meta.Simp.Config
declare_config_elab elabSimpConfigCtxCore Meta.Simp.ConfigCtx
declare_config_elab elabDSimpConfigCore Meta.DSimp.Config
inductive SimpKind where
| simp
| simpAll
| dsimp
deriving Inhabited, BEq
/--
Implement a `simp` discharge function using the given tactic syntax code.
Recall that `simp` dischargers are in `SimpM` which does not have access to `Term.State`.
We need access to `Term.State` to store messages and update the info tree.
Thus, we create an `IO.ref` to track these changes at `Term.State` when we execute `tacticCode`.
We must set this reference with the current `Term.State` before we execute `simp` using the
generated `Simp.Discharge`. -/
def tacticToDischarge (tacticCode : Syntax) : TacticM (IO.Ref Term.State × Simp.Discharge) := do
let tacticCode ← `(tactic| try ($tacticCode:tacticSeq))
let ref ← IO.mkRef (← getThe Term.State)
let ctx ← readThe Term.Context
let disch : Simp.Discharge := fun e => do
let mvar ← mkFreshExprSyntheticOpaqueMVar e `simp.discharger
let s ← ref.get
let runTac? : TermElabM (Option Expr) :=
try
/- We must only save messages and info tree changes. Recall that `simp` uses temporary metavariables (`withNewMCtxDepth`).
So, we must not save references to them at `Term.State`.
-/
Term.withoutModifyingElabMetaStateWithInfo do
Term.withSynthesize (postpone := .no) do
Term.runTactic (report := false) mvar.mvarId! tacticCode .term
let result ← instantiateMVars mvar
if result.hasExprMVar then
return none
else
return some result
catch _ =>
return none
let (result?, s) ← liftM (m := MetaM) <| Term.TermElabM.run runTac? ctx s
ref.set s
return result?
return (ref, disch)
inductive Simp.DischargeWrapper where
| default
| custom (ref : IO.Ref Term.State) (discharge : Simp.Discharge)
def Simp.DischargeWrapper.with (w : Simp.DischargeWrapper) (x : Option Simp.Discharge → TacticM α) : TacticM α := do
match w with
| default => x none
| custom ref d =>
ref.set (← getThe Term.State)
try
x d
finally
set (← ref.get)
/-- Construct a `Simp.DischargeWrapper` from the `Syntax` for a `simp` discharger. -/
private def mkDischargeWrapper (optDischargeSyntax : Syntax) : TacticM Simp.DischargeWrapper := do
if optDischargeSyntax.isNone then
return Simp.DischargeWrapper.default
else
let (ref, d) ← tacticToDischarge optDischargeSyntax[0][3]
return Simp.DischargeWrapper.custom ref d
/-
`optConfig` is of the form `("(" "config" ":=" term ")")?`
-/
def elabSimpConfig (optConfig : Syntax) (kind : SimpKind) : TacticM Meta.Simp.Config := do
let cfg ← match kind with
| .simp => elabSimpConfigCore optConfig
| .simpAll => pure (← elabSimpConfigCtxCore optConfig).toConfig
| .dsimp => pure { (← elabDSimpConfigCore optConfig) with }
if Simp.backward.dsimp.instances.get (← getOptions) then
return { cfg with instances := true }
else
return cfg
inductive ResolveSimpIdResult where
| none
| expr (e : Expr)
| simproc (declName : Name)
/--
Recall that when we declare a `simp` attribute using `register_simp_attr`, we automatically
create a `simproc` attribute. However, if the user creates `simp` and `simproc` attributes
programmatically, then one of them may be missing. Moreover, when we write `simp [seval]`,
we want to retrieve both the simp and simproc sets. We want to hide from users that
`simp` and `simproc` sets are stored in different data-structures.
-/
| ext (ext₁? : Option SimpExtension) (ext₂? : Option Simp.SimprocExtension) (h : ext₁?.isSome || ext₂?.isSome)
private def resolveSimpIdTheorem? (simpArgTerm : Term) : TermElabM ResolveSimpIdResult := do
let resolveExt (n : Name) : TermElabM ResolveSimpIdResult := do
let ext₁? ← getSimpExtension? n
let ext₂? ← Simp.getSimprocExtension? n
if h : ext₁?.isSome || ext₂?.isSome then
return .ext ext₁? ext₂? h
else
return .none
match simpArgTerm with
| `($id:ident) =>
try
if let some e ← Term.resolveId? simpArgTerm (withInfo := true) then
if let some simprocDeclName ← isSimproc? e then
return .simproc simprocDeclName
else
return .expr e
else
let name := id.getId.eraseMacroScopes
if (← Simp.isBuiltinSimproc name) then
return .simproc name
else
resolveExt name
catch _ =>
resolveExt id.getId.eraseMacroScopes
| _ =>
if let some e ← Term.elabCDotFunctionAlias? simpArgTerm then
return .expr e
else
return .none
where
isSimproc? (e : Expr) : MetaM (Option Name) := do
let .const declName _ := e | return none
unless (← Simp.isSimproc declName) do return none
return some declName
/--
The result of elaborating a single `simp` argument
-/
inductive ElabSimpArgResult where
| addEntries (entries : Array SimpEntry)
| addSimproc («simproc» : Name) (post : Bool)
| addLetToUnfold (fvarId : FVarId)
| ext (ext₁? : Option SimpExtension) (ext₂? : Option Simp.SimprocExtension) (h : ext₁?.isSome || ext₂?.isSome)
| erase (toErase : Origin)
| eraseSimproc (toErase : Name)
| star
| none -- used for example when elaboration fails
def ElabSimpArgResult.simpTheorems : ElabSimpArgResult → Array SimpTheorem
| addEntries entries => Id.run do
let mut thms := #[]
for entry in entries do
if let .thm thm := entry then
thms := thms.push thm
return thms
| _ => #[]
private def elabDeclToUnfoldOrTheorem (config : Meta.ConfigWithKey) (id : Origin)
(e : Expr) (post : Bool) (inv : Bool) (kind : SimpKind) : MetaM ElabSimpArgResult := do
if e.isConst then
let declName := e.constName!
let info ← getConstVal declName
if (← isProp info.type) then
let thms ← mkSimpTheoremFromConst declName (post := post) (inv := inv)
return .addEntries <| thms.map (SimpEntry.thm ·)
else
if inv then
throwError m!"Invalid `←` modifier: `{.ofConstName declName}` is a declaration name to be unfolded"
++ .hint' m!"The simplifier cannot \"refold\" definitions by name. Use `rw` for this instead,
or use the `←` simp modifier with an equational lemma for `{.ofConstName declName}`."
if kind == .dsimp then
return .addEntries #[.toUnfold declName]
else
.addEntries <$> mkSimpEntryOfDeclToUnfold declName
else if e.isFVar then
let fvarId := e.fvarId!
let decl ← fvarId.getDecl
if (← isProp decl.type) then
let thms ← mkSimpTheoremFromExpr id #[] e (post := post) (inv := inv) (config := config)
return .addEntries <| thms.map (SimpEntry.thm ·)
else if !decl.isLet then
throwError "Invalid argument: Variable `{e}` is not a proposition or let-declaration"
else if inv then
throwError m!"Invalid `←` modifier: `{e}` is a let-declaration name to be unfolded"
++ .note "The simplifier cannot \"refold\" local declarations by name"
else
return .addLetToUnfold fvarId
else
let thms ← mkSimpTheoremFromExpr id #[] e (post := post) (inv := inv) (config := config)
return .addEntries <| thms.map (SimpEntry.thm ·)
private def elabSimpTheorem (config : Meta.ConfigWithKey) (id : Origin) (stx : Syntax)
(post : Bool) (inv : Bool) : TermElabM ElabSimpArgResult := do
let thm? ← Term.withoutModifyingElabMetaStateWithInfo <| withRef stx do
let e ← Term.elabTerm stx .none
Term.synthesizeSyntheticMVars (postpone := .no) (ignoreStuckTC := true)
let e ← instantiateMVars e
if e.hasSyntheticSorry then
return .none
let e := e.eta
if e.hasMVar then
let r ← abstractMVars e
return some (r.paramNames, r.expr)
else
return some (#[], e)
if let some (levelParams, proof) := thm? then
let thms ← mkSimpTheoremFromExpr id levelParams proof (post := post) (inv := inv) (config := config)
return .addEntries <| thms.map (SimpEntry.thm ·)
else
return .none
private def elabSimpArg (indexConfig : Meta.ConfigWithKey) (eraseLocal : Bool) (kind : SimpKind)
(arg : Syntax) : TacticM ElabSimpArgResult := withRef arg do
try
/-
syntax simpPre := "↓"
syntax simpPost := "↑"
syntax simpLemma := (simpPre <|> simpPost)? "← "? term
syntax simpErase := "-" ident
-/
if arg.getKind == ``Lean.Parser.Tactic.simpErase then
let fvar? ← if eraseLocal then Term.isLocalIdent? arg[1] else pure none
if let some fvar := fvar? then
-- We use `eraseCore` because the simp theorem for the hypothesis was not added yet
return .erase (.fvar fvar.fvarId!)
else
let id := arg[1]
if let .ok declName ← observing (realizeGlobalConstNoOverloadWithInfo id) then
if (← Simp.isSimproc declName) then
return .eraseSimproc declName
else
return .erase (.decl declName)
else
-- If `id` could not be resolved, we should check whether it is a builtin simproc.
-- before returning error.
let name := id.getId.eraseMacroScopes
if (← Simp.isBuiltinSimproc name) then
return .eraseSimproc name
else
throwUnknownConstantAt id name
else if arg.getKind == ``Lean.Parser.Tactic.simpLemma then
let post :=
if arg[0].isNone then
true
else
arg[0][0].getKind == ``Parser.Tactic.simpPost
let inv := !arg[1].isNone
let term := arg[2]
match (← resolveSimpIdTheorem? term) with
| .expr e =>
let name ← mkFreshId
elabDeclToUnfoldOrTheorem indexConfig (.stx name arg) e post inv kind
| .simproc declName =>
return .addSimproc declName post
| .ext ext₁? ext₂? h =>
return .ext ext₁? ext₂? h
| .none =>
let name ← mkFreshId
elabSimpTheorem indexConfig (.stx name arg) term post inv
else if arg.getKind == ``Lean.Parser.Tactic.simpStar then
return .star
else
throwUnsupportedSyntax
catch ex =>
if (← read).recover then
logException ex
return .none
else
throw ex
/--
The result of elaborating a full array of simp arguments and applying them to the simp context.
-/
structure ElabSimpArgsResult where
ctx : Simp.Context
simprocs : Simp.SimprocsArray
/-- The elaborated simp arguments with syntax -/
simpArgs : Array (Syntax × ElabSimpArgResult)
/-- Implements the effect of the `*` attribute. -/
private def applyStarArg (ctx : Simp.Context) : MetaM Simp.Context := do
let mut simpTheorems := ctx.simpTheorems
/-
When using `zetaDelta := false`, we do not expand let-declarations when using `[*]`.
Users must explicitly include it in the list.
-/
let hs ← getPropHyps
for h in hs do
unless simpTheorems.isErased (.fvar h) do
simpTheorems ← simpTheorems.addTheorem (.fvar h) (← h.getDecl).toExpr (config := ctx.indexConfig)
return ctx.setSimpTheorems simpTheorems
/--
Elaborate extra simp theorems provided to `simp`. `stx` is of the form `"[" simpTheorem,* "]"`
If `eraseLocal == true`, then we consider local declarations when resolving names for erased theorems (`- id`),
this option only makes sense for `simp_all` or `*` is used.
When `recover := true`, try to recover from errors as much as possible so that users keep seeing
the current goal.
-/
def elabSimpArgs (stx : Syntax) (ctx : Simp.Context) (simprocs : Simp.SimprocsArray) (eraseLocal : Bool)
(kind : SimpKind) (ignoreStarArg := false) : TacticM ElabSimpArgsResult := do
if stx.isNone then
return { ctx, simprocs, simpArgs := #[] }
else
/-
syntax simpPre := "↓"
syntax simpPost := "↑"
syntax simpLemma := (simpPre <|> simpPost)? "← "? term
syntax simpErase := "-" ident
-/
let go := withMainContext do
let zetaDeltaSet ← toZetaDeltaSet stx ctx
withTrackingZetaDeltaSet zetaDeltaSet do
let mut starArg := false -- only after * we can erase local declarations
let mut args : Array (Syntax × ElabSimpArgResult) := #[]
for argStx in stx[1].getSepArgs do
let arg ← elabSimpArg ctx.indexConfig (eraseLocal || starArg) kind argStx
starArg := !ignoreStarArg && (starArg || arg matches .star)
args := args.push (argStx, arg)
let mut thmsArray := ctx.simpTheorems
let mut thms := thmsArray[0]!
let mut simprocs := simprocs
for (ref, arg) in args do
match arg with
| .addEntries entries =>
for entry in entries do
thms := thms.uneraseSimpEntry entry
thms := thms.addSimpEntry entry
| .addLetToUnfold fvarId =>
thms := thms.addLetDeclToUnfold fvarId
| .addSimproc declName post =>
simprocs ← simprocs.add declName post
| .erase origin =>
-- `thms.erase` checks if the erasure is effective.
-- We do not want this check for local hypotheses (they are added later based on `starArg`)
if origin matches .fvar _ then
thms := thms.eraseCore origin
-- Nor for decls to unfold when we do auto unfolding
else if ctx.config.autoUnfold then
thms := thms.eraseCore origin
else
thms ← withRef ref <| thms.erase origin
| .eraseSimproc name =>
simprocs := simprocs.erase name
| .ext simpExt? simprocExt? _ =>
if let some simpExt := simpExt? then
thmsArray := thmsArray.push (← simpExt.getTheorems)
if let some simprocExt := simprocExt? then
simprocs := simprocs.push (← simprocExt.getSimprocs)
| .star => pure ()
| .none => pure ()
let mut ctx := ctx.setZetaDeltaSet zetaDeltaSet (← getZetaDeltaFVarIds)
ctx := ctx.setSimpTheorems (thmsArray.set! 0 thms)
if !ignoreStarArg && starArg then
ctx ← applyStarArg ctx
return { ctx, simprocs, simpArgs := args}
-- If recovery is disabled, then we want simp argument elaboration failures to be exceptions.
-- This affects `addSimpTheorem`.
if (← read).recover then
go
else
Term.withoutErrToSorry go
where
/-- If `zetaDelta := false`, create a `FVarId` set with all local let declarations in the `simp` argument list. -/
toZetaDeltaSet (stx : Syntax) (ctx : Simp.Context) : TacticM FVarIdSet := do
if ctx.config.zetaDelta then return {}
Term.withoutCheckDeprecated do -- We do not want to report deprecated constants in the first pass
let mut s : FVarIdSet := {}
for arg in stx[1].getSepArgs do
if arg.getKind == ``Lean.Parser.Tactic.simpLemma then
if arg[0].isNone && arg[1].isNone then
let term := arg[2]
let .expr (.fvar fvarId) ← resolveSimpIdTheorem? term | pure ()
if (← fvarId.getDecl).isLet then
s := s.insert fvarId
return s
/-- Position for the `[..]` child syntax in the `simp` tactic. -/
def simpParamsPos := 4
/-- Position for the `only` child syntax in the `simp` tactic. -/
def simpOnlyPos := 3
def isSimpOnly (stx : TSyntax `tactic) : Bool :=
stx.raw.getKind == ``Parser.Tactic.simp && !stx.raw[simpOnlyPos].isNone
def getSimpParams (stx : TSyntax `tactic) : Array Syntax :=
stx.raw[simpParamsPos][1].getSepArgs
def setSimpParams (stx : TSyntax `tactic) (params : Array Syntax) : TSyntax `tactic :=
if params.isEmpty then
⟨stx.raw.setArg simpParamsPos (mkNullNode)⟩
else
let paramsStx := #[mkAtom "[", (mkAtom ",").mkSep params, mkAtom "]"]
⟨stx.raw.setArg simpParamsPos (mkNullNode paramsStx)⟩
@[inline] def simpOnlyBuiltins : List Name := [``eq_self, ``iff_self]
structure MkSimpContextResult where
ctx : Simp.Context
simprocs : Simp.SimprocsArray
dischargeWrapper : Simp.DischargeWrapper
/-- The elaborated simp arguments with syntax -/
simpArgs : Array (Syntax × ElabSimpArgResult) := #[]
/-- Add all definitions from the current file to unfold. -/
def elabSimpLocals (thms : SimpTheorems) (kind : SimpKind) : MetaM SimpTheorems := do
let env ← getEnv
let mut thms := thms
for (name, ci) in env.constants.map₂.toList do
-- Skip internal details, but allow private names (which are accessible from current module)
if name.isInternalDetail && !isPrivateName name then continue
if (← Meta.isInstance name) then continue
match ci with
| .defnInfo _ =>
-- Definitions are added to unfold
try
if kind == .dsimp then
thms := thms.addDeclToUnfoldCore name
else
let entries ← mkSimpEntryOfDeclToUnfold name
for entry in entries do
thms := thms.addSimpEntry entry
catch _ => pure () -- Skip definitions that can't be added
| _ => continue
return thms
/--
Create the `Simp.Context` for the `simp`, `dsimp`, and `simp_all` tactics.
If `kind != SimpKind.simp`, the `discharge` option must be `none`
TODO: generate error message if non `rfl` theorems are provided as arguments to `dsimp`.
The argument `simpTheorems` defaults to `getSimpTheorems`,
but allows overriding with an arbitrary mechanism to choose
the simp theorems besides those specified in the syntax.
Note that if the syntax includes `simp only`, the `simpTheorems` argument is ignored.
-/
def mkSimpContext (stx : Syntax) (eraseLocal : Bool) (kind := SimpKind.simp)
(ignoreStarArg : Bool := false) (simpTheorems : CoreM SimpTheorems := getSimpTheorems) :
TacticM MkSimpContextResult := do
if !stx[2].isNone then
if kind == SimpKind.simpAll then
throwError "Tactic `simp_all` does not support the `discharger` option"
if kind == SimpKind.dsimp then
throwError "Tactic `dsimp` does not support the `discharger' option"
let dischargeWrapper ← mkDischargeWrapper stx[2]
let simpOnly := !stx[simpOnlyPos].isNone
let mut simpTheorems ← if simpOnly then
simpOnlyBuiltins.foldlM (·.addConst ·) ({} : SimpTheorems)
else
simpTheorems
let simprocs ← if simpOnly then pure {} else Simp.getSimprocs
let congrTheorems ← getSimpCongrTheorems
let config ← elabSimpConfig stx[1] (kind := kind)
-- Add local definitions if +locals is enabled
if config.locals then
simpTheorems ← elabSimpLocals simpTheorems kind
let ctx ← Simp.mkContext
(config := config)
(simpTheorems := #[simpTheorems])
congrTheorems
let r ← elabSimpArgs stx[4] (eraseLocal := eraseLocal) (kind := kind) (simprocs := #[simprocs]) (ignoreStarArg := ignoreStarArg) ctx
return { r with dischargeWrapper }
/--
Runs the given action.
If it throws a maxRecDepth exception (nested or not), run the loop checking.
If it does not throw, run the loop checking only if explicitly enabled.
-/
@[inline] def withLoopChecking [Monad m] [MonadExcept Exception m] [MonadRuntimeException m] [MonadLiftT MetaM m]
(r : MkSimpContextResult) (k : m α) : m α := do
-- We use tryCatchRuntimeEx here, normal try-catch would swallow the trace messages
-- from diagnostics
let x ← tryCatchRuntimeEx do
k
fun e => do
if e.isMaxRecDepth || e.toMessageData.hasTag (· = `nested.runtime.maxRecDepth) then
go (force := true)
throw e
go (force := false)
pure x
where
go force : m Unit := liftMetaM do
let { ctx, simprocs, dischargeWrapper := _, simpArgs } := r
for (ref, arg) in simpArgs do
for thm in arg.simpTheorems do
withRef ref do
Simp.checkLoops (force := force) ctx (methods := Simp.mkDefaultMethodsCore simprocs) thm
register_builtin_option tactic.simp.trace : Bool := {
defValue := false
descr := "When tracing is enabled, calls to `simp` or `dsimp` will print an equivalent `simp only` call."
}
/--
If `stx` is the syntax of a `simp`, `simp_all` or `dsimp` tactic invocation, and
`usedSimps` is the set of simp lemmas used by this invocation, then `mkSimpOnly`
creates the syntax of an equivalent `simp only`, `simp_all only` or `dsimp only`
invocation.
-/
def mkSimpOnly (stx : Syntax) (usedSimps : Simp.UsedSimps) : MetaM Syntax := do
let isSimpAll := stx.isOfKind ``Parser.Tactic.simpAll
let mut stx := stx
if stx[3].isNone then
stx := stx.setArg 3 (mkNullNode #[mkAtom "only"])
let mut args := #[]
let mut localsOrStar := some #[]
let lctx ← getLCtx
let env ← getEnv
for thm in usedSimps.toArray do
match thm with
| .decl declName post inv => -- global definitions in the environment
if env.contains declName
&& (inv || !simpOnlyBuiltins.contains declName)
&& !Match.isMatchEqnTheorem env declName then
let decl : Term ← `($(mkIdent (← unresolveNameGlobalAvoidingLocals declName)):ident)
let arg ← match post, inv with
| true, true => `(Parser.Tactic.simpLemma| ← $decl:term)
| true, false => `(Parser.Tactic.simpLemma| $decl:term)
| false, true => `(Parser.Tactic.simpLemma| ↓ ← $decl:term)
| false, false => `(Parser.Tactic.simpLemma| ↓ $decl:term)
args := args.push arg
else if (← Simp.isBuiltinSimproc declName) then
let decl := mkIdent declName
let arg ← match post with
| true => `(Parser.Tactic.simpLemma| $decl:term)
| false => `(Parser.Tactic.simpLemma| ↓ $decl:term)
args := args.push arg
| .fvar fvarId =>
-- local hypotheses in the context
if let some ldecl := lctx.find? fvarId then
-- `simp_all` always uses all propositional hypotheses.
-- So `simp_all only [x]`, only makes sense if `ldecl` is a let-variable.
if isSimpAll && !ldecl.hasValue then
continue
localsOrStar := localsOrStar.bind fun locals =>
if !ldecl.userName.isInaccessibleUserName && !ldecl.userName.hasMacroScopes &&
(lctx.findFromUserName? ldecl.userName).get!.fvarId == ldecl.fvarId then
some (locals.push ldecl.userName)
else
none
-- Note: the `if let` can fail for `simp (config := {contextual := true})` when
-- rewriting with a variable that was introduced in a scope. In that case we just ignore.
| .stx _ thmStx => -- simp theorems provided in the local invocation
args := args.push thmStx
| .other _ => -- Ignore "special" simp lemmas such as constructed by `simp_all`.
pure () -- We can't display them anyway.
if let some locals := localsOrStar then
args := args ++ (← locals.mapM fun id => `(Parser.Tactic.simpLemma| $(mkIdent id):ident))
else
args := args.push (← `(Parser.Tactic.simpStar| *))
return setSimpParams stx args
def traceSimpCall (stx : Syntax) (usedSimps : Simp.UsedSimps) : MetaM Unit := do
logInfoAt stx[0] m!"Try this: {← mkSimpOnly stx usedSimps}"
register_builtin_option linter.unusedSimpArgs : Bool := {
defValue := true,
descr := "enable the linter that warns when explicit `simp` arguments are unused.\n\
\n\
The linter suggests removing the unused arguments. This hint may not be correct in the case \
that `simp [← thm]` is given, when `thm` has the `@[simp]` attribute, and it is relevant that \
`thm` it disabled (which is a side-effect of specifying `← thm`). In that case, replace \
it with `simp [- thm]`.\n\
\n\
When one `simp` invocation is run multiple times (e.g. `all_goals simp [thm]`), it warns \
about simp arguments that are unused in all invocations. For this reason, the linter \
does not warn about uses of `simp` inside a macro, as there it is usually not possible to see \
all invocations."
}
structure UnusedSimpArgsInfo where
mask : Array Bool
deriving TypeName
def pushUnusedSimpArgsInfo [Monad m] [MonadInfoTree m] (simpStx : Syntax) (mask : Array Bool) : m Unit := do
pushInfoLeaf <| .ofCustomInfo {
stx := simpStx
value := .mk { mask := mask : UnusedSimpArgsInfo } }
/--
Checks the simp arguments for unused ones, and stores a bitmask of unused ones in the info tree,
to be picked up by the linter.
(This indirection is necessary because the same `simp` syntax may be executed multiple times,
and different simp arguments may be used in each step.)
-/
def warnUnusedSimpArgs (simpArgs : Array (Syntax × ElabSimpArgResult)) (usedSimps : Simp.UsedSimps) : MetaM Unit := do
if simpArgs.isEmpty then return
let mut mask : Array Bool := #[]
for h : i in *...simpArgs.size do
let (ref, arg) := simpArgs[i]
let used ←
match arg with
| .addEntries entries =>
entries.anyM fun
| .thm thm => return usedSimps.contains (← usedThmIdOfSimpTheorem thm)
| .toUnfold declName => return usedSimps.contains (.decl declName)
| .toUnfoldThms _declName thms => return thms.any (usedSimps.contains <| .decl ·)
| .addSimproc declName post =>
pure <| usedSimps.contains (.decl declName post)
| .addLetToUnfold fvarId =>
pure <| usedSimps.contains (.fvar fvarId)
| .erase _
| .eraseSimproc _
| .ext _ _ _
| .star
| .none
=> pure true -- not supported yet
mask := mask.push used
pushUnusedSimpArgsInfo (← getRef) mask
where
/--
For equational theorems, usedTheorems record the declaration name. So if the user
specified `foo.eq_1`, we get `foo` in `usedTheorems`, but we still want to mark
`foo.eq_1` as used.
(cf. `recordSimpTheorem`)
This may lead to unused, explicitly given `foo.eq_1` to not be warned about. Ok for now,
eventually `recordSimpTheorem` could record the actual theorem, and the logic for
treating `foo.eq_1` as `foo` be moved to `SimpTrace.lean`
-/
usedThmIdOfSimpTheorem (thm : SimpTheorem) : MetaM Origin := do
let thmId := thm.origin
if let .decl declName post false := thmId then
if let some declName ← isEqnThm? declName then
return (Origin.decl declName post false)
return thmId
/--
`simpLocation ctx discharge? varIdToLemmaId loc`
runs the simplifier at locations specified by `loc`,
using the simp theorems collected in `ctx`
optionally running a discharger specified in `discharge?` on generated subgoals.
Its primary use is as the implementation of the
`simp [...] at ...` and `simp only [...] at ...` syntaxes,
but can also be used by other tactics when a `Syntax` is not available.
For many tactics other than the simplifier,
one should use the `withLocation` tactic combinator
when working with a `location`.
-/
def simpLocation (ctx : Simp.Context) (simprocs : Simp.SimprocsArray) (discharge? : Option Simp.Discharge := none) (loc : Location) : TacticM Simp.Stats := do
match loc with
| Location.targets hyps simplifyTarget =>
withMainContext do
let fvarIds ← getFVarIds hyps
go fvarIds simplifyTarget
| Location.wildcard =>
withMainContext do
go (← (← getMainGoal).getNondepPropHyps) (simplifyTarget := true)
where
go (fvarIdsToSimp : Array FVarId) (simplifyTarget : Bool) : TacticM Simp.Stats := do
let mvarId ← getMainGoal
let (result?, stats) ← simpGoal mvarId ctx (simprocs := simprocs) (simplifyTarget := simplifyTarget) (discharge? := discharge?) (fvarIdsToSimp := fvarIdsToSimp)
match result? with
| none => replaceMainGoal []
| some (_, mvarId) => replaceMainGoal [mvarId]
return stats
def withSimpDiagnostics (x : TacticM Simp.Diagnostics) : TacticM Unit := do
let stats ← x
Simp.reportDiag stats
/-
"simp" optConfig (discharger)? (" only")? (" [" ((simpStar <|> simpErase <|> simpLemma),*,?) "]")?
(location)?
-/
@[builtin_tactic Lean.Parser.Tactic.simp] def evalSimp : Tactic := fun stx => withMainContext do withSimpDiagnostics do
let r@{ ctx, simprocs, dischargeWrapper, simpArgs } ← mkSimpContext stx (eraseLocal := false)
if ctx.config.suggestions then
throwError "+suggestions requires using simp? instead of simp"
let stats ← dischargeWrapper.with fun discharge? =>
withLoopChecking r do
simpLocation ctx simprocs discharge? (expandOptLocation stx[5])
if tactic.simp.trace.get (← getOptions) then
traceSimpCall stx stats.usedTheorems
else if linter.unusedSimpArgs.get (← getOptions) then
withRef stx do
warnUnusedSimpArgs simpArgs stats.usedTheorems
return stats.diag
@[builtin_tactic Lean.Parser.Tactic.simpAll] def evalSimpAll : Tactic := fun stx => withMainContext do withSimpDiagnostics do
let r@{ ctx, simprocs, dischargeWrapper := _, simpArgs } ← mkSimpContext stx (eraseLocal := true) (kind := .simpAll) (ignoreStarArg := true)
if ctx.config.suggestions then
throwError "+suggestions requires using simp_all? instead of simp_all"
let (result?, stats) ←
withLoopChecking r do
simpAll (← getMainGoal) ctx (simprocs := simprocs)
match result? with
| none => replaceMainGoal []
| some mvarId => replaceMainGoal [mvarId]
if tactic.simp.trace.get (← getOptions) then
traceSimpCall stx stats.usedTheorems
else if linter.unusedSimpArgs.get (← getOptions) then
withRef stx do
warnUnusedSimpArgs simpArgs stats.usedTheorems
return stats.diag
def dsimpLocation (ctx : Simp.Context) (simprocs : Simp.SimprocsArray) (loc : Location) : TacticM Unit := do
match loc with
| Location.targets hyps simplifyTarget =>
withMainContext do
let fvarIds ← getFVarIds hyps
go fvarIds simplifyTarget
| Location.wildcard =>
withMainContext do
go (← (← getMainGoal).getNondepPropHyps) (simplifyTarget := true)
where
go (fvarIdsToSimp : Array FVarId) (simplifyTarget : Bool) : TacticM Unit := withSimpDiagnostics do
let mvarId ← getMainGoal
let (result?, stats) ← dsimpGoal mvarId ctx simprocs (simplifyTarget := simplifyTarget) (fvarIdsToSimp := fvarIdsToSimp)
match result? with
| none => replaceMainGoal []
| some mvarId => replaceMainGoal [mvarId]
if tactic.simp.trace.get (← getOptions) then
mvarId.withContext <| traceSimpCall (← getRef) stats.usedTheorems
return stats.diag
@[builtin_tactic Lean.Parser.Tactic.dsimp] def evalDSimp : Tactic := fun stx => do
let { ctx, simprocs, .. } ← withMainContext <| mkSimpContext stx (eraseLocal := false) (kind := .dsimp)
dsimpLocation ctx simprocs (expandOptLocation stx[5])
end Lean.Elab.Tactic
/-!
The following parsers for `simp` arguments are not actually used at present in the
implementation of `simp` above, but are useful for further tactics which need
to parse `simp` arguments.
-/
namespace Lean.Parser.Tactic
/-- Extract the arguments from a `simpArgs` syntax as an array of syntaxes -/
def getSimpArgs? : Syntax → Option (Array Syntax)
| `(simpArgs| [$args,*]) => pure args.getElems
| _ => none
/-- Extract the arguments from a `dsimpArgs` syntax as an array of syntaxes -/
def getDSimpArgs? : Syntax → Option (Array Syntax)
| `(dsimpArgs| [$args,*]) => pure args.getElems
| _ => none
end Lean.Parser.Tactic