lean4-htt/src/Lean/Meta/Tactic/Grind/EMatchTheorem.lean
Sebastian Ullrich 62c6edffef
feat: do not export theorem bodies (#8090)
This PR adjusts the experimental module system to elide theorem bodies
(i.e. proofs) from being imported into other modules.
2025-04-25 20:22:32 +00:00

932 lines
38 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) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Leonardo de Moura
-/
prelude
import Init.Grind.Util
import Init.Grind.Tactics
import Lean.HeadIndex
import Lean.PrettyPrinter
import Lean.Util.FoldConsts
import Lean.Util.CollectFVars
import Lean.Meta.Basic
import Lean.Meta.InferType
import Lean.Meta.Eqns
import Lean.Meta.Tactic.Grind.Util
namespace Lean.Meta.Grind
def mkOffsetPattern (pat : Expr) (k : Nat) : Expr :=
mkApp2 (mkConst ``Grind.offset) pat (mkRawNatLit k)
private def detectOffsets (pat : Expr) : MetaM Expr := do
let pre (e : Expr) := do
if e == pat then
-- We only consider nested offset patterns
return .continue e
else match e with
| .letE .. | .lam .. | .forallE .. => return .done e
| _ =>
let some (e, k) ← isOffset? e
| return .continue e
if k == 0 then return .continue e
return .continue <| mkOffsetPattern e k
Core.transform pat (pre := pre)
def isOffsetPattern? (pat : Expr) : Option (Expr × Nat) := Id.run do
let_expr Grind.offset pat k := pat | none
let .lit (.natVal k) := k | none
return some (pat, k)
def mkEqBwdPattern (u : List Level) (α : Expr) (lhs rhs : Expr) : Expr :=
mkApp3 (mkConst ``Grind.eqBwdPattern u) α lhs rhs
def isEqBwdPattern (e : Expr) : Bool :=
e.isAppOfArity ``Grind.eqBwdPattern 3
def isEqBwdPattern? (e : Expr) : Option (Expr × Expr) :=
let_expr Grind.eqBwdPattern _ lhs rhs := e
| none
some (lhs, rhs)
-- Configuration for the `grind` normalizer. We want both `zetaDelta` and `zeta`
private def normConfig : Grind.Config := {}
theorem normConfig_zeta : normConfig.zeta = true := rfl
theorem normConfig_zetaDelta : normConfig.zetaDelta = true := rfl
def preprocessPattern (pat : Expr) (normalizePattern := true) : MetaM Expr := do
let pat ← instantiateMVars pat
let pat ← unfoldReducible pat
let pat ← if normalizePattern then normalize pat normConfig else pure pat
let pat ← detectOffsets pat
let pat ← foldProjs pat
return pat
inductive Origin where
/-- A global declaration in the environment. -/
| decl (declName : Name)
/-- A local hypothesis. -/
| fvar (fvarId : FVarId)
/--
A proof term provided directly to a call to `grind` where `ref`
is the provided grind argument. The `id` is a unique identifier for the call.
-/
| stx (id : Name) (ref : Syntax)
/-- It is local, but we don't have a local hypothesis for it. -/
| local (id : Name)
deriving Inhabited, Repr, BEq
/-- A unique identifier corresponding to the origin. -/
def Origin.key : Origin → Name
| .decl declName => declName
| .fvar fvarId => fvarId.name
| .stx id _ => id
| .local id => id
def Origin.pp [Monad m] [MonadEnv m] [MonadError m] (o : Origin) : m MessageData := do
match o with
| .decl declName => return MessageData.ofConst (← mkConstWithLevelParams declName)
| .fvar fvarId => return mkFVar fvarId
| .stx _ ref => return ref
| .local id => return id
instance : BEq Origin where
beq a b := a.key == b.key
instance : Hashable Origin where
hash a := hash a.key
inductive EMatchTheoremKind where
| eqLhs | eqRhs | eqBoth | eqBwd | fwd | bwd | leftRight | rightLeft | default | user /- pattern specified using `grind_pattern` command -/
deriving Inhabited, BEq, Repr, Hashable
private def EMatchTheoremKind.toAttribute : EMatchTheoremKind → String
| .eqLhs => "[grind =]"
| .eqRhs => "[grind =_]"
| .eqBoth => "[grind _=_]"
| .eqBwd => "[grind ←=]"
| .fwd => "[grind →]"
| .bwd => "[grind ←]"
| .leftRight => "[grind =>]"
| .rightLeft => "[grind <=]"
| .default => "[grind]"
| .user => "[grind]"
private def EMatchTheoremKind.explainFailure : EMatchTheoremKind → String
| .eqLhs => "failed to find pattern in the left-hand side of the theorem's conclusion"
| .eqRhs => "failed to find pattern in the right-hand side of the theorem's conclusion"
| .eqBoth => unreachable! -- eqBoth is a macro
| .eqBwd => "failed to use theorem's conclusion as a pattern"
| .fwd => "failed to find patterns in the antecedents of the theorem"
| .bwd => "failed to find patterns in the theorem's conclusion"
| .leftRight => "failed to find patterns searching from left to right"
| .rightLeft => "failed to find patterns searching from right to left"
| .default => "failed to find patterns"
| .user => unreachable!
/-- A theorem for heuristic instantiation based on E-matching. -/
structure EMatchTheorem where
/--
It stores universe parameter names for universe polymorphic proofs.
Recall that it is non-empty only when we elaborate an expression provided by the user.
When `proof` is just a constant, we can use the universe parameter names stored in the declaration.
-/
levelParams : Array Name
proof : Expr
numParams : Nat
patterns : List Expr
/-- Contains all symbols used in `pattterns`. -/
symbols : List HeadIndex
origin : Origin
/-- The `kind` is used for generating the `patterns`. We save it here to implement `grind?`. -/
kind : EMatchTheoremKind
deriving Inhabited
/-- Set of E-matching theorems. -/
structure EMatchTheorems where
/-- The key is a symbol from `EMatchTheorem.symbols`. -/
private smap : PHashMap Name (List EMatchTheorem) := {}
/-- Set of theorem ids that have been inserted using `insert`. -/
private origins : PHashSet Origin := {}
/-- Theorems that have been marked as erased -/
private erased : PHashSet Origin := {}
/-- Mapping from origin to E-matching theorems associated with this origin. -/
private omap : PHashMap Origin (List EMatchTheorem) := {}
deriving Inhabited
/--
Inserts a `thm` with symbols `[s_1, ..., s_n]` to `s`.
We add `s_1 -> { thm with symbols := [s_2, ..., s_n] }`.
When `grind` internalizes a term containing symbol `s`, we
process all theorems `thm` associated with key `s`.
If their `thm.symbols` is empty, we say they are activated.
Otherwise, we reinsert into `map`.
-/
def EMatchTheorems.insert (s : EMatchTheorems) (thm : EMatchTheorem) : EMatchTheorems := Id.run do
let .const declName :: syms := thm.symbols
| unreachable!
let thm := { thm with symbols := syms }
let { smap, origins, erased, omap } := s
let origin := thm.origin
let origins := origins.insert origin
let erased := erased.erase origin
let smap := if let some thms := smap.find? declName then
smap.insert declName (thm::thms)
else
smap.insert declName [thm]
let omap := if let some thms := omap.find? origin then
omap.insert origin (thm::thms)
else
omap.insert origin [thm]
return { smap, origins, erased, omap }
/-- Returns `true` if `s` contains a theorem with the given origin. -/
def EMatchTheorems.contains (s : EMatchTheorems) (origin : Origin) : Bool :=
s.origins.contains origin
/-- Mark the theorem with the given origin as `erased` -/
def EMatchTheorems.erase (s : EMatchTheorems) (origin : Origin) : EMatchTheorems :=
{ s with erased := s.erased.insert origin, origins := s.origins.erase origin }
/-- Returns true if the theorem has been marked as erased. -/
def EMatchTheorems.isErased (s : EMatchTheorems) (origin : Origin) : Bool :=
s.erased.contains origin
/--
Retrieves theorems from `s` associated with the given symbol. See `EMatchTheorem.insert`.
The theorems are removed from `s`.
-/
@[inline]
def EMatchTheorems.retrieve? (s : EMatchTheorems) (sym : Name) : Option (List EMatchTheorem × EMatchTheorems) :=
if let some thms := s.smap.find? sym then
some (thms, { s with smap := s.smap.erase sym })
else
none
/--
Returns theorems associated with the given origin.
-/
def EMatchTheorems.find (s : EMatchTheorems) (origin : Origin) : List EMatchTheorem :=
if let some thms := s.omap.find? origin then
thms
else
[]
def EMatchTheorem.getProofWithFreshMVarLevels (thm : EMatchTheorem) : MetaM Expr := do
if thm.proof.isConst && thm.levelParams.isEmpty then
let declName := thm.proof.constName!
let info ← getConstInfo declName
if info.levelParams.isEmpty then
return thm.proof
else
mkConstWithFreshMVarLevels declName
else if thm.levelParams.isEmpty then
return thm.proof
else
let us ← thm.levelParams.mapM fun _ => mkFreshLevelMVar
return thm.proof.instantiateLevelParamsArray thm.levelParams us
private builtin_initialize ematchTheoremsExt : SimpleScopedEnvExtension EMatchTheorem EMatchTheorems ←
registerSimpleScopedEnvExtension {
addEntry := EMatchTheorems.insert
initial := {}
}
/-- Returns `true` if `declName` has been tagged as an E-match theorem using `[grind]`. -/
def isEMatchTheorem (declName : Name) : CoreM Bool := do
return ematchTheoremsExt.getState (← getEnv) |>.omap.contains (.decl declName)
def resetEMatchTheoremsExt : CoreM Unit := do
modifyEnv fun env => ematchTheoremsExt.modifyState env fun _ => {}
/--
Symbols with built-in support in `grind` are unsuitable as pattern candidates for E-matching.
This is because `grind` performs normalization operations and uses specialized data structures
to implement these symbols, which may interfere with E-matching behavior.
-/
-- TODO: create attribute?
private def forbiddenDeclNames := #[``Eq, ``HEq, ``Iff, ``And, ``Or, ``Not]
private def isForbidden (declName : Name) := forbiddenDeclNames.contains declName
/--
Auxiliary function to expand a pattern containing forbidden application symbols
into a multi-pattern.
This function enhances the usability of the `[grind =]` attribute by automatically handling
forbidden pattern symbols. For example, consider the following theorem tagged with this attribute:
```
getLast?_eq_some_iff {xs : List α} {a : α} : xs.getLast? = some a ↔ ∃ ys, xs = ys ++ [a]
```
Here, the selected pattern is `xs.getLast? = some a`, but `Eq` is a forbidden pattern symbol.
Instead of producing an error, this function converts the pattern into a multi-pattern,
allowing the attribute to be used conveniently.
The function recursively expands patterns with forbidden symbols by splitting them
into their sub-components. If the pattern does not contain forbidden symbols,
it is returned as-is.
-/
partial def splitWhileForbidden (pat : Expr) : List Expr :=
match_expr pat with
| Not p => splitWhileForbidden p
| And p₁ p₂ => splitWhileForbidden p₁ ++ splitWhileForbidden p₂
| Or p₁ p₂ => splitWhileForbidden p₁ ++ splitWhileForbidden p₂
| Eq _ lhs rhs => splitWhileForbidden lhs ++ splitWhileForbidden rhs
| Iff lhs rhs => splitWhileForbidden lhs ++ splitWhileForbidden rhs
| HEq _ lhs _ rhs => splitWhileForbidden lhs ++ splitWhileForbidden rhs
| _ => [pat]
private def dontCare := mkConst (Name.mkSimple "[grind_dontcare]")
def mkGroundPattern (e : Expr) : Expr :=
mkAnnotation `grind.ground_pat e
def groundPattern? (e : Expr) : Option Expr :=
annotation? `grind.ground_pat e
private def isGroundPattern (e : Expr) : Bool :=
groundPattern? e |>.isSome
def isPatternDontCare (e : Expr) : Bool :=
e == dontCare
private def isAtomicPattern (e : Expr) : Bool :=
e.isBVar || isPatternDontCare e || isGroundPattern e
partial def ppPattern (pattern : Expr) : MessageData := Id.run do
if let some e := groundPattern? pattern then
return m!"`[{e}]"
else if isPatternDontCare pattern then
return m!"_"
else match pattern with
| .bvar idx => return m!"#{idx}"
| _ =>
if pattern.isAppOfArity ``Grind.offset 2 then
let lhs := ppArg <| pattern.getArg! 0
let rhs := ppPattern <| pattern.getArg! 1
return m!"{lhs} + {rhs}"
else
let mut r := m!"{pattern.getAppFn}"
for arg in pattern.getAppArgs do
r := r ++ " " ++ ppArg arg
return r
where
ppArg (arg : Expr) : MessageData :=
if isAtomicPattern arg then
ppPattern arg
else
.paren (ppPattern arg)
namespace NormalizePattern
structure State where
symbols : Array HeadIndex := #[]
symbolSet : Std.HashSet HeadIndex := {}
bvarsFound : Std.HashSet Nat := {}
abbrev M := StateRefT State MetaM
private def saveSymbol (h : HeadIndex) : M Unit := do
unless (← get).symbolSet.contains h do
modify fun s => { s with symbols := s.symbols.push h, symbolSet := s.symbolSet.insert h }
private def foundBVar (idx : Nat) : M Bool :=
return (← get).bvarsFound.contains idx
private def saveBVar (idx : Nat) : M Unit := do
modify fun s => { s with bvarsFound := s.bvarsFound.insert idx }
private def getPatternFn? (pattern : Expr) : Option Expr :=
if !pattern.isApp && !pattern.isConst then
none
else match pattern.getAppFn with
| f@(.const declName _) => if isForbidden declName then none else some f
| f@(.fvar _) => some f
| _ => none
/--
Returns a bit-mask `mask` s.t. `mask[i]` is true if the corresponding argument is
- a type (that is not a proposition) or type former (which has forward dependencies) or
- a proof, or
- an instance implicit argument
When `mask[i]`, we say the corresponding argument is a "support" argument.
-/
def getPatternSupportMask (f : Expr) (numArgs : Nat) : MetaM (Array Bool) := do
let pinfos := (← getFunInfoNArgs f numArgs).paramInfo
forallBoundedTelescope (← inferType f) numArgs fun xs _ => do
xs.mapIdxM fun idx x => do
if (← isProp x) then
return false
else if (← isProof x) then
return true
else if (← isTypeFormer x) then
if h : idx < pinfos.size then
/-
We originally wanted to ignore types and type formers in `grind` and treat them as supporting elements.
Thus, we would always return `true`. However, we changed our heuristic because of the following example:
```
example {α} (f : α → Type) (a : α) (h : ∀ x, Nonempty (f x)) : Nonempty (f a) := by
grind
```
In this example, we are reasoning about types. Therefore, we adjusted the heuristic as follows:
a type or type former is considered a supporting element only if it has forward dependencies.
Note that this is not the case for `Nonempty`.
-/
return pinfos[idx].hasFwdDeps
else
return true
else
return (← x.fvarId!.getDecl).binderInfo matches .instImplicit
private partial def go (pattern : Expr) : M Expr := do
if let some (e, k) := isOffsetPattern? pattern then
let e ← goArg e (isSupport := false)
if e == dontCare then
return dontCare
else
return mkOffsetPattern e k
let some f := getPatternFn? pattern
| throwError "invalid pattern, (non-forbidden) application expected{indentExpr pattern}"
assert! f.isConst || f.isFVar
unless f.isConstOf ``Grind.eqBwdPattern do
saveSymbol f.toHeadIndex
let mut args := pattern.getAppArgs.toVector
let supportMask ← getPatternSupportMask f args.size
for h : i in [:args.size] do
let arg := args[i]
let isSupport := supportMask[i]?.getD false
args := args.set i (← goArg arg isSupport)
return mkAppN f args.toArray
where
goArg (arg : Expr) (isSupport : Bool) : M Expr := do
if !arg.hasLooseBVars then
if arg.hasMVar then
pure dontCare
else
pure <| mkGroundPattern arg
else match arg with
| .bvar idx =>
if isSupport && (← foundBVar idx) then
pure dontCare
else
saveBVar idx
pure arg
| _ =>
if isSupport then
pure dontCare
else if let some _ := getPatternFn? arg then
go arg
else
pure dontCare
def main (patterns : List Expr) : MetaM (List Expr × List HeadIndex × Std.HashSet Nat) := do
let (patterns, s) ← patterns.mapM go |>.run {}
return (patterns, s.symbols.toList, s.bvarsFound)
def normalizePattern (e : Expr) : M Expr := do
go e
end NormalizePattern
/--
Returns `true` if free variables in `type` are not in `thmVars` or are in `fvarsFound`.
We use this function to check whether `type` is fully instantiated.
-/
private def checkTypeFVars (thmVars : FVarIdSet) (fvarsFound : FVarIdSet) (type : Expr) : Bool :=
let typeFVars := (collectFVars {} type).fvarIds
typeFVars.all fun fvarId => !thmVars.contains fvarId || fvarsFound.contains fvarId
/--
Given an type class instance type `instType`, returns true if free variables in input parameters
1- are not in `thmVars`, or
2- are in `fvarsFound`.
Remark: `fvarsFound` is a subset of `thmVars`
-/
private def canBeSynthesized (thmVars : FVarIdSet) (fvarsFound : FVarIdSet) (instType : Expr) : MetaM Bool := do
forallTelescopeReducing instType fun xs type => type.withApp fun classFn classArgs => do
for x in xs do
unless checkTypeFVars thmVars fvarsFound (← inferType x) do return false
forallBoundedTelescope (← inferType classFn) type.getAppNumArgs fun params _ => do
for param in params, classArg in classArgs do
let paramType ← inferType param
if !paramType.isAppOf ``semiOutParam && !paramType.isAppOf ``outParam then
unless checkTypeFVars thmVars fvarsFound classArg do
return false
return true
/--
Auxiliary type for the `checkCoverage` function.
-/
inductive CheckCoverageResult where
| /-- `checkCoverage` succeeded -/
ok
| /--
`checkCoverage` failed because some of the theorem parameters are missing,
`pos` contains their positions
-/
missing (pos : List Nat)
/--
After we process a set of patterns, we obtain the set of de Bruijn indices in these patterns.
We say they are pattern variables. This function checks whether the set of pattern variables is sufficient for
instantiating the theorem with proof `thmProof`. The theorem has `numParams` parameters.
The missing parameters:
1- we may be able to infer them using type inference or type class synthesis, or
2- they are propositions, and may become hypotheses of the instantiated theorem.
For type class instance parameters, we must check whether the free variables in class input parameters are available.
-/
private def checkCoverage (thmProof : Expr) (numParams : Nat) (bvarsFound : Std.HashSet Nat) : MetaM CheckCoverageResult := do
if bvarsFound.size == numParams then return .ok
forallBoundedTelescope (← inferType thmProof) numParams fun xs _ => do
assert! numParams == xs.size
let patternVars := bvarsFound.toList.map fun bidx => xs[numParams - bidx - 1]!.fvarId!
-- `xs` as a `FVarIdSet`.
let thmVars : FVarIdSet := RBTree.ofList <| xs.toList.map (·.fvarId!)
-- Collect free variables occurring in `e`, and insert the ones that are in `thmVars` into `fvarsFound`
let update (fvarsFound : FVarIdSet) (e : Expr) : FVarIdSet :=
(collectFVars {} e).fvarIds.foldl (init := fvarsFound) fun s fvarId =>
if thmVars.contains fvarId then s.insert fvarId else s
-- Theorem variables found so far. We initialize with the variables occurring in patterns
-- Remark: fvarsFound is a subset of thmVars
let mut fvarsFound : FVarIdSet := RBTree.ofList patternVars
for patternVar in patternVars do
let type ← patternVar.getType
fvarsFound := update fvarsFound type
if fvarsFound.size == numParams then return .ok
-- Now, we keep traversing remaining variables and collecting
-- `processed` contains the variables we have already processed.
let mut processed : FVarIdSet := RBTree.ofList patternVars
let mut modified := false
repeat
modified := false
for x in xs do
let fvarId := x.fvarId!
unless processed.contains fvarId do
let xType ← inferType x
if fvarsFound.contains fvarId then
-- Collect free vars in `x`s type and mark as processed
fvarsFound := update fvarsFound xType
processed := processed.insert fvarId
modified := true
else if (← isProp xType) then
-- If `x` is a proposition, and all theorem variables in `x`s type have already been found
-- add it to `fvarsFound` and mark it as processed.
if checkTypeFVars thmVars fvarsFound xType then
fvarsFound := fvarsFound.insert fvarId
processed := processed.insert fvarId
modified := true
else if (← fvarId.getDecl).binderInfo matches .instImplicit then
-- If `x` is instance implicit, check whether
-- we have found all free variables needed to synthesize instance
if (← canBeSynthesized thmVars fvarsFound xType) then
fvarsFound := fvarsFound.insert fvarId
fvarsFound := update fvarsFound xType
processed := processed.insert fvarId
modified := true
if fvarsFound.size == numParams then
return .ok
if !modified then
break
let mut pos := #[]
for h : i in [:xs.size] do
let fvarId := xs[i].fvarId!
unless fvarsFound.contains fvarId do
pos := pos.push i
return .missing pos.toList
/--
Given a theorem with proof `proof` and `numParams` parameters, returns a message
containing the parameters at positions `paramPos`.
-/
private def ppParamsAt (proof : Expr) (numParams : Nat) (paramPos : List Nat) : MetaM MessageData := do
forallBoundedTelescope (← inferType proof) numParams fun xs _ => do
let mut msg := m!""
let mut first := true
for h : i in [:xs.size] do
if paramPos.contains i then
let x := xs[i]
if first then first := false else msg := msg ++ "\n"
msg := msg ++ m!"{x} : {← inferType x}"
addMessageContextFull msg
/--
Creates an E-matching theorem for a theorem with proof `proof`, `numParams` parameters, and the given set of patterns.
Pattern variables are represented using de Bruijn indices.
-/
def mkEMatchTheoremCore (origin : Origin) (levelParams : Array Name) (numParams : Nat) (proof : Expr) (patterns : List Expr) (kind : EMatchTheoremKind) : MetaM EMatchTheorem := do
let (patterns, symbols, bvarFound) ← NormalizePattern.main patterns
if symbols.isEmpty then
throwError "invalid pattern for `{← origin.pp}`{indentD (patterns.map ppPattern)}\nthe pattern does not contain constant symbols for indexing"
trace[grind.ematch.pattern] "{MessageData.ofConst proof}: {patterns.map ppPattern}"
if let .missing pos ← checkCoverage proof numParams bvarFound then
let pats : MessageData := m!"{patterns.map ppPattern}"
throwError "invalid pattern(s) for `{← origin.pp}`{indentD pats}\nthe following theorem parameters cannot be instantiated:{indentD (← ppParamsAt proof numParams pos)}"
return {
proof, patterns, numParams, symbols
levelParams, origin, kind
}
private def getProofFor (declName : Name) : MetaM Expr := do
let info ← getConstInfo declName
-- For theorems, `isProp` has already been checked at declaration time
unless wasOriginallyTheorem (← getEnv) declName do
unless (← isProp info.type) do
throwError "invalid E-matching theorem `{declName}`, type is not a proposition"
let us := info.levelParams.map mkLevelParam
return mkConst declName us
/--
Creates an E-matching theorem for `declName` with `numParams` parameters, and the given set of patterns.
Pattern variables are represented using de Bruijn indices.
-/
def mkEMatchTheorem (declName : Name) (numParams : Nat) (patterns : List Expr) (kind : EMatchTheoremKind) : MetaM EMatchTheorem := do
mkEMatchTheoremCore (.decl declName) #[] numParams (← getProofFor declName) patterns kind
/--
Given a theorem with proof `proof` and type of the form `∀ (a_1 ... a_n), lhs = rhs`,
creates an E-matching pattern for it using `addEMatchTheorem n [lhs]`
If `normalizePattern` is true, it applies the `grind` simplification theorems and simprocs to the pattern.
-/
def mkEMatchEqTheoremCore (origin : Origin) (levelParams : Array Name) (proof : Expr) (normalizePattern : Bool) (useLhs : Bool) : MetaM EMatchTheorem := do
let (numParams, patterns) ← forallTelescopeReducing (← inferType proof) fun xs type => do
let (lhs, rhs) ← match_expr type with
| Eq _ lhs rhs => pure (lhs, rhs)
| Iff lhs rhs => pure (lhs, rhs)
| HEq _ lhs _ rhs => pure (lhs, rhs)
| _ => throwError "invalid E-matching equality theorem, conclusion must be an equality{indentExpr type}"
let pat := if useLhs then lhs else rhs
trace[grind.debug.ematch.pattern] "mkEMatchEqTheoremCore: origin: {← origin.pp}, pat: {pat}, useLhs: {useLhs}"
let pat ← preprocessPattern pat normalizePattern
trace[grind.debug.ematch.pattern] "mkEMatchEqTheoremCore: after preprocessing: {pat}, {← normalize pat normConfig}"
let pats := splitWhileForbidden (pat.abstract xs)
return (xs.size, pats)
mkEMatchTheoremCore origin levelParams numParams proof patterns (if useLhs then .eqLhs else .eqRhs)
def mkEMatchEqBwdTheoremCore (origin : Origin) (levelParams : Array Name) (proof : Expr) : MetaM EMatchTheorem := do
let (numParams, patterns) ← forallTelescopeReducing (← inferType proof) fun xs type => do
let_expr f@Eq α lhs rhs := type
| throwError "invalid E-matching `←=` theorem, conclusion must be an equality{indentExpr type}"
let pat ← preprocessPattern (mkEqBwdPattern f.constLevels! α lhs rhs)
return (xs.size, [pat.abstract xs])
mkEMatchTheoremCore origin levelParams numParams proof patterns .eqBwd
/--
Given theorem with name `declName` and type of the form `∀ (a_1 ... a_n), lhs = rhs`,
creates an E-matching pattern for it using `addEMatchTheorem n [lhs]`
If `normalizePattern` is true, it applies the `grind` simplification theorems and simprocs to the
pattern.
-/
def mkEMatchEqTheorem (declName : Name) (normalizePattern := true) (useLhs : Bool := true) : MetaM EMatchTheorem := do
mkEMatchEqTheoremCore (.decl declName) #[] (← getProofFor declName) normalizePattern useLhs
/--
Adds an E-matching theorem to the environment.
See `mkEMatchTheorem`.
-/
def addEMatchTheorem (declName : Name) (numParams : Nat) (patterns : List Expr) (kind : EMatchTheoremKind) : MetaM Unit := do
ematchTheoremsExt.add (← mkEMatchTheorem declName numParams patterns kind)
/--
Adds an E-matching equality theorem to the environment.
See `mkEMatchEqTheorem`.
-/
def addEMatchEqTheorem (declName : Name) : MetaM Unit := do
ematchTheoremsExt.add (← mkEMatchEqTheorem declName)
/-- Returns the E-matching theorems registered in the environment. -/
def getEMatchTheorems : CoreM EMatchTheorems :=
return ematchTheoremsExt.getState (← getEnv)
/-- Returns the types of `xs` that are propositions. -/
private def getPropTypes (xs : Array Expr) : MetaM (Array Expr) :=
xs.filterMapM fun x => do
let type ← inferType x
if (← isProp type) then return some type else return none
/-- State for the (pattern) `CollectorM` monad -/
private structure Collector.State where
/-- Pattern found so far. -/
patterns : Array Expr := #[]
done : Bool := false
private structure Collector.Context where
proof : Expr
xs : Array Expr
/-- Monad for collecting patterns for a theorem. -/
private abbrev CollectorM := ReaderT Collector.Context $ StateRefT Collector.State NormalizePattern.M
/-- Similar to `getPatternFn?`, but operates on expressions that do not contain loose de Bruijn variables. -/
private def isPatternFnCandidate (f : Expr) : CollectorM Bool := do
match f with
| .const declName _ => return !isForbidden declName
| .fvar .. => return !(← read).xs.contains f
| _ => return false
private def addNewPattern (p : Expr) : CollectorM Unit := do
trace[grind.ematch.pattern.search] "found pattern: {ppPattern p}"
let bvarsFound := (← getThe NormalizePattern.State).bvarsFound
let done := (← checkCoverage (← read).proof (← read).xs.size bvarsFound) matches .ok
if done then
trace[grind.ematch.pattern.search] "found full coverage"
modify fun s => { s with patterns := s.patterns.push p, done }
/-- Collect the pattern (i.e., de Bruijn) variables in the given pattern. -/
private def collectPatternBVars (p : Expr) : List Nat :=
go p |>.run [] |>.2
where
go (e : Expr) : StateM (List Nat) Unit := do
match e with
| .app f a => go f; go a
| .mdata _ b => go b
| .bvar idx => modify fun s => if s.contains idx then s else idx :: s
| _ => return ()
private def diff (s : List Nat) (found : Std.HashSet Nat) : List Nat :=
if found.isEmpty then s else s.filter fun x => !found.contains x
/--
Returns `true` if pattern `p` contains a child `c` such that
1- `p` and `c` have the same new pattern variables. We say a pattern variable is new if it is not in `alreadyFound`.
2- `c` is not a support argument. See `NormalizePattern.getPatternSupportMask` for definition.
3- `c` is not an offset pattern.
4- `c` is not a bound variable.
-/
private def hasChildWithSameNewBVars (p : Expr) (supportMask : Array Bool) (alreadyFound : Std.HashSet Nat) : CoreM Bool := do
let s := diff (collectPatternBVars p) alreadyFound
for arg in p.getAppArgs, support in supportMask do
unless support do
unless arg.isBVar do
unless isOffsetPattern? arg |>.isSome do
let sArg := diff (collectPatternBVars arg) alreadyFound
if s ⊆ sArg then
return true
return false
private partial def collect (e : Expr) : CollectorM Unit := do
if (← get).done then return ()
match e with
| .app .. =>
let f := e.getAppFn
let supportMask ← NormalizePattern.getPatternSupportMask f e.getAppNumArgs
if (← isPatternFnCandidate f) then
let saved ← getThe NormalizePattern.State
try
trace[grind.ematch.pattern.search] "candidate: {e}"
let p := e.abstract (← read).xs
unless p.hasLooseBVars do
trace[grind.ematch.pattern.search] "skip, does not contain pattern variables"
return ()
let p ← NormalizePattern.normalizePattern p
if saved.bvarsFound.size < (← getThe NormalizePattern.State).bvarsFound.size then
unless (← hasChildWithSameNewBVars p supportMask saved.bvarsFound) do
addNewPattern p
return ()
trace[grind.ematch.pattern.search] "skip, no new variables covered"
-- restore state and continue search
set saved
catch _ =>
trace[grind.ematch.pattern.search] "skip, exception during normalization"
-- restore state and continue search
set saved
let args := e.getAppArgs
for arg in args, support in supportMask do
unless support do
collect arg
| .forallE _ d b _ =>
if (← pure e.isArrow <&&> isProp d <&&> isProp b) then
collect d
collect b
| _ => return ()
private def collectPatterns? (proof : Expr) (xs : Array Expr) (searchPlaces : Array Expr) : MetaM (Option (List Expr × List HeadIndex)) := do
let go : CollectorM (Option (List Expr)) := do
for place in searchPlaces do
let place ← preprocessPattern place
collect place
if (← get).done then
return some ((← get).patterns.toList)
return none
let (some ps, s) ← go { proof, xs } |>.run' {} |>.run {}
| return none
return some (ps, s.symbols.toList)
/--
Tries to find a ground pattern to activate the theorem.
This is used for theorems such as `theorem evenZ : Even 0`.
This function is only used if `collectPatterns?` returns `none`.
-/
private partial def collectGroundPattern? (proof : Expr) (xs : Array Expr) (searchPlaces : Array Expr) : MetaM (Option (Expr × List HeadIndex)) := do
unless (← checkCoverage proof xs.size {}) matches .ok do
return none
let go? : CollectorM (Option Expr) := do
for place in searchPlaces do
let place ← preprocessPattern place
if let some r ← visit? place then
return r
return none
let (some p, s) ← go? { proof, xs } |>.run' {} |>.run {}
| return none
return some (p, s.symbols.toList)
where
visit? (e : Expr) : CollectorM (Option Expr) := do
match e with
| .app .. =>
let f := e.getAppFn
if (← isPatternFnCandidate f) then
let e ← NormalizePattern.normalizePattern e
return some e
else
let args := e.getAppArgs
for arg in args, flag in (← NormalizePattern.getPatternSupportMask f args.size) do
unless flag do
if let some r ← visit? arg then
return r
return none
| .forallE _ d b _ =>
if (← pure e.isArrow <&&> isProp d <&&> isProp b) then
if let some d ← visit? d then return d
visit? b
else
return none
| _ => return none
/--
Creates an E-match theorem using the given proof and kind.
If `groundPatterns` is `true`, it accepts patterns without pattern variables. This is useful for
theorems such as `theorem evenZ : Even 0`. For local theorems, we use `groundPatterns := false`
since the theorem is already in the `grind` state and there is nothing to be instantiated.
-/
def mkEMatchTheoremWithKind?
(origin : Origin) (levelParams : Array Name) (proof : Expr) (kind : EMatchTheoremKind)
(groundPatterns := true) : MetaM (Option EMatchTheorem) := do
if kind == .eqLhs then
return (← mkEMatchEqTheoremCore origin levelParams proof (normalizePattern := true) (useLhs := true))
else if kind == .eqRhs then
return (← mkEMatchEqTheoremCore origin levelParams proof (normalizePattern := true) (useLhs := false))
else if kind == .eqBwd then
return (← mkEMatchEqBwdTheoremCore origin levelParams proof)
let type ← inferType proof
/-
Remark: we should not use `forallTelescopeReducing` (with default reducibility) here
because it may unfold a definition/abstraction, and then select a suboptimal pattern.
Here is an example. Suppose we have
```
def State.le (σ₁ σ₂ : State) : Prop := ∀ ⦃x : Var⦄ ⦃v : Val⦄, σ₁.find? x = some v → σ₂.find? x = some v
infix:50 " ≼ " => State.le
```
Then, we write the theorem
```
@[grind] theorem State.join_le_left (σ₁ σ₂ : State) : σ₁.join σ₂ ≼ σ₁ := by
```
We do not want `State.le` to be unfolded and the abstraction exposed.
That said, we must still reduce `[reducible]` definitions since `grind` unfolds them.
-/
withReducible <| forallTelescopeReducing type fun xs type => withDefault do
let searchPlaces ← match kind with
| .fwd =>
let ps ← getPropTypes xs
if ps.isEmpty then
throwError "invalid `grind` forward theorem, theorem `{← origin.pp}` does not have propositional hypotheses"
pure ps
| .bwd => pure #[type]
| .leftRight => pure <| (← getPropTypes xs).push type
| .rightLeft => pure <| #[type] ++ (← getPropTypes xs).reverse
| .default => pure <| #[type] ++ (← getPropTypes xs)
| _ => unreachable!
go xs searchPlaces
where
go (xs : Array Expr) (searchPlaces : Array Expr) : MetaM (Option EMatchTheorem) := do
let (patterns, symbols) ← if let some r ← collectPatterns? proof xs searchPlaces then
pure r
else if !groundPatterns then
return none
else if let some (pattern, symbols) ← collectGroundPattern? proof xs searchPlaces then
pure ([pattern], symbols)
else
return none
let numParams := xs.size
trace[grind.ematch.pattern] "{← origin.pp}: {patterns.map ppPattern}"
return some {
proof, patterns, numParams, symbols
levelParams, origin, kind
}
def mkEMatchTheoremForDecl (declName : Name) (thmKind : EMatchTheoremKind) : MetaM EMatchTheorem := do
let some thm ← mkEMatchTheoremWithKind? (.decl declName) #[] (← getProofFor declName) thmKind
| throwError "`@{thmKind.toAttribute} theorem {declName}` {thmKind.explainFailure}, consider using different options or the `grind_pattern` command"
return thm
def mkEMatchEqTheoremsForDef? (declName : Name) : MetaM (Option (Array EMatchTheorem)) := do
let some eqns ← getEqnsFor? declName | return none
eqns.mapM fun eqn => do
mkEMatchEqTheorem eqn (normalizePattern := true)
private def addGrindEqAttr (declName : Name) (attrKind : AttributeKind) (thmKind : EMatchTheoremKind) (useLhs := true) : MetaM Unit := do
if wasOriginallyTheorem (← getEnv) declName then
ematchTheoremsExt.add (← mkEMatchEqTheorem declName (normalizePattern := true) (useLhs := useLhs)) attrKind
else if let some thms ← mkEMatchEqTheoremsForDef? declName then
unless useLhs do
throwError "`{declName}` is a definition, you must only use the left-hand side for extracting patterns"
thms.forM (ematchTheoremsExt.add · attrKind)
else
throwError s!"`{thmKind.toAttribute}` attribute can only be applied to equational theorems or function definitions"
def EMatchTheorems.eraseDecl (s : EMatchTheorems) (declName : Name) : MetaM EMatchTheorems := do
let throwErr {α} : MetaM α :=
throwError "`{declName}` is not marked with the `[grind]` attribute"
if !wasOriginallyTheorem (← getEnv) declName then
if let some eqns ← getEqnsFor? declName then
let s := ematchTheoremsExt.getState (← getEnv)
unless eqns.all fun eqn => s.contains (.decl eqn) do
throwErr
return eqns.foldl (init := s) fun s eqn => s.erase (.decl eqn)
else
throwErr
else
unless ematchTheoremsExt.getState (← getEnv) |>.contains (.decl declName) do
throwErr
return s.erase <| .decl declName
def addEMatchAttr (declName : Name) (attrKind : AttributeKind) (thmKind : EMatchTheoremKind) : MetaM Unit := do
if thmKind == .eqLhs then
addGrindEqAttr declName attrKind thmKind (useLhs := true)
else if thmKind == .eqRhs then
addGrindEqAttr declName attrKind thmKind (useLhs := false)
else if thmKind == .eqBoth then
addGrindEqAttr declName attrKind thmKind (useLhs := true)
addGrindEqAttr declName attrKind thmKind (useLhs := false)
else
let info ← getConstInfo declName
if !wasOriginallyTheorem (← getEnv) declName && !info.isCtor && !info.isAxiom then
addGrindEqAttr declName attrKind thmKind
else
let thm ← mkEMatchTheoremForDecl declName thmKind
ematchTheoremsExt.add thm attrKind
def eraseEMatchAttr (declName : Name) : MetaM Unit := do
/-
Remark: consider the following example
```
attribute [grind] foo -- ok
attribute [-grind] foo.eqn_2 -- ok
attribute [-grind] foo -- error
```
One may argue that the correct behavior should be
```
attribute [grind] foo -- ok
attribute [-grind] foo.eqn_2 -- error
attribute [-grind] foo -- ok
```
-/
let s := ematchTheoremsExt.getState (← getEnv)
let s ← s.eraseDecl declName
modifyEnv fun env => ematchTheoremsExt.modifyState env fun _ => s
end Lean.Meta.Grind