Skip to content

Commit cdaa827

Browse files
authored
fix: grind linarith counterexample (#10960)
This PR fixes a bug in the `grind linarith` model/counterexample construction. Closes #10500
1 parent a9d6bc6 commit cdaa827

File tree

7 files changed

+168
-2
lines changed

7 files changed

+168
-2
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ def internalize (e : Expr) (parent? : Option Expr) : GoalM Unit := do
9191
let some type := getType? e | return ()
9292
if isForbiddenParent parent? then return ()
9393
if let some structId ← getStructId? type then LinearM.run structId do
94+
trace[grind.linarith.internalize] "{e}"
9495
setTermStructId e
9596
linearExt.markTerm e
9697
markVars e

src/Lean/Meta/Tactic/Grind/Arith/Linear/Model.lean

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Authors: Leonardo de Moura
66
module
77
prelude
88
public import Lean.Meta.Tactic.Grind.Arith.Linear.Types
9+
import Lean.Meta.Tactic.Grind.Arith.Linear.Reify
910
import Lean.Meta.Tactic.Grind.Arith.ModelUtil
1011
import Init.Grind.Module.Envelope
1112
public section
@@ -28,6 +29,58 @@ private def toQ? (e : Expr) : Option Expr :=
2829
| Grind.IntModule.OfNatModule.toQ _ _ a => some a
2930
| _ => none
3031

32+
/--
33+
Helper function for evaluating terms that have been processed by `internalize`, but
34+
we did not added them to constraints. See comment at `assignTerms`.
35+
-/
36+
private partial def evalTermAt? (e : Expr) (s : Struct) (model : Std.HashMap Expr Rat) : MetaM (Option Rat) := do
37+
go e
38+
where
39+
go (e : Expr) : OptionT MetaM Rat := do
40+
if let some val := model.get? e then
41+
return val
42+
match_expr e with
43+
| Neg.neg _ i a => if isNegInst s i then return - (← go a) else failure
44+
| HAdd.hAdd _ _ _ i a b => if isAddInst s i then return (← go a) + (← go b) else failure
45+
| HSub.hSub _ _ _ i a b => if isSubInst s i then return (← go a) - (← go b) else failure
46+
| HMul.hMul _ _ _ i a b => if isHomoMulInst s i then return (← go a) * (← go b) else failure
47+
| HSMul.hSMul _ _ _ i a b =>
48+
if isSMulIntInst s i then
49+
let k ← getIntValue? a
50+
return k * (← go b)
51+
else if isSMulNatInst s i then
52+
let k ← getNatValue? a
53+
return k * (← go b)
54+
else
55+
failure
56+
| Zero.zero _ i => if isZeroInst s i then return 0 else failure
57+
| OfNat.ofNat _ n _ => let k ← getNatValue? n; return k
58+
| _ => failure
59+
60+
/--
61+
Assigns terms that do not have an assignment associated with them because they were used only as markers
62+
for communicating with the `grind` core.
63+
For example, suppose we have `a + b = c`. The `internalize` function marks `a + b`, `a`, and `b`
64+
with theory variables. Let's assume we also have `c = 2*b`. In this case, `internalize` marks `2*b`
65+
with a theory variable, and `b` is already marked. Then, when both equalities are asserted
66+
in the `grind` core, the `linarith` module is notified that `a + b = 2*b` is true, and it is then
67+
normalized as `a + -1*b = 0`. The terms `a` and `b` are assigned rational values by the search
68+
procedure, but `a + b` and `2*b` are not. This procedure assigns them using the model computed by the
69+
search procedure.
70+
71+
**Note**: We should reconsider whether to add the equalities `「a+b」 = a + b` and `「2*b」 = 2*b`
72+
to force the search procedure to assign interpretations to these terms.
73+
-/
74+
private def assignTerms (goal : Goal) (structId : Nat) (model : Std.HashMap Expr Rat) : MetaM (Std.HashMap Expr Rat) := do
75+
let mut model := model
76+
let s ← linearExt.getStateCore goal
77+
let struct := s.structs[structId]!
78+
for (e, structId') in s.exprToStructIdEntries do
79+
if structId == structId' && !model.contains e then
80+
if let some v ← evalTermAt? e struct model then
81+
model := assignEqc goal e v model
82+
return model
83+
3184
/--
3285
Construct a model that satisfies all constraints in the linarith model for the structure with id `structId`.
3386
It also assigns values to (integer) terms that have not been internalized by the linarith model.
@@ -42,6 +95,7 @@ def mkModel (goal : Goal) (structId : Nat) : MetaM (Array (Expr × Rat)) := do
4295
if (← hasType s.type node) then
4396
if let some v := getAssignment? s node.self then
4497
model := assignEqc goal node.self v model
98+
model ← assignTerms goal structId model
4599
-- Assign `toQ a` terms
46100
for e in goal.exprs do
47101
let node ← goal.getENode e

src/Lean/Meta/Tactic/Grind/Arith/Linear/Search.lean

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ def IneqCnstr.throwUnexpected (c : IneqCnstr) : LinearM α := do
1818
def DiseqCnstr.throwUnexpected (c : DiseqCnstr) : LinearM α := do
1919
throwError "`grind linarith` internal error, unexpected{indentD (← c.denoteExpr)}"
2020

21+
def EqCnstr.throwUnexpected (c :EqCnstr) : LinearM α := do
22+
throwError "`grind linarith` internal error, unexpected{indentD (← c.denoteExpr)}"
23+
2124
private def checkIsNextVar (x : Var) : LinearM Unit := do
2225
if x != (← getStruct).assignment.size then
2326
throwError "`grind linarith` internal error, assigning variable out of order"
@@ -246,13 +249,36 @@ private def resetDecisionStack : SearchM Unit := do
246249
let first := (← get).cases[0]!
247250
modifyStruct fun s => { first.saved with assignment := s.assignment }
248251

252+
/-- Assign eliminated variables using `elimEqs` field. -/
253+
private def assignElimVars : SearchM Unit := do
254+
if (← inconsistent) then return ()
255+
go (← getStruct).elimStack
256+
where
257+
go (xs : List Var) : SearchM Unit := do
258+
match xs with
259+
| [] => return ()
260+
| x :: xs =>
261+
let some c := (← getStruct).elimEqs[x]!
262+
| throwError "`grind` internal error, eliminated variable must have equation associated with it"
263+
-- `x` may not be the max variable
264+
let a := c.p.coeff x
265+
if a == 0 then c.throwUnexpected
266+
-- ensure `x` is 0 when evaluating `c.p`
267+
modifyStruct fun s => { s with assignment := s.assignment.set x 0 }
268+
let some v ← c.p.eval? | c.throwUnexpected
269+
let v := (-v) / a
270+
traceAssignment x v
271+
modifyStruct fun s => { s with assignment := s.assignment.set x v }
272+
go xs
273+
249274
/-- Search for an assignment/model for the linear constraints. -/
250275
private def searchAssignmentMain : SearchM Unit := do
251276
repeat
252277
trace[grind.debug.linarith.search] "main loop"
253278
checkSystem "linarith"
254279
if (← hasAssignment) then
255280
trace[grind.debug.linarith.search] "found assignment"
281+
assignElimVars
256282
return ()
257283
if (← isInconsistent) then
258284
-- `grind` state is inconsistent

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,8 @@ structure State where
245245
typeIdOf : PHashMap ExprPtr (Option Nat) := {}
246246
/- Mapping from expressions/terms to their structure ids. -/
247247
exprToStructId : PHashMap ExprPtr Nat := {}
248+
/-- `exprToStructId` content as an array for traversal. -/
249+
exprToStructIdEntries : PArray (Expr × Nat) := {}
248250
/--
249251
Some types are unordered rings, so we do not process them in `linarith`.
250252
When such types are detected in `getStructId?`, we add them to the set

src/Lean/Meta/Tactic/Grind/Arith/Linear/Util.lean

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ def setTermStructId (e : Expr) : LinearM Unit := do
4040
unless structId' == structId do
4141
reportIssue! "expression in two different structure in linarith module{indentExpr e}"
4242
return ()
43-
modify' fun s => { s with exprToStructId := s.exprToStructId.insert { expr := e } structId }
43+
modify' fun s => { s with
44+
exprToStructId := s.exprToStructId.insert { expr := e } structId
45+
exprToStructIdEntries := s.exprToStructIdEntries.push (e, structId)
46+
}
4447

4548
def getNoNatDivInst : LinearM Expr := do
4649
let some inst := (← getStruct).noNatDivInst?

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ Returns `true` if `e` should be treated as an interpreted value by the arithmeti
5959
def isInterpretedTerm (e : Expr) : Bool :=
6060
isNatNum e || isIntNum e || e.isAppOf ``HAdd.hAdd || e.isAppOf ``HMul.hMul || e.isAppOf ``HSub.hSub || e.isAppOf ``HSMul.hSMul
6161
|| e.isAppOf ``Neg.neg || e.isAppOf ``HDiv.hDiv || e.isAppOf ``HMod.hMod || e.isAppOf ``One.one || e.isAppOf ``Zero.zero
62-
|| e.isAppOf ``NatCast.natCast || e.isIte || e.isDIte || e.isAppOf ``OfNat.ofNat || e.isAppOf ``Grind.ToInt.toInt
62+
|| e.isAppOf ``Inv.inv || e.isAppOf ``NatCast.natCast || e.isIte || e.isDIte || e.isAppOf ``OfNat.ofNat || e.isAppOf ``Grind.ToInt.toInt
6363
|| e.isAppOf ``Grind.IntModule.OfNatModule.toQ || e matches .lit (.natVal _)
6464

6565
/--

tests/lean/run/grind_10500.lean

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
open Lean Grind Std
2+
3+
set_option warn.sorry false
4+
5+
namespace Ex1
6+
variable [Field Q] [LT Q] [LE Q] [LawfulOrderLT Q] [IsLinearOrder Q] [OrderedRing Q]
7+
8+
variable (a₁ a₂ a₃ a₄ a₅ : Q)
9+
variable (α L ν : Q)
10+
11+
/--
12+
trace: [grind.linarith.model] a₁ := 0
13+
[grind.linarith.model] a₂ := 0
14+
[grind.linarith.model] a₃ := 0
15+
[grind.linarith.model] a₄ := 0
16+
[grind.linarith.model] a₅ := 0
17+
[grind.linarith.model] α := 0
18+
[grind.linarith.model] L := 0
19+
[grind.linarith.model] ν := 2
20+
-/
21+
#guard_msgs in
22+
set_option trace.grind.linarith.model true in
23+
theorem upper_bound
24+
(hL : L = α) (hL1 : L ≤ 1)
25+
(ha₁ : 0 ≤ a₁) (ha₂ : 0 ≤ a₂) (ha₃ : 0 ≤ a₃) (ha₄ : 0 ≤ a₄) (ha₅ : 0 ≤ a₅)
26+
(hα : α = a₁ + a₂ + a₃ + a₄ + a₅) :
27+
ν ≤ 9/10 := by
28+
fail_if_success grind
29+
sorry
30+
31+
end Ex1
32+
33+
namespace Ex2
34+
35+
variable (a₁ a₂ a₃ a₄ a₅ : Rat)
36+
variable (α L ν : Rat)
37+
38+
/--
39+
trace: [grind.linarith.model] a₁ := 0
40+
[grind.linarith.model] a₂ := 0
41+
[grind.linarith.model] a₃ := 0
42+
[grind.linarith.model] a₄ := 0
43+
[grind.linarith.model] a₅ := 0
44+
[grind.linarith.model] α := 0
45+
[grind.linarith.model] L := 0
46+
[grind.linarith.model] ν := 2
47+
-/
48+
#guard_msgs in
49+
set_option trace.grind.linarith.model true in
50+
theorem upper_bound
51+
(hL : L = α) (hL1 : L ≤ 1)
52+
(ha₁ : 0 ≤ a₁) (ha₂ : 0 ≤ a₂) (ha₃ : 0 ≤ a₃) (ha₄ : 0 ≤ a₄) (ha₅ : 0 ≤ a₅)
53+
(hα : α = a₁ + a₂ + a₃ + a₄ + a₅) :
54+
ν ≤ 9/10 := by
55+
fail_if_success grind
56+
sorry
57+
58+
end Ex2
59+
60+
/--
61+
trace: [grind.linarith.model] a := 0
62+
[grind.linarith.model] b := 0
63+
[grind.linarith.model] c := 0
64+
-/
65+
#guard_msgs in
66+
set_option trace.grind.linarith.model true in
67+
example [Field α] [LE α] [LT α] [Std.IsPreorder α] [OrderedRing α] (a b c : α) (h : a = b + c) : False := by
68+
fail_if_success grind
69+
sorry
70+
71+
/--
72+
trace: [grind.linarith.model] a := 0
73+
[grind.linarith.model] b := 0
74+
[grind.linarith.model] c := 0
75+
-/
76+
#guard_msgs in
77+
set_option trace.grind.linarith.model true in
78+
example (a b c : Rat) (h : a = b + c) : False := by
79+
fail_if_success grind
80+
sorry

0 commit comments

Comments
 (0)