Skip to content

Commit 86283cc

Browse files
leodemouraJovanGerb
authored andcommitted
feat: canonicalizer diagnostics (leanprover#6662)
This PR improves the canonicalizer used in the `grind` tactic and the diagnostics it produces. It also adds a new configuration option, `canonHeartbeats`, to address (some of) the issues. Here is an example illustrating the new diagnostics, where we intentionally create a problem by using a very small number of heartbeats. <img width="1173" alt="image" src="https://github.com/user-attachments/assets/484005c8-dcaa-4164-8fbf-617864ed7350" />
1 parent 8744c90 commit 86283cc

File tree

6 files changed

+155
-103
lines changed

6 files changed

+155
-103
lines changed

src/Init/Grind/Tactics.lean

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ structure Config where
4747
splitIndPred : Bool := true
4848
/-- By default, `grind` halts as soon as it encounters a sub-goal where no further progress can be made. -/
4949
failures : Nat := 1
50+
/-- Maximum number of heartbeats (in thousands) the canonicalizer can spend per definitional equality test. -/
51+
canonHeartbeats : Nat := 1000
5052
deriving Inhabited, BEq
5153

5254
end Lean.Grind

src/Lean/Meta/Tactic/Grind/Canon.lean

+48-51
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Lean.Meta.FunInfo
1010
import Lean.Util.FVarSubset
1111
import Lean.Util.PtrSet
1212
import Lean.Util.FVarSubset
13+
import Lean.Meta.Tactic.Grind.Types
1314

1415
namespace Lean.Meta.Grind
1516
namespace Canon
@@ -40,42 +41,37 @@ additions will still use structurally different (and definitionally different) i
4041
Furthermore, `grind` will not be able to infer that `HEq (a + a) (b + b)` even if we add the assumptions `n = m` and `HEq a b`.
4142
-/
4243

43-
structure State where
44-
argMap : PHashMap (Expr × Nat) (List Expr) := {}
45-
canon : PHashMap Expr Expr := {}
46-
proofCanon : PHashMap Expr Expr := {}
47-
deriving Inhabited
44+
@[inline] private def get' : GoalM State :=
45+
return (← get).canon
4846

49-
inductive CanonElemKind where
50-
| /--
51-
Type class instances are canonicalized using `TransparencyMode.instances`.
52-
-/
53-
instance
54-
| /--
55-
Types and Type formers are canonicalized using `TransparencyMode.default`.
56-
Remark: propositions are just visited. We do not invoke `canonElemCore` for them.
57-
-/
58-
type
59-
| /--
60-
Implicit arguments that are not types, type formers, or instances, are canonicalized
61-
using `TransparencyMode.reducible`
62-
-/
63-
implicit
64-
deriving BEq
47+
@[inline] private def modify' (f : State → State) : GoalM Unit :=
48+
modify fun s => { s with canon := f s.canon }
6549

66-
def CanonElemKind.explain : CanonElemKind → String
67-
| .instance => "type class instances"
68-
| .type => "types (or type formers)"
69-
| .implicit => "implicit arguments (which are not type class instances or types)"
50+
/--
51+
Helper function for `canonElemCore`. It tries `isDefEq a b` with default transparency, but using
52+
at most `canonHeartbeats` heartbeats. It reports an issue if the threshold is reached.
53+
Remark: `parent` is use only to report an issue
54+
-/
55+
private def isDefEqBounded (a b : Expr) (parent : Expr) : GoalM Bool := do
56+
withCurrHeartbeats do
57+
let config ← getConfig
58+
tryCatchRuntimeEx
59+
(withTheReader Core.Context (fun ctx => { ctx with maxHeartbeats := config.canonHeartbeats }) do
60+
withDefault <| isDefEq a b)
61+
fun ex => do
62+
if ex.isRuntime then
63+
let curr := (← getConfig).canonHeartbeats
64+
reportIssue m!"failed to show that{indentExpr a}\nis definitionally equal to{indentExpr b}\nwhile canonicalizing{indentExpr parent}\nusing `{curr}*1000` heartbeats, `(canonHeartbeats := {curr})`"
65+
return false
66+
else
67+
throw ex
7068

7169
/--
7270
Helper function for canonicalizing `e` occurring as the `i`th argument of an `f`-application.
73-
74-
Thus, if diagnostics are enabled, we also re-check them using `TransparencyMode.default`. If the result is different
75-
we report to the user.
71+
If `useIsDefEqBounded` is `true`, we try `isDefEqBounded` before returning false
7672
-/
77-
def canonElemCore (f : Expr) (i : Nat) (e : Expr) (kind : CanonElemKind) : StateT State MetaM Expr := do
78-
let s ← get
73+
def canonElemCore (parent : Expr) (f : Expr) (i : Nat) (e : Expr) (useIsDefEqBounded : Bool) : GoalM Expr := do
74+
let s ← get'
7975
if let some c := s.canon.find? e then
8076
return c
8177
let key := (f, i)
@@ -87,20 +83,21 @@ def canonElemCore (f : Expr) (i : Nat) (e : Expr) (kind : CanonElemKind) : State
8783
-- However, we don't revert previously canonicalized elements in the `grind` tactic.
8884
-- Moreover, we store the canonicalizer state in the `Goal` because we case-split
8985
-- and different locals are added in different branches.
90-
modify fun s => { s with canon := s.canon.insert e c }
91-
trace[grind.debug.canon] "found {e} ===> {c}"
86+
modify' fun s => { s with canon := s.canon.insert e c }
87+
trace[grind.debugn.canon] "found {e} ===> {c}"
9288
return c
93-
if kind != .type then
94-
if (← isTracingEnabledFor `grind.issues <&&> (withDefault <| isDefEq e c)) then
95-
-- TODO: consider storing this information in some structure that can be browsed later.
96-
trace[grind.issues] "the following {kind.explain} are definitionally equal with `default` transparency but not with a more restrictive transparency{indentExpr e}\nand{indentExpr c}"
89+
if useIsDefEqBounded then
90+
if (← isDefEqBounded e c parent) then
91+
modify' fun s => { s with canon := s.canon.insert e c }
92+
trace[grind.debugn.canon] "found using `isDefEqBounded`: {e} ===> {c}"
93+
return c
9794
trace[grind.debug.canon] "({f}, {i}) ↦ {e}"
98-
modify fun s => { s with canon := s.canon.insert e e, argMap := s.argMap.insert key (e::cs) }
95+
modify' fun s => { s with canon := s.canon.insert e e, argMap := s.argMap.insert key (e::cs) }
9996
return e
10097

101-
abbrev canonType (f : Expr) (i : Nat) (e : Expr) := withDefault <| canonElemCore f i e .type
102-
abbrev canonInst (f : Expr) (i : Nat) (e : Expr) := withReducibleAndInstances <| canonElemCore f i e .instance
103-
abbrev canonImplicit (f : Expr) (i : Nat) (e : Expr) := withReducible <| canonElemCore f i e .implicit
98+
abbrev canonType (parent f : Expr) (i : Nat) (e : Expr) := withDefault <| canonElemCore parent f i e (useIsDefEqBounded := false)
99+
abbrev canonInst (parent f : Expr) (i : Nat) (e : Expr) := withReducibleAndInstances <| canonElemCore parent f i e (useIsDefEqBounded := true)
100+
abbrev canonImplicit (parent f : Expr) (i : Nat) (e : Expr) := withReducible <| canonElemCore parent f i e (useIsDefEqBounded := true)
104101

105102
/--
106103
Return type for the `shouldCanon` function.
@@ -148,10 +145,10 @@ def shouldCanon (pinfos : Array ParamInfo) (i : Nat) (arg : Expr) : MetaM Should
148145
else
149146
return .visit
150147

151-
unsafe def canonImpl (e : Expr) : StateT State MetaM Expr := do
148+
unsafe def canonImpl (e : Expr) : GoalM Expr := do
152149
visit e |>.run' mkPtrMap
153150
where
154-
visit (e : Expr) : StateRefT (PtrMap Expr Expr) (StateT State MetaM) Expr := do
151+
visit (e : Expr) : StateRefT (PtrMap Expr Expr) GoalM Expr := do
155152
unless e.isApp || e.isForall do return e
156153
-- Check whether it is cached
157154
if let some r := (← get).find? e then
@@ -161,11 +158,11 @@ where
161158
if f.isConstOf ``Lean.Grind.nestedProof && args.size == 2 then
162159
let prop := args[0]!
163160
let prop' ← visit prop
164-
if let some r := (← getThe State).proofCanon.find? prop' then
161+
if let some r := (← get').proofCanon.find? prop' then
165162
pure r
166163
else
167164
let e' := if ptrEq prop prop' then e else mkAppN f (args.set! 0 prop')
168-
modifyThe State fun s => { s with proofCanon := s.proofCanon.insert prop' e' }
165+
modify' fun s => { s with proofCanon := s.proofCanon.insert prop' e' }
169166
pure e'
170167
else
171168
let pinfos := (← getFunInfo f).paramInfo
@@ -175,9 +172,9 @@ where
175172
let arg := args[i]
176173
trace[grind.debug.canon] "[{repr (← shouldCanon pinfos i arg)}]: {arg} : {← inferType arg}"
177174
let arg' ← match (← shouldCanon pinfos i arg) with
178-
| .canonType => canonType f i arg
179-
| .canonInst => canonInst f i arg
180-
| .canonImplicit => canonImplicit f i (← visit arg)
175+
| .canonType => canonType e f i arg
176+
| .canonInst => canonInst e f i arg
177+
| .canonImplicit => canonImplicit e f i (← visit arg)
181178
| .visit => visit arg
182179
unless ptrEq arg arg' do
183180
args := args.set i arg'
@@ -193,11 +190,11 @@ where
193190
modify fun s => s.insert e e'
194191
return e'
195192

193+
end Canon
194+
196195
/-- Canonicalizes nested types, type formers, and instances in `e`. -/
197-
def canon (e : Expr) : StateT State MetaM Expr := do
196+
def canon (e : Expr) : GoalM Expr := do
198197
trace[grind.debug.canon] "{e}"
199-
unsafe canonImpl e
200-
201-
end Canon
198+
unsafe Canon.canonImpl e
202199

203200
end Lean.Meta.Grind

src/Lean/Meta/Tactic/Grind/Internalize.lean

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Lean.Meta.Match.MatcherInfo
1111
import Lean.Meta.Match.MatchEqsExt
1212
import Lean.Meta.Tactic.Grind.Types
1313
import Lean.Meta.Tactic.Grind.Util
14+
import Lean.Meta.Tactic.Grind.Canon
1415
import Lean.Meta.Tactic.Grind.Arith.Internalize
1516

1617
namespace Lean.Meta.Grind
@@ -98,13 +99,16 @@ private def pushCastHEqs (e : Expr) : GoalM Unit := do
9899
| [email protected] α a motive b h v => pushHEq e v (mkApp6 (mkConst ``Grind.eqRecOn_heq f.constLevels!) α a motive b h v)
99100
| _ => return ()
100101

102+
private def preprocessGroundPattern (e : Expr) : GoalM Expr := do
103+
shareCommon (← canon (← normalizeLevels (← unfoldReducible e)))
104+
101105
mutual
102106
/-- Internalizes the nested ground terms in the given pattern. -/
103107
private partial def internalizePattern (pattern : Expr) (generation : Nat) : GoalM Expr := do
104108
if pattern.isBVar || isPatternDontCare pattern then
105109
return pattern
106110
else if let some e := groundPattern? pattern then
107-
let e ← shareCommon (← canon (← normalizeLevels (← unfoldReducible e)))
111+
let e ← preprocessGroundPattern e
108112
internalize e generation none
109113
return mkGroundPattern e
110114
else pattern.withApp fun f args => do

src/Lean/Meta/Tactic/Grind/Simp.lean

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Lean.Meta.Tactic.Grind.Util
1111
import Lean.Meta.Tactic.Grind.Types
1212
import Lean.Meta.Tactic.Grind.DoNotSimp
1313
import Lean.Meta.Tactic.Grind.MarkNestedProofs
14+
import Lean.Meta.Tactic.Grind.Canon
1415

1516
namespace Lean.Meta.Grind
1617
/-- Simplifies the given expression using the `grind` simprocs and normalization theorems. -/

src/Lean/Meta/Tactic/Grind/Types.lean

+7-8
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import Lean.Meta.AbstractNestedProofs
1414
import Lean.Meta.Tactic.Simp.Types
1515
import Lean.Meta.Tactic.Util
1616
import Lean.Meta.Tactic.Grind.ENodeKey
17-
import Lean.Meta.Tactic.Grind.Canon
1817
import Lean.Meta.Tactic.Grind.Attr
1918
import Lean.Meta.Tactic.Grind.Arith.Types
2019
import Lean.Meta.Tactic.Grind.EMatchTheorem
@@ -333,6 +332,13 @@ structure NewFact where
333332
generation : Nat
334333
deriving Inhabited
335334

335+
/-- Canonicalizer state. See `Canon.lean` for additional details. -/
336+
structure Canon.State where
337+
argMap : PHashMap (Expr × Nat) (List Expr) := {}
338+
canon : PHashMap Expr Expr := {}
339+
proofCanon : PHashMap Expr Expr := {}
340+
deriving Inhabited
341+
336342
structure Goal where
337343
mvarId : MVarId
338344
canon : Canon.State := {}
@@ -402,13 +408,6 @@ abbrev GoalM := StateRefT Goal GrindM
402408
@[inline] def GoalM.run' (goal : Goal) (x : GoalM Unit) : GrindM Goal :=
403409
goal.mvarId.withContext do StateRefT'.run' (x *> get) goal
404410

405-
/-- Canonicalizes nested types, type formers, and instances in `e`. -/
406-
def canon (e : Expr) : GoalM Expr := do
407-
let canonS ← modifyGet fun s => (s.canon, { s with canon := {} })
408-
let (e, canonS) ← Canon.canon e |>.run canonS
409-
modify fun s => { s with canon := canonS }
410-
return e
411-
412411
def updateLastTag : GoalM Unit := do
413412
if (← isTracingEnabledFor `grind) then
414413
let currTag ← (← get).mvarId.getTag

0 commit comments

Comments
 (0)