lean4-htt/src/Lean/Meta/Tactic/Rfl.lean
Joachim Breitner fc963ffceb
feat: apply_rfl tactic: handle Eq, HEq, better error messages (#3714)
This implements the first half of #3302: It improves the extensible
`apply_rfl` tactic (the one that looks at `refl` attributes, part of
the `rfl` macro) to

* Check itself and ahead of time that the lhs and rhs are defEq, and
give
a nice consistent error message when they don't (instead of just passing
on
  the less helpful error message from `apply Foo.refl`), and using the 
machinery that `apply` uses to elaborate expressions to highlight diffs
  in implicit arguments.

* Also handle `Eq` and `HEq` (built in) and `Iff` (using the attribute)

Care is taken that, as before, the current transparency setting affects
comparing the lhs and rhs, but not the reduction of the relation

So before we had

```lean
opaque P : Nat → Nat → Prop
@[refl] axiom P.refl (n : Nat) : P n n

/--
error: tactic 'apply' failed, failed to unify
  P ?n ?n
with
  P 42 23
⊢ P 42 23
-/
#guard_msgs in
example : P 42 23 := by apply_rfl

opaque withImplicitNat {n : Nat} : Nat

/--
error: tactic 'apply' failed, failed to unify
  P ?n ?n
with
  P withImplicitNat withImplicitNat
⊢ P withImplicitNat withImplicitNat
-/
#guard_msgs in
example : P (@withImplicitNat 42) (@withImplicitNat 23) := by apply_rfl
```

and with this PR the messages we get are

```
error: tactic 'apply_rfl' failed, The lhs
  42
is not definitionally equal to rhs
  23
⊢ P 42 23
```
resp.
```
error: tactic 'apply_rfl' failed, The lhs
  @withImplicitNat 42
is not definitionally equal to rhs
  @withImplicitNat 23
⊢ P withImplicitNat withImplicitNat
```

A test file checks the various failure modes and error messages.

I believe this `apply_rfl` can serve as the only implementation of
`rfl`, which would then complete #3302, and actually expose these
improved
error messages to the user. But as that seems to require a
non-trivial bootstrapping dance, it’ll be separate.
2024-09-20 08:25:10 +00:00

