feat: MVarId.assertAfter fvar alias info, MVarId.replace mvar dependencies, specialize tactic using eta arguments (#13528)

This PR gives the `specialize` tactic the ability to instantiate
universal quantifiers other than the first using `specialize h (y := v)`
syntax. It also fixes an issue where `MVarId.assertAfter` did not record
variable alias information, and an issue where `MVarId.replace` and
`MVarId.replaceLocalDecl` did not take metavariables into account when
calculating dependencies. Additionally it fixes some uninstantiated
metavariables bugs, including one in the Infoview tactic state
hypothesis diff.

The `specialize` tactic now uses `Lean.MVarId.replace` to simplify the
implementation, and as a consequence it tries to keep the specialized
hypothesis close to its original spot in the local context.

Additional metaprogramming API:
- `Lean.Expr.getLambdaBody` to accompany `Lean.Expr.getNumHeadLambdas`
- `Lean.LocalContext.setType`, `Lean.MetavarContext.setFVarType`,
`Lean.MVarId.setFVarType`
- `Lean.MVarId.assertAfter'` to assert a new hypothesis as early as
possibly in the context where it is well-formed, as a frontend to
`Lean.MVarId.assertAfter`, which assumes the new hypothesis is
well-formed

Breaking change: metaprograms cannot assume that `MVarId`s change if
metavariables are assigned. For example, the `change` tactic will no
longer change `MVarId`s if the only effect is incidental metavariable
assignments.

Mathlib impact: this revealed many `dsimp`s that did nothing and could
be deleted.

Closes #9893
This commit is contained in:
Kyle Miller 2026-04-30 03:36:29 -07:00 committed by GitHub
parent ad5a3291dc
commit 19baa470e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 251 additions and 126 deletions

View file

@ -138,9 +138,7 @@ theorem getElem_zero_flatten.proof {xss : Array (Array α)} (h : 0 < xss.flatten
@[grind =]
theorem getElem_zero_flatten {xss : Array (Array α)} (h) :
(flatten xss)[0] = (xss.findSome? fun xs => xs[0]?).get (getElem_zero_flatten.proof h) := by
have t := getElem?_zero_flatten xss
simp at t
simp [← t]
simp [← getElem?_zero_flatten xss]
@[grind =]
theorem findSome?_replicate : findSome? f (replicate n a) = if n = 0 then none else f a := by

View file

@ -1518,7 +1518,6 @@ theorem sdiv_ne_intMin_of_ne_intMin {x y : BitVec w} (h : x ≠ intMin w) :
simp only [sdiv, udiv_eq, neg_eq]
by_cases hx : x.msb <;> by_cases hy : y.msb
<;> simp only [hx, hy, neg_ne_intMin_inj]
<;> simp only [Bool.not_eq_true] at hx hy
<;> apply ne_intMin_of_msb_eq_false (by omega)
<;> rw [msb_udiv]
<;> try simp only [hx, Bool.false_and]
@ -1588,8 +1587,7 @@ theorem toInt_sdiv_of_ne_or_ne (a b : BitVec w) (h : a ≠ intMin w b ≠ -1
have := Nat.two_pow_pos (w - 1)
by_cases hbintMin : b = intMin w
· simp only at hbintMin
subst hbintMin
· subst hbintMin
have toIntA_lt := @BitVec.toInt_lt w a; norm_cast at toIntA_lt
have le_toIntA := @BitVec.le_toInt w a; norm_cast at le_toIntA
simp only [sdiv_intMin, toInt_intMin, wpos,
@ -1607,7 +1605,7 @@ theorem toInt_sdiv_of_ne_or_ne (a b : BitVec w) (h : a ≠ intMin w b ≠ -1
omega
· by_cases ha : a.msb <;> by_cases hb : b.msb
<;> simp only [not_eq_true] at ha hb
<;> (try simp only [not_eq_true] at ha hb)
· simp only [sdiv_eq, ha, hb, udiv_eq]
rw [toInt_eq_neg_toNat_neg_of_nonpos (x := a) (by simp [ha]),
toInt_eq_neg_toNat_neg_of_nonpos (x := b) (by simp [hb]),

View file

@ -4625,8 +4625,6 @@ theorem toInt_ediv_toInt_lt_of_nonpos_of_lt_neg_one {w : Nat} {x y : BitVec w} (
rcases hx' with hx'|hx'|hx'
· simp [hx']; omega
· have := BitVec.neg_one_ediv_toInt_eq (y := y)
simp only [
Int.reduceNeg] at this
simp [hx', this]
omega
· have := Int.ediv_lt_natAbs_self_of_lt_neg_one_of_lt_neg_one (x := x.toInt) (y := y.toInt) (by omega) hy

View file

@ -636,7 +636,6 @@ private theorem eq_of_norm_eq_of_divCoeffs {ctx : Context} {p₁ p₂ : Poly} {k
have hz : k ≠ 0 := Int.ne_of_gt h₀
replace h₁ := Poly.denote_div_eq_of_divCoeffs ctx p₁ k h₁
replace h₂ := congrArg (Poly.denote ctx) h₂
simp at h₂
rw [h₂, ← h₁]; clear h₁ h₂
apply mul_add_cmod_le_iff
assumption

View file

@ -970,7 +970,6 @@ theorem eq_push_append_of_mem {xs : Vector α n} {x : α} (h : x ∈ xs) :
xs = (as.push x ++ bs).cast h ∧ x ∉ as:= by
rcases xs with ⟨xs, rfl⟩
obtain ⟨as, bs, h, w⟩ := Array.eq_push_append_of_mem (by simpa using h)
simp only at h
obtain rfl := h
exact ⟨_, _, as.toVector, bs.toVector, by simp, by simp, by simpa using w⟩

View file

@ -359,7 +359,7 @@ instance (priority := high) {p} [Semiring α] [AddRightCancel α] [IsCharP α p]
replace h := AddRightCancel.add_right_cancel _ _ _ h
simp [Semiring.ofNat_eq_natCast, h]
have := IsCharP.ofNat_ext_iff p |>.mp h
simp at this; assumption
exact this
next =>
intro h
have := IsCharP.ofNat_ext_iff (α := α) p |>.mpr h

View file

@ -143,7 +143,6 @@ theorem monotone_mapM (f : γα → m β) (xs : List α) (hmono : monotone
apply hmono
· apply monotone_of_monotone_apply
intro y
dsimp
generalize [y] = ys
induction xs generalizing ys with
| nil => apply monotone_const
@ -481,8 +480,7 @@ theorem monotone_mapFinIdxM (xs : Array α) (f : γ → (i : Nat) → α → i <
apply monotone_const
case case2 ih =>
apply monotone_bind
· dsimp
apply monotone_apply
· apply monotone_apply
apply monotone_apply
apply monotone_apply
apply hmono

View file

@ -104,7 +104,6 @@ theorem ofNat_sub_dichotomy {a b : Nat} :
by_cases h : b ≤ a
· left
have t := Int.ofNat_sub h
simp at t
exact ⟨h, t⟩
· right
have t := Nat.not_le.mp h

View file

@ -1163,16 +1163,6 @@ and less messy than commenting the remainder of the proof.
-/
macro "stop" tacticSeq : tactic => `(tactic| repeat sorry)
/--
The tactic `specialize h a₁ ... aₙ` works on local hypothesis `h`.
The premises of this hypothesis, either universal quantifications or
non-dependent implications, are instantiated by concrete terms coming
from arguments `a₁` ... `aₙ`.
The tactic adds a new hypothesis with the same name `h := h a₁ ... aₙ`
and tries to clear the previous one.
-/
syntax (name := specialize) "specialize " term : tactic
/--
`unhygienic tacs` runs `tacs` with name hygiene disabled.
This means that tactics that would normally create inaccessible names will instead
@ -1260,8 +1250,8 @@ macro "nomatch " es:term,+ : tactic =>
`(tactic| exact nomatch $es:term,*)
/--
Acts like `have`, but removes a hypothesis with the same name as
this one if possible. For example, if the state is:
`replace h := e` is like `have h := e`, but it removes a previous hypothesis
of the same name as this one if possible. For example, if the state is:
```lean
f : α → β
@ -1286,10 +1276,29 @@ h : β
⊢ goal
```
This can be used to simulate the `specialize` and `apply at` tactics of Coq.
The tactic `specialize h a₁ ... aₙ` is a way to write `replace h := h a₁ ... aₙ`,
automatically inferring which hypothesis should be replaced.
The `replace` tactic can be used to simulate Rocq's `apply at` tactic.
-/
syntax (name := replace) "replace" letDecl : tactic
/--
`specialize h a₁ ... aₙ` is equivalent to `replace h := h a₁ ... aₙ`.
It specializes the local hypothesis `h` by instantiating
universal quantifications and implications using the concrete terms `a₁` ... `aₙ`.
The tactic adds a new hypothesis with the same name and tries to remove
the original `h` if possible.
Example: given `h : ∀ (n : Nat), p n → q n` and `h' : p 2`,
then `specialize h 2 h'` replaces `h` with `h : q 2`.
The tactic also supports instantiating particular universal quantifiers
using named argument syntax. Example: given `h : ∀ (m n : Nat), p m n`,
then `specialize h (n := 2)` replaces `h` with `h : ∀ (m : Nat), p m 2`.
-/
syntax (name := specialize) "specialize " term : tactic
/-- `and_intros` applies `And.intro` until it does not make progress. -/
syntax "and_intros" : tactic
macro_rules | `(tactic| and_intros) => `(tactic| repeat' refine And.intro ?_ ?_)

View file

@ -420,7 +420,6 @@ where
for h : idx in 0...chunks do
have : idx * 8 + 7 < scalarArgs.size := by
have : idx < scalarArgs.size / 8 := Std.Rco.lt_upper_of_mem h
simp at this
omega
let b1 := scalarArgs[idx * 8]
let b2 := scalarArgs[idx * 8 + 1]

View file

@ -107,6 +107,7 @@ target.
-/
def convClear (mvarId : MVarId) (fvarId : FVarId) : MetaM MVarId := do
let (lhs, rhs) ← getLhsRhsCore mvarId
let rhs ← instantiateMVars rhs
unless rhs.isMVar do
return (← mvarId.clear fvarId)
let rhsKind ← rhs.mvarId!.getKind

View file

@ -7,6 +7,7 @@ module
prelude
public import Lean.Elab.Tactic.Basic
public import Lean.Meta.Tactic.Cleanup
import Lean.Meta.Native
import Lean.Elab.Tactic.ElabTerm

View file

@ -7,8 +7,7 @@ module
prelude
public import Lean.Meta.Tactic.Constructor
public import Lean.Meta.Tactic.Assert
public import Lean.Meta.Tactic.Cleanup
public import Lean.Meta.Tactic.Replace
public import Lean.Meta.Tactic.Rename
public import Lean.Elab.Tactic.Config
@ -234,13 +233,10 @@ def refineCore (stx : Syntax) (tagSuffix : Name) (allowNaturalHoles : Bool) : Ta
match stx with
| `(tactic| specialize $e:term) =>
let (e, mvarIds') ← elabTermWithHoles e none `specialize (allowNaturalHoles := true)
let h := e.getAppFn
if h.isFVar then
let localDecl ← h.fvarId!.getDecl
let mvarId ← (← getMainGoal).assert localDecl.userName (← inferType e).headBeta e
let (_, mvarId) ← mvarId.intro1P
let mvarId ← mvarId.tryClear h.fvarId!
replaceMainGoal (mvarIds' ++ [mvarId])
if let Expr.fvar fvarId := e.getLambdaBody.getAppFn then
let mvarId ← getMainGoal
let result ← mvarId.replace fvarId e (← inferType e).headBeta
replaceMainGoal (mvarIds' ++ [result.mvarId])
else
throwError "'specialize' requires a term of the form `h x_1 .. x_n` where `h` appears in the local context"
| _ => throwUnsupportedSyntax

View file

@ -1621,6 +1621,15 @@ def getNumHeadLambdas : Expr → Nat
| .mdata _ b => getNumHeadLambdas b
| _ => 0
/--
Returns the "body" of a nested lambda expression, like in `Expr.getForallBody`.
The number of skipped lambdas is given by `Expr.getNumHeadLambdas`.
-/
def getLambdaBody : Expr → Expr
| .lam _ _ b _ => getLambdaBody b
| .mdata _ b => getLambdaBody b
| e => e
/--
Return true if the given expression is the function of an expression that is target for (head) beta reduction.
If `useZeta = true`, then `let`-expressions are visited. That is, it assumes

View file

@ -476,6 +476,9 @@ def setKind (lctx : LocalContext) (fvarId : FVarId)
def setBinderInfo (lctx : LocalContext) (fvarId : FVarId) (bi : BinderInfo) : LocalContext :=
modifyLocalDecl lctx fvarId fun decl => decl.setBinderInfo bi
def setType (lctx : LocalContext) (fvarId : FVarId) (type : Expr) : LocalContext :=
modifyLocalDecl lctx fvarId fun decl => decl.setType type
@[export lean_local_ctx_num_indices]
def numIndices (lctx : LocalContext) : Nat :=
lctx.decls.size

View file

@ -9,6 +9,7 @@ prelude
public import Lean.Meta.Tactic.FVarSubst
public import Lean.Meta.Tactic.Intro
public import Lean.Meta.Tactic.Revert
public import Lean.Elab.InfoTree.Main
public import Lean.Util.ForEachExpr
import Lean.Meta.AppBuilder
@ -78,11 +79,51 @@ def _root_.Lean.MVarId.assertAfter (mvarId : MVarId) (fvarId : FVarId) (userName
let mvarId ← mvarId.assert userName type val
let (fvarIdNew, mvarId) ← mvarId.intro1P
let (fvarIdsNew, mvarId) ← mvarId.introNP fvarIds.size
let lctx := (← mvarId.getDecl).lctx
let mut subst := {}
for f in fvarIds, fNew in fvarIdsNew do
subst := subst.insert f (mkFVar fNew)
Elab.pushInfoLeaf (.ofFVarAliasInfo { id := fNew, baseId := f, userName := (lctx.get! fNew).userName })
return { fvarId := fvarIdNew, mvarId, subst }
/--
Like `Lean.MVarId.assertAfter`, but asserts the new hypothesis at the earliest point after `fvarId`
where `type` is well-formed. Note that `val` may depend on any variables in the local context.
The expression `type` may contain metavariables, and this procedure ensures they are well-formed
at the point in the local context where the hypothesis is asserted.
The metavariables in `type` are instantiated to avoid false dependencies.
-/
def _root_.Lean.MVarId.assertAfter' (mvarId : MVarId) (fvarId : FVarId) (userName : Name) (type : Expr) (val : Expr) :
MetaM AssertAfterResult :=
mvarId.withContext do
-- Instantiate metavariables, which possibly allows the asserted hypothesis to appear earlier.
-- Assigned metavariables can create false dependences on variables.
let type ← instantiateMVars type
-- `type` may contain variables that occur after `fvarId`.
-- Thus, we use the auxiliary function `findMaxFVar` to ensure `type` is well-formed
-- at the position we are inserting it.
let (_, ldecl') ← findMaxFVar type |>.run (← fvarId.getDecl)
mvarId.assertAfter ldecl'.fvarId userName type val
where
/-- Finds the `LocalDecl` for the FVar in `e` with the highest index. -/
findMaxFVar (e : Expr) : StateRefT LocalDecl MetaM Unit :=
e.forEach' fun e => do
if let Expr.fvar fvarId' := e then
visitLocalDecl (← fvarId'.getDecl)
return false
else if let Expr.mvar mvarId' := e then
-- Metavariables need to appear after all local variables appearing in their own local contexts
let lctx' := (← mvarId'.getDecl).lctx
lctx'.forM fun ldecl' => do
-- We need the corresponding `LocalDecl` from the current context, to get the correct `index`
visitLocalDecl (← ldecl'.fvarId.getDecl)
return false
else
return e.hasFVar || e.hasExprMVar
visitLocalDecl (ldecl' : LocalDecl) : StateRefT LocalDecl MetaM Unit :=
modify fun ldecl => if ldecl'.index > ldecl.index then ldecl' else ldecl
structure Hypothesis where
userName : Name
type : Expr
@ -117,34 +158,4 @@ def _root_.Lean.MVarId.assertHypotheses (mvarId : MVarId) (hs : Array Hypothesis
pure lctx
return (fvarIds, mvarId)
/--
Replace hypothesis `hyp` in goal `g` with `proof : typeNew`.
The new hypothesis is given the same user name as the original,
it attempts to avoid reordering hypotheses, and the original is cleared if possible.
-/
-- adapted from Lean.Meta.replaceLocalDeclCore
def _root_.Lean.MVarId.replace (g : MVarId) (hyp : FVarId) (proof : Expr) (typeNew : Option Expr := none) :
MetaM AssertAfterResult :=
g.withContext do
let typeNew ← match typeNew with
| some t => pure t
| none => inferType proof
let ldecl ← hyp.getDecl
-- `typeNew` may contain variables that occur after `hyp`.
-- Thus, we use the auxiliary function `findMaxFVar` to ensure `typeNew` is well-formed
-- at the position we are inserting it.
let (_, ldecl') ← findMaxFVar typeNew |>.run ldecl
let result ← g.assertAfter ldecl'.fvarId ldecl.userName typeNew proof
(return { result with mvarId := ← result.mvarId.clear hyp }) <|> pure result
where
/-- Finds the `LocalDecl` for the FVar in `e` with the highest index. -/
findMaxFVar (e : Expr) : StateRefT LocalDecl MetaM Unit :=
e.forEach' fun e => do
if e.isFVar then
let ldecl' ← e.fvarId!.getDecl
modify fun ldecl => if ldecl'.index > ldecl.index then ldecl' else ldecl
return false
else
return e.hasFVar
end Lean.Meta

View file

@ -49,35 +49,39 @@ def _root_.Lean.MVarId.replaceTargetDefEq (mvarId : MVarId) (targetNew : Expr) :
if Expr.equal target targetNew then
return mvarId
else
let tag ← mvarId.getTag
let mvarNew ← mkFreshExprSyntheticOpaqueMVar targetNew tag
let newVal ← mkExpectedTypeHint mvarNew target
mvarId.assign newVal
return mvarNew.mvarId!
private def replaceLocalDeclCore (mvarId : MVarId) (fvarId : FVarId) (typeNew : Expr) (eqProof : Expr) : MetaM AssertAfterResult :=
mvarId.withContext do
let localDecl ← fvarId.getDecl
let typeNewPr ← mkEqMP eqProof (mkFVar fvarId)
/- `typeNew` may contain variables that occur after `fvarId`.
Thus, we use the auxiliary function `findMaxFVar` to ensure `typeNew` is well-formed at the
position we are inserting it.
We must `instantiateMVars` first to ensure that there is no mvar in `typeNew` which is
assigned to some later-occurring fvar. -/
let (_, localDecl') ← findMaxFVar (← instantiateMVars typeNew) |>.run localDecl
let result ← mvarId.assertAfter localDecl'.fvarId localDecl.userName typeNew typeNewPr
(do let mvarIdNew ← result.mvarId.clear fvarId
pure { result with mvarId := mvarIdNew })
<|> pure result
where
findMaxFVar (e : Expr) : StateRefT LocalDecl MetaM Unit :=
e.forEach' fun e => do
if e.isFVar then
let localDecl' ← e.fvarId!.getDecl
modify fun localDecl => if localDecl'.index > localDecl.index then localDecl' else localDecl
return false
-- For an accurate `Expr.equal` check, we need to instantiate metavariables.
-- Some tactics depend on this returning the same metavariable to check that they made no progress.
let target ← instantiateMVars target
let targetNew ← instantiateMVars targetNew
if Expr.equal target targetNew then
mvarId.setType target -- cache the instantiated type
return mvarId
else
return e.hasFVar
let tag ← mvarId.getTag
let mvarNew ← mkFreshExprSyntheticOpaqueMVar targetNew tag
let newVal ← mkExpectedTypeHint mvarNew target
mvarId.assign newVal
return mvarNew.mvarId!
/--
Given a hypothesis `fvarId` named `h`, asserts a new hypothesis `h : type`
as soon after `fvarId` as possible, then tries to clear `h`. This is useful to avoid
reordering hypotheses.
The new hypothesis is asserted after any local variables that `type` depends on, and it may contain
metavariables. The value `val : type` can depend on any variables in the local context.
See also `Lean.MVarId.assertAfter'`, which asserts hypotheses without clearing.
-/
def _root_.Lean.MVarId.replace
(mvarId : MVarId) (fvarId : FVarId) (val : Expr) (type? : Option Expr := none) (userName? : Option Name := none) :
MetaM AssertAfterResult :=
mvarId.withContext do
let type ← type?.getDM (inferType val)
let userName ← userName?.getDM (LocalDecl.userName <$> fvarId.getDecl)
let result ← mvarId.assertAfter' fvarId userName type val
let mvarId ← result.mvarId.tryClear fvarId
return { result with mvarId }
/--
Replace type of the local declaration with id `fvarId` with one with the same user-facing name, but with type `typeNew`.
@ -89,35 +93,43 @@ where
well-formed. That is, if `typeNew` involves declarations which occur later than `fvarId` in the
local context, the new local declaration will be inserted immediately after the latest-occurring
one. Otherwise, it will be inserted immediately after `fvarId`.
Note: `replaceLocalDecl` should not be used when unassigned pending mvars might be present in
`typeNew`, as these may later be synthesized to fvars which occur after `fvarId` (by e.g.
`Term.withSynthesize` or `Term.synthesizeSyntheticMVars`) .
-/
@[inline] def _root_.Lean.MVarId.replaceLocalDecl (mvarId : MVarId) (fvarId : FVarId) (typeNew : Expr) (eqProof : Expr) : MetaM AssertAfterResult :=
replaceLocalDeclCore mvarId fvarId typeNew eqProof
mvarId.withContext do
let typeNewPr ← mkEqMP eqProof (mkFVar fvarId)
mvarId.replace fvarId typeNewPr typeNew
/--
Replaces the type of `fvarId` at `mvarId` with `typeNew`.
Remark: this method assumes that `typeNew` is definitionally equal to the current type of `fvarId`.
Remark: this method assumes that `typeNew` is definitionally equal to the current type of `fvarId`,
and that `typeNew` is well-formed at this position.
If `typeNew` is equal to current type of `fvarId`, then returns `mvarId` unchanged.
Uses `Expr.equal` for the comparison so that it is possible to update binder names, etc., which are user-visible.
-/
def _root_.Lean.MVarId.replaceLocalDeclDefEq (mvarId : MVarId) (fvarId : FVarId) (typeNew : Expr) : MetaM MVarId := do
mvarId.withContext do
if Expr.equal typeNew (← fvarId.getType) then
let type ← fvarId.getType
if Expr.equal type typeNew then
return mvarId
else
let mvarDecl ← mvarId.getDecl
let lctxNew := (← getLCtx).modifyLocalDecl fvarId (·.setType typeNew)
withLCtx' lctxNew do
-- `typeNew` might not be defeq to the old type at reducible transparency (e.g. a definition was unfolded)
-- so it might now be recognized as an instance.
withLocalInstances [lctxNew.get! fvarId] do
let mvarNew ← mkFreshExprMVar mvarDecl.type mvarDecl.kind mvarDecl.userName
mvarId.assign mvarNew
return mvarNew.mvarId!
-- For an accurate `Expr.equal` check, we need to instantiate metavariables.
-- Some tactics depend on this returning the same metavariable to check that they made no progress.
let type ← instantiateMVars type
let typeNew ← instantiateMVars typeNew
if Expr.equal type typeNew then
mvarId.setFVarType fvarId type -- cache the instantiated type
return mvarId
else
let lctxNew := (← getLCtx).setType fvarId typeNew
withLCtx' lctxNew do
-- `typeNew` might not be defeq to the old type at reducible transparency (e.g. a definition was unfolded)
-- so it might now be recognized as an instance.
withLocalInstances [lctxNew.get! fvarId] do
let mvarDecl ← mvarId.getDecl
let mvarNew ← mkFreshExprMVar mvarDecl.type mvarDecl.kind mvarDecl.userName
mvarId.assign mvarNew
return mvarNew.mvarId!
/--
Replace the target type of `mvarId` with `typeNew`.

View file

@ -6,7 +6,7 @@ Authors: Siddhartha Gadgil, Mario Carneiro, Kim Morrison
module
prelude
public import Lean.Meta.Reduce
public import Lean.Meta.Tactic.Assert
public import Lean.Meta.Tactic.Replace
public import Lean.Meta.DiscrTree.Main
import Lean.Meta.AppBuilder
public section

View file

@ -911,6 +911,10 @@ def setFVarBinderInfo (mctx : MetavarContext) (mvarId : MVarId)
(fvarId : FVarId) (bi : BinderInfo) : MetavarContext :=
mctx.modifyExprMVarLCtx mvarId (·.setBinderInfo fvarId bi)
def setFVarType (mctx : MetavarContext) (mvarId : MVarId)
(fvarId : FVarId) (type : Expr) : MetavarContext :=
mctx.modifyExprMVarLCtx mvarId (·.setType fvarId type)
def findLevelDepth? (mctx : MetavarContext) (mvarId : LMVarId) : Option Nat :=
(mctx.findLevelDecl? mvarId).map LevelMetavarDecl.depth
@ -1535,6 +1539,14 @@ def setFVarBinderInfo [MonadMCtx m] (mvarId : MVarId) (fvarId : FVarId)
(bi : BinderInfo) : m Unit :=
modifyMCtx (·.setFVarBinderInfo mvarId fvarId bi)
/--
Set the `BinderInfo` of an fvar. If the given metavariable is not declared or
the given fvar doesn't exist in its context, nothing happens.
-/
def setFVarType [MonadMCtx m] (mvarId : MVarId) (fvarId : FVarId)
(type : Expr) : m Unit :=
modifyMCtx (·.setFVarType mvarId fvarId type)
end MVarId
end Lean

View file

@ -206,7 +206,7 @@ def diffHypothesesBundle (useAfter : Bool) (ctx₀ : LocalContext) (h₁ : Inte
if !(ctx₀.contains fvid) then
if let some decl₀ := ctx₀.findFromUserName? (.mkSimple ppName) then
-- on ctx₀ there is an fvar with the same name as this one.
let t₀ := decl₀.type
let t₀ ← instantiateMVars decl₀.type
return ← withTypeDiff t₀ h₁
else
if useAfter then
@ -219,7 +219,7 @@ where
withTypeDiff (t₀ : Expr) (h₁ : InteractiveHypothesisBundle) : MetaM InteractiveHypothesisBundle := do
let some x₁ := h₁.fvarIds[0]?
| throwError "internal error: empty fvar list!"
let t₁ ← inferType <| Expr.fvar x₁
let t₁ ← instantiateMVars =<< inferType (Expr.fvar x₁)
let tδ ← exprDiff t₀ t₁ useAfter
let c₁ ← addDiffTags useAfter tδ h₁.type
return {h₁ with type := c₁}

View file

@ -1511,8 +1511,7 @@ theorem any_toList {p : (_ : α) → β → Bool} :
simp only [List.forIn'_cons, Id.run_bind, List.any_cons]
by_cases h : p hd.fst hd.snd = true
· simp [h]
· simp only at ih
simp [h, ih]
· simp [h, ih]
theorem any_eq_true [LawfulHashable α] [EquivBEq α] {p : (_ : α) → β → Bool} (h : m.1.WF) :
m.1.any p = true ↔ ∃ (a : α) (h : m.contains a), p (m.getKey a h) (Const.get m a h) := by
@ -1576,8 +1575,7 @@ theorem all_toList {p : (_ : α) → β → Bool} :
simp only [List.forIn'_cons, Id.run_bind, List.all_cons]
by_cases h : p hd.fst hd.snd = false
· simp [h]
· simp only at ih
simp [h, ih]
· simp [h, ih]
theorem all_eq_true [EquivBEq α] [LawfulHashable α] {p : (a : α) → β → Bool} (h : m.1.WF) :
m.1.all p = true ↔ ∀ (a : α) (h : m.contains a), p (m.getKey a h) (Const.get m a h) := by

View file

@ -43,7 +43,6 @@ theorem mem_of_necessary_assignment {n : Nat} {p : (PosFin n) → Bool} {c : Def
next hne => simp [h] at pv
· specialize p'_not_entails_c v
have h := p'_not_entails_c.2 v_in_c
simp only at h
split at h
next heq => simp [Literal.negate, ← heq, h, v_in_c]
next hne => simp [h] at pv
@ -175,7 +174,6 @@ theorem assignmentsInvariant_insertRatUnits {n : Nat} (f : DefaultFormula n)
· rfl
have hp1 := hp j1_unit ((Or.inr ∘ Or.inr) j1_unit_in_insertRatUnits_res)
have hp2 := hp j2_unit ((Or.inr ∘ Or.inr) j2_unit_in_insertRatUnits_res)
simp only at hp1 hp2
rcases hp1 with ⟨i1, hp1⟩
rcases hp2 with ⟨i2, hp2⟩
simp only [Fin.getElem_fin] at h1 h2

View file

@ -728,7 +728,6 @@ theorem sat_of_confirmRupHint_insertRup_fold {n : Nat} (f : DefaultFormula n)
specialize pc v
rw [v'_eq_v] at v'_in_c
have pv := pc.2 v'_in_c
simp only at pv
simp only [p_unsat_c] at pv
cases pv
· simp only [negate_eq, List.mem_map, Prod.exists, Bool.exists_bool] at v_in_neg_c
@ -742,7 +741,6 @@ theorem sat_of_confirmRupHint_insertRup_fold {n : Nat} (f : DefaultFormula n)
specialize pc v
rw [v'_eq_v] at v'_in_c
have pv := pc.1 v'_in_c
simp only at pv
simp only [p_unsat_c] at pv
cases pv
· grind [Literal.negate]

90
tests/elab/9893.lean Normal file
View file

@ -0,0 +1,90 @@
import Lean.Elab.Tactic
open Lean Elab Tactic
/-!
# `MVarId.replaceLocalDeclDefEq` should do equality checks after instantiating metavariables
https://github.com/leanprover/lean4/issues/9893
-/
set_option linter.unusedVariables false
elab "test_change_to" t:term "at" h:ident : tactic => withMainContext do
let e : Expr ← elabTerm t none
let fvarId ← getFVarId h
let ty ← fvarId.getType
logInfo m!"{h} has metavariables: {ty.hasExprMVar}"
liftMetaTactic1 fun g ↦ do
let g' ← g.replaceLocalDeclDefEq fvarId e
logInfo m!"old goal equals new goal: {g == g'}"
return g'
/-!
This always output 'true'
-/
/--
info: h has metavariables: false
---
info: old goal equals new goal: true
---
trace: x y : Nat
h : x ≤ y
⊢ True
-/
#guard_msgs in
example {x y : Nat} (h : x ≤ y) : True := by
test_change_to x ≤ y at h
trace_state
exact trivial
/-!
This used to output 'false'
-/
/--
info: h has metavariables: true
---
info: old goal equals new goal: true
---
trace: x y : Nat
h : x ≤ y
⊢ True
---
trace: x y : Nat
h : x ≤ y
⊢ True
---
warning: declaration uses `sorry`
-/
#guard_msgs in
example {x y : Nat} : True := by
have h : ?foo := sorry
case foo => refine x ≤ ?baz; exact y
trace_state
test_change_to x ≤ y at h
trace_state
exact trivial
/-!
Make sure the goal changes when the hypothesis changes.
-/
/--
info: h has metavariables: true
---
info: old goal equals new goal: false
---
trace: x y : Nat
h : x ≤ y
⊢ True
---
trace: x y : Nat
h : x ≥ y
⊢ True
---
warning: declaration uses `sorry`
-/
#guard_msgs in
example {x y : Nat} : True := by
have h : x ≤ y := sorry
trace_state
test_change_to x ≥ y at h
trace_state
exact trivial

View file

@ -16,6 +16,7 @@ example (B C : Prop) (f : forall {A : Prop}, A → C) (x : B) : C := by
example (X : Type) [Add X] (f : forall {A : Type} [Add A], A → A → A) (x : X) : X := by
specialize f x x
clear x
assumption
def ex (f : Nat → Nat → Nat) : Nat := by

View file

@ -3387,8 +3387,6 @@ theorem toInt_ediv_toInt_lt_of_nonpos_of_lt_neg_one {w : Nat} {x y : BitVec w} (
rcases hx' with hx'|hx'|hx'
· simp [hx']; omega
· have := BitVec.neg_one_ediv_toInt_eq (y := y)
simp only [
Int.reduceNeg] at this
simp [hx', this]
omega
· have := Int.ediv_lt_natAbs_self_of_lt_neg_one_of_lt_neg_one (x := x.toInt) (y := y.toInt) (by omega) hy