Skip to content

Commit 356a7e5

Browse files
authored
feat: add the liaSteps configuration option to grind (#14392)
This PR adds a new `liaSteps` configuration option to `grind`. The motivation is to quickly interrupt the search for hard linear integer arithmetic problems.
1 parent d16f714 commit 356a7e5

7 files changed

Lines changed: 118 additions & 0 deletions

File tree

src/Init/Grind/Config.lean

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,15 @@ structure Config where
134134
-/
135135
lia := true
136136
/--
137+
Maximum number of steps performed by the `lia` solver while searching for an assignment
138+
satisfying the linear integer arithmetic constraints.
139+
A step is counted for each variable processed during the search. This threshold prevents
140+
case-split explosion in examples that require enumerating a huge number of cases
141+
(e.g., Cooper conflict resolution with large coefficients).
142+
When the threshold is reached, the search is interrupted, and the solver becomes incomplete.
143+
-/
144+
liaSteps : Nat := 10000
145+
/--
137146
When `true` (default: `true`), uses procedure for handling associative (and commutative) operators.
138147
-/
139148
ac := true

src/Init/Grind/Tactics.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ This *model-based* search is **complete for LIA**.
175175
176176
* `grind -lia` disable the solver (useful for debugging)
177177
* `grind +qlia` accept rational models (shrinks the search space but is incomplete for ℤ)
178+
* `grind (liaSteps := n)` cap the number of steps performed by the model search (the solver becomes incomplete when the threshold is reached)
178179
179180
#### Examples:
180181

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,12 @@ private def searchAssignmentMain : SearchM Unit := do
512512
return ()
513513
if let some c := (← get').conflict? then
514514
resolveConflict c
515+
else if (← checkMaxSteps) then
516+
-- Interrupt the search. Remark: we do not interrupt while a conflict is unresolved
517+
-- because the cutsat state would be left with a pending `conflict?`.
518+
trace[grind.debug.lia.search] "search interrupted, maximum number of steps reached"
519+
setImprecise
520+
return ()
515521
else
516522
let x : Var := (← get').assignment.size
517523
trace[grind.debug.lia.search] "next var: {← getVar x}, {x}, {(← get').assignment.toList}"
@@ -530,6 +536,7 @@ private def searchQLiaAssignment : GoalM Bool := do
530536
searchAssignmentMain
531537
let precise := (← get).precise
532538
resetDecisionStack
539+
saveSteps
533540
return precise
534541
go .rat |>.run' {}
535542

@@ -551,6 +558,7 @@ private def searchLiaAssignment : GoalM Unit := do
551558
let go : SearchM Unit := do
552559
searchAssignmentMain
553560
resetDecisionStack
561+
saveSteps
554562
traceActiveCnstrs
555563
reorderVars
556564
traceActiveCnstrs
@@ -566,6 +574,8 @@ def searchAssignment : GoalM Unit := do
566574
searchLiaAssignment
567575
trace[grind.debug.lia.search] "after search int model, inconsistent: {← isInconsistent}"
568576
if (← isInconsistent) then return ()
577+
-- If the search was interrupted (`liaSteps` threshold), the assignment is partial.
578+
unless (← hasAssignment) do return ()
569579
-- TODO: constructing a model is only worth if `grind` will **not** continue searching.
570580
assignElimVars
571581

@@ -580,6 +590,10 @@ The result is `false` if module already has a satisfying assignment.
580590
def check : GoalM Bool := do profileitM Exception "grind cutsat" (← getOptions) do
581591
if (← hasAssignment) then
582592
return false
593+
else if (← get').steps ≥ (← getConfig).liaSteps then
594+
-- A previous search reached the `liaSteps` threshold. The solver is disabled to
595+
-- ensure `grind` does not keep restarting the (bounded) search at every iteration.
596+
return false
583597
else
584598
searchAssignment
585599
return true

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ structure Search.State where
5757
precise : Bool := true
5858
/-- Set of decision variables in `cases`. -/
5959
decVars : FVarIdSet := {}
60+
/--
61+
Number of steps performed by the current search.
62+
Remark: we cannot use the `steps` counter in the cutsat state because it is rolled
63+
back when backtracking case splits.
64+
-/
65+
steps : Nat := 0
6066

6167
abbrev SearchM := ReaderT Search.Kind (StateRefT Search.State GoalM)
6268

@@ -68,6 +74,23 @@ def isApprox : SearchM Bool :=
6874
def setImprecise : SearchM Unit := do
6975
modify fun s => { s with precise := false }
7076

77+
/--
78+
Increments the search steps counter, and returns `true` if the cumulative number of
79+
steps reached the `liaSteps` configuration threshold.
80+
-/
81+
def checkMaxSteps : SearchM Bool := do
82+
modify fun s => { s with steps := s.steps + 1 }
83+
return (← get').steps + (← get).steps > (← getConfig).liaSteps
84+
85+
/--
86+
Adds the number of steps performed by the current search to the cumulative counter
87+
in the cutsat state. It must be invoked after `resetDecisionStack` because backtracking
88+
rolls back the cutsat state.
89+
-/
90+
def saveSteps : SearchM Unit := do
91+
let steps := (← get).steps
92+
modify' fun s => { s with steps := s.steps + steps }
93+
7194
def mkCase (kind : CaseKind) : SearchM FVarId := do
7295
let fvarId ← mkFreshFVarId
7396
let saved ← get'

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,12 @@ structure State where
319319
-/
320320
caseSplits : Bool := false
321321
/--
322+
Cumulative number of steps performed by the model search.
323+
The model search is interrupted when this counter reaches the `liaSteps`
324+
configuration threshold, and the solver becomes incomplete.
325+
-/
326+
steps : Nat := 0
327+
/--
322328
`conflict?` is `some ..` if a contradictory constraint was derived.
323329
This field is only set when `caseSplits` is `true`. Otherwise, we
324330
can convert `UnsatProof` into a Lean term and close the current `grind` goal.

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ def Arith.Cutsat.pp? (goal : Goal) : MetaM (Option MessageData) := do
200200
let s ← Arith.Cutsat.cutsatExt.getStateCore goal
201201
let nodes := s.varMap
202202
if nodes.isEmpty then return none
203+
-- The assignment is partial (and does not satisfy the constraints) if the search
204+
-- was interrupted because the `liaSteps` threshold was reached.
205+
if s.assignment.size < s.vars.size then return none
203206
let model ← Arith.Cutsat.mkModel goal
204207
if model.isEmpty then return none
205208
let mut ms := #[]
@@ -240,6 +243,9 @@ private def ppThresholds (c : Grind.Config) : M Unit := do
240243
if maxGen ≥ c.gen then
241244
msgs := msgs.push <| .trace { cls := `limit } m!"maximum term generation has been reached, threshold: `(gen := {c.gen})`" #[]
242245
msgs ← Arith.CommRing.addThresholdMessage goal c msgs
246+
let cutsat ← Arith.Cutsat.cutsatExt.getStateCore goal
247+
if cutsat.steps ≥ c.liaSteps then
248+
msgs := msgs.push <| .trace { cls := `limit } m!"maximum number of steps performed by the `lia` solver has been reached, threshold: `(liaSteps := {c.liaSteps})`" #[]
243249
unless msgs.isEmpty do
244250
pushMsg <| .trace { cls := `limits } "Thresholds reached" msgs
245251

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module
2+
set_option warn.sorry false
3+
4+
/-!
5+
Tests the `liaSteps` threshold that interrupts the cutsat model search.
6+
7+
The "tight rhombus" example below requires enumerating thousands of cases during
8+
Cooper conflict resolution. Without the threshold, `grind` used to spend a long time
9+
searching and then produce a proof term so large that the kernel failed with a
10+
deep-recursion error. With the default configuration, `grind` must fail quickly.
11+
-/
12+
13+
example (x : Int) (y : Int)
14+
(h0 : ((0 <= ((27300 * x) - (24501 * y))) ∧ (((27300 * x) - (24501 * y)) <= 99) ∧
15+
(1 <= ((27301 * x) - (24500 * y))) ∧ (((27301 * x) - (24500 * y)) <= 100))) : False := by
16+
fail_if_success grind
17+
sorry
18+
19+
/-!
20+
Checks that the `liaSteps` configuration option is wired: this example is solvable with
21+
the default budget, but not with `liaSteps := 1`.
22+
-/
23+
24+
example (x y : Int) :
25+
2711*x + 13*y →
26+
11*x + 13*y ≤ 45
27+
-107*x - 9*y →
28+
7*x - 9*y ≤ 4 → False := by
29+
fail_if_success grind (liaSteps := 1)
30+
grind
31+
32+
33+
/--
34+
error: `grind` failed
35+
case grind
36+
x y : Int
37+
h0 : 0 ≤ 27300 * x - 24501 * y ∧ 27300 * x - 24501 * y ≤ 99 ∧ 1 ≤ 27301 * x - 24500 * y ∧ 27301 * x - 24500 * y ≤ 100
38+
⊢ False
39+
[grind] Goal diagnostics
40+
[facts] Asserted facts
41+
[prop] -9100 * x + 8167 * y ≤ 0 ∧
42+
9100 * x + -8167 * y + -33 ≤ 0 ∧ -27301 * x + 24500 * y + 1 ≤ 0 ∧ 27301 * x + -24500 * y + -100 ≤ 0
43+
[eqc] True propositions
44+
[prop] -27301 * x + 24500 * y + 1 ≤ 0 ∧ 27301 * x + -24500 * y + -100 ≤ 0
45+
[prop] 9100 * x + -8167 * y + -33 ≤ 0 ∧ -27301 * x + 24500 * y + 1 ≤ 0 ∧ 27301 * x + -24500 * y + -100 ≤ 0
46+
[prop] -9100 * x + 8167 * y ≤ 0 ∧
47+
9100 * x + -8167 * y + -33 ≤ 0 ∧ -27301 * x + 24500 * y + 1 ≤ 0 ∧ 27301 * x + -24500 * y + -100 ≤ 0
48+
[prop] -27301 * x + 24500 * y + 1 ≤ 0
49+
[prop] 9100 * x + -8167 * y + -33 ≤ 0
50+
[prop] 27301 * x + -24500 * y + -100 ≤ 0
51+
[prop] -9100 * x + 8167 * y ≤ 0
52+
[limits] Thresholds reached
53+
[limit] maximum number of steps performed by the `lia` solver has been reached, threshold: `(liaSteps := 1)`
54+
-/
55+
#guard_msgs in
56+
example (x : Int) (y : Int)
57+
(h0 : ((0 <= ((27300 * x) - (24501 * y))) ∧ (((27300 * x) - (24501 * y)) <= 99) ∧
58+
(1 <= ((27301 * x) - (24500 * y))) ∧ (((27301 * x) - (24500 * y)) <= 100))) : False := by
59+
grind (liaSteps := 1)

0 commit comments

Comments
 (0)