136 lines
5 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) 2022 Newell Jensen. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Newell Jensen, Thomas Murrills
-/
prelude
import Lean.Meta.Tactic.Apply
import Lean.Elab.Tactic.Basic
import Lean.Meta.Tactic.Refl
/-!
# `rfl` tactic extension for reflexive relations
This extends the `rfl` tactic so that it works on any reflexive relation,
provided the reflexivity lemma has been marked as `@[refl]`.
-/
namespace Lean.Meta.Rfl
open Lean Meta
/-- Discrimation tree settings for the `refl` extension. -/
def reflExt.config : WhnfCoreConfig := {}
/-- Environment extensions for `refl` lemmas -/
initialize reflExt :
SimpleScopedEnvExtension (Name × Array DiscrTree.Key) (DiscrTree Name) ←
registerSimpleScopedEnvExtension {
addEntry := fun dt (n, ks) => dt.insertCore ks n
initial := {}
}
initialize registerBuiltinAttribute {
name := `refl
descr := "reflexivity relation"
add := fun decl _ kind => MetaM.run' do
let declTy := (← getConstInfo decl).type
let (_, _, targetTy) ← withReducible <| forallMetaTelescopeReducing declTy
let fail := throwError
"@[refl] attribute only applies to lemmas proving x x, got {declTy}"
let .app (.app rel lhs) rhs := targetTy | fail
if let .app (.const ``Eq [_]) _ := rel then
throwError "@[refl] attribute may not be used on `Eq.refl`."
unless ← withNewMCtxDepth <| isDefEq lhs rhs do fail
let key ← DiscrTree.mkPath rel reflExt.config
reflExt.add (decl, key) kind
}
open Elab Tactic
/-- `MetaM` version of the `rfl` tactic.
This tactic applies to a goal whose target has the form `x ~ x`, where `~` is a reflexive
relation, that is, equality or another relation which has a reflexive lemma tagged with the
attribute [refl].
-/
def _root_.Lean.MVarId.applyRfl (goal : MVarId) : MetaM Unit := goal.withContext do
-- NB: uses whnfR, we do not want to unfold the relation itself
let t ← whnfR <|← instantiateMVars <|← goal.getType
if t.getAppNumArgs < 2 then
throwError "rfl can only be used on binary relations, not{indentExpr (← goal.getType)}"
-- Special case HEq here as it has a different argument order.
if t.isAppOfArity ``HEq 4 then
let gs ← goal.applyConst ``HEq.refl
unless gs.isEmpty do
throwError MessageData.tagged `Tactic.unsolvedGoals <| m!"unsolved goals\n{
goalsToMessageData gs}"
return
let rel := t.appFn!.appFn!
let lhs := t.appFn!.appArg!
let rhs := t.appArg!
let success ← approxDefEq <| isDefEqGuarded lhs rhs
unless success do
let explanation := MessageData.ofLazyM (es := #[lhs, rhs]) do
let (lhs, rhs) ← addPPExplicitToExposeDiff lhs rhs
return m!"The lhs{indentExpr lhs}\nis not definitionally equal to rhs{indentExpr rhs}"
throwTacticEx `apply_rfl goal explanation
if rel.isAppOfArity `Eq 1 then
-- The common case is equality: just use `Eq.refl`
let us := rel.appFn!.constLevels!
let α := rel.appArg!
goal.assign (mkApp2 (mkConst ``Eq.refl us) α lhs)
else
-- Else search through `@refl` keyed by the relation
let s ← saveState
let mut ex? := none
for lem in ← (reflExt.getState (← getEnv)).getMatch rel reflExt.config do
try
let gs ← goal.apply (← mkConstWithFreshMVarLevels lem)
if gs.isEmpty then return () else
throwError MessageData.tagged `Tactic.unsolvedGoals <| m!"unsolved goals\n{
goalsToMessageData gs}"
catch e =>
unless ex?.isSome do
ex? := some (← saveState, e) -- stash the first failure of `apply`
s.restore
if let some (sErr, e) := ex? then
sErr.restore
throw e
else
throwError "rfl failed, no @[refl] lemma registered for relation{indentExpr rel}"
/-- Helper theorem for `Lean.MVarId.liftReflToEq`. -/
private theorem rel_of_eq_and_refl {α : Sort _} {R : αα → Prop}
{x y : α} (hxy : x = y) (h : R x x) : R x y :=
hxy ▸ h
/--
Convert a goal of the form `x ~ y` into the form `x = y`, where `~` is a reflexive
relation, that is, a relation which has a reflexive lemma tagged with the attribute `@[refl]`.
If this can't be done, returns the original `MVarId`.
-/
def _root_.Lean.MVarId.liftReflToEq (mvarId : MVarId) : MetaM MVarId := do
mvarId.checkNotAssigned `liftReflToEq
let .app (.app rel _) _ ← withReducible mvarId.getType' | return mvarId
if rel.isAppOf `Eq then
-- No need to lift Eq to Eq
return mvarId
for lem in ← (reflExt.getState (← getEnv)).getMatch rel reflExt.config do
let res ← observing? do
-- First create an equality relating the LHS and RHS
-- and reduce the goal to proving that LHS is related to LHS.
let [mvarIdEq, mvarIdR] ← mvarId.apply (← mkConstWithFreshMVarLevels ``rel_of_eq_and_refl)
| failure
-- Then fill in the proof of the latter by reflexivity.
let [] ← mvarIdR.apply (← mkConstWithFreshMVarLevels lem) | failure
return mvarIdEq
if let some mvarIdEq := res then
return mvarIdEq
return mvarId
end Lean.Meta.Rfl