Skip to content

Commit bd05f87

Browse files
authored
fix: grind proof instability (#10881)
This PR fixes a proof instability source in `grind`. We say a proof is *unstable* if minor changes in the `.lean` file containing the proof **affect** it.
1 parent 56f3ca6 commit bd05f87

File tree

6 files changed

+47
-17
lines changed

6 files changed

+47
-17
lines changed

src/Lean/Meta/Tactic/Grind/Arith/Cutsat/MBTC.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ private def getAssignmentExt? (e : Expr) : GoalM (Option Rat) := do
2222
return none
2323
else if type == Nat.mkType then
2424
-- TODO: improve this case.
25-
for parent in (← getParents e) do
25+
for parent in (← getParents e).elems do
2626
let_expr NatCast.natCast _ inst _ := parent | pure ()
2727
let_expr instNatCastInt := inst | pure ()
2828
return (← getAssignment? (← get) parent)

src/Lean/Meta/Tactic/Grind/Arith/ModelUtil.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Returns `true` if adding the assignment `e := v` to `a` will falsify any asserte
2222
-/
2323
private partial def satisfyDiseqs (goal : Goal) (a : Std.HashMap Expr Rat) (e : Expr) (v : Int) : Bool := Id.run do
2424
let some parents := goal.parents.find? { expr := e } | return true
25-
for parent in parents do
25+
for parent in parents.elems do
2626
let_expr Eq _ lhs rhs := parent | continue
2727
let some root := goal.getRoot? parent | continue
2828
if root.isConstOf ``False then

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ This is an auxiliary function performed while merging equivalence classes.
4848
-/
4949
private def removeParents (root : Expr) : GoalM ParentSet := do
5050
let parents ← getParents root
51-
for parent in parents do
51+
for parent in parents.elems do
5252
-- Recall that we may have `Expr.forallE` in `parents` because of `ForallProp.lean`
5353
if (← pure (isCongrRelevant parent) <&&> isCongrRoot parent) then
5454
trace_goal[grind.debug.parent] "remove: {parent}"
@@ -60,7 +60,7 @@ Reinserts parents into the congruence table and detect new equalities.
6060
This is an auxiliary function performed while merging equivalence classes.
6161
-/
6262
private def reinsertParents (parents : ParentSet) : GoalM Unit := do
63-
for parent in parents do
63+
for parent in parents.elems do
6464
if (← pure (isCongrRelevant parent) <&&> isCongrRoot parent) then
6565
trace_goal[grind.debug.parent] "reinsert: {parent}"
6666
addCongrTable parent
@@ -88,7 +88,7 @@ The modification time is used to decide which terms are considered during e-matc
8888
-/
8989
private partial def updateMT (root : Expr) : GoalM Unit := do
9090
let gmt := (← get).ematch.gmt
91-
for parent in (← getParents root) do
91+
for parent in (← getParents root).elems do
9292
let node ← getENode parent
9393
if node.mt < gmt then
9494
setENode parent { node with mt := gmt }
@@ -103,8 +103,8 @@ def propagateBeta (lams : Array Expr) (fns : Array Expr) : GoalM Unit := do
103103
let lamRoot ← getRoot lams.back!
104104
trace_goal[grind.debug.beta] "fns: {fns}, lams: {lams}"
105105
for fn in fns do
106-
trace_goal[grind.debug.beta] "fn: {fn}, parents: {(← getParents fn).toArray}"
107-
for parent in (← getParents fn) do
106+
trace_goal[grind.debug.beta] "fn: {fn}, parents: {(← getParents fn).elems}"
107+
for parent in (← getParents fn).elems do
108108
let mut args := #[]
109109
let mut curr := parent
110110
trace_goal[grind.debug.beta] "parent: {parent}"
@@ -230,7 +230,7 @@ where
230230
unless (← isInconsistent) do
231231
updateMT rhsRoot.self
232232
unless (← isInconsistent) do
233-
for parent in parents do
233+
for parent in parents.elems do
234234
propagateUp parent
235235
for e in toPropagateDown do
236236
propagateDown e

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def checkMatchCondParent (e : Expr) (parent : Expr) : GoalM Bool := do
6666

6767
def checkParents (e : Expr) : GoalM Unit := do
6868
if (← isRoot e) then
69-
for parent in (← getParents e) do
69+
for parent in (← getParents e).elems do
7070
if isMatchCond parent then
7171
unless (← checkMatchCondParent e parent) do
7272
throwError "e: {e}, parent: {parent}"

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -598,8 +598,40 @@ instance : BEq (CongrKey enodeMap) where
598598

599599
abbrev CongrTable (enodeMap : ENodeMap) := PHashSet (CongrKey enodeMap)
600600

601-
-- Remark: we cannot use pointer addresses here because we have to traverse the tree.
602-
abbrev ParentSet := Std.TreeSet Expr Expr.quickComp
601+
/-
602+
**Note**: If inserting elements in a `ParentSet` becomes a performance bottleneck,
603+
we can add an extra field `Std.HashSet ExprPtr` for detecting whether the `ParentSet` already
604+
contains an element or not.
605+
606+
**Note**: We used to implement `ParentSet`s as
607+
```abbrev ParentSet := Std.TreeSet Expr Expr.quickComp```
608+
This representation created proof stability issues.
609+
For example, we traverse this set to implement congruence closure.
610+
There is no non-determinism here, but the traversal depends on the `Expr`
611+
hash code, which is very sensitive to changes in a `.lean` file.
612+
Thus, minor changes may affect the proof found by `grind`. We found examples
613+
where proving the same goal multiple times in the same file produced different
614+
proofs.
615+
When we inspected the hash codes, they were completely different.
616+
Using `Expr.comp` does not help because it still relies on internal free variable IDs.
617+
One might think we can just reset them at the beginning of the `grind` search, but
618+
this is not sufficient. When tactics such as `finish?` generate the final tactic
619+
script, we remove unnecessary case splits. Removing case splits affects the generated
620+
free variable IDs, which in turn affects the result of Expr.comp :(
621+
-/
622+
structure ParentSet where
623+
parents : List Expr := []
624+
deriving Inhabited
625+
626+
def ParentSet.insert (ps : ParentSet) (p : Expr) : ParentSet :=
627+
{ ps with parents := ps.parents.insert p }
628+
629+
def ParentSet.isEmpty (ps : ParentSet) : Bool :=
630+
ps.parents.isEmpty
631+
632+
def ParentSet.elems (ps : ParentSet) : List Expr :=
633+
ps.parents
634+
603635
abbrev ParentMap := PHashMap ExprPtr ParentSet
604636

605637
/--
@@ -1124,7 +1156,7 @@ Copy `parents` to the parents of `root`.
11241156
-/
11251157
def copyParentsTo (parents : ParentSet) (root : Expr) : GoalM Unit := do
11261158
let mut curr := if let some parents := (← get).parents.find? { expr := root } then parents else {}
1127-
for parent in parents do
1159+
for parent in parents.elems do
11281160
curr := curr.insert parent
11291161
modify fun s => { s with parents := s.parents.insert { expr := root } curr }
11301162

@@ -1172,7 +1204,7 @@ For each equality `b = c` in `parents`, executes `k b c` IF
11721204
- `b = c` is equal to `False`, and
11731205
-/
11741206
@[inline] def forEachDiseq (parents : ParentSet) (k : (lhs : Expr) → (rhs : Expr) → GoalM Unit) : GoalM Unit := do
1175-
for parent in parents do
1207+
for parent in parents.elems do
11761208
let_expr Eq _ b c := parent | continue
11771209
if (← isEqFalse parent) then
11781210
if (← isEqv b c) then

tests/lean/run/grind_indexmap_trace.lean

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -147,8 +147,7 @@ example (m : IndexMap α β) (a : α) (h : a ∈ m) :
147147
info: Try this:
148148
[apply]
149149
instantiate only [= mem_indices_of_mem, insert]
150-
instantiate only [=_ HashMap.contains_iff_mem, = getElem?_neg, = getElem?_pos]
151-
instantiate only [=_ HashMap.contains_iff_mem]
150+
instantiate only [= getElem?_neg, = getElem?_pos, =_ HashMap.contains_iff_mem]
152151
cases #4ed2
153152
next =>
154153
cases #ffdf
@@ -176,12 +175,11 @@ example (m : IndexMap α β) (a a' : α) (b : β) :
176175
a' ∈ m.insert a b ↔ a' = a ∨ a' ∈ m := by
177176
grind => finish?
178177

179-
-- **TODO**: Investigate whey the following `finish?` has one fewer step.
180178
/--
181179
info: Try this:
182180
[apply]
183181
instantiate only [= mem_indices_of_mem, insert]
184-
instantiate only [=_ HashMap.contains_iff_mem, = getElem?_neg, = getElem?_pos]
182+
instantiate only [= getElem?_neg, = getElem?_pos, =_ HashMap.contains_iff_mem]
185183
cases #4ed2
186184
next =>
187185
cases #ffdf

0 commit comments

Comments
 (0)