Skip to content

Commit 7f3f2fd

Browse files
authored
fix: 0 divides constraint in grind (#14373)
This PR fixes nontermination in `grind` triggered by constraints of the form `0 ∣ p`.
1 parent 5c74909 commit 7f3f2fd

5 files changed

Lines changed: 38 additions & 0 deletions

File tree

src/Init/Data/Int/Linear.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,10 @@ theorem dvd_norm (ctx : Context) (d : Int) (p₁ p₂ : Poly) : p₁.norm.beq' p
980980
intro h₁
981981
simp [Poly.denote_norm ctx p₁, h₁]
982982

983+
theorem eq_of_zero_dvd (ctx : Context) (p : Poly) : 0 ∣ p.denote' ctx → p.denote' ctx = 0 := by
984+
intro ⟨k, h⟩
985+
rw [h, Int.zero_mul]
986+
983987
theorem le_norm (ctx : Context) (p₁ p₂ : Poly) (h : p₁.norm.beq' p₂) : p₁.denote' ctx ≤ 0 → p₂.denote' ctx ≤ 0 := by
984988
simp at h
985989
replace h := congrArg (Poly.denote ctx) h

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ partial def DvdCnstr.assert (c : DvdCnstr) : GoalM Unit := withIncRecDepth do
6262
if c.isTrivial then
6363
trace[grind.lia.assert.trivial] "{← c.pp}"
6464
return ()
65+
if c.d == 0 then
66+
-- `0 ∣ p` is equivalent to `p = 0`. The model search assumes `d ≠ 0` for
67+
-- stored divisibility constraints (it computes `_ % d` and `_ / d`).
68+
let c' : EqCnstr := { p := c.p, h := .ofZeroDvd c }
69+
c'.assert
70+
return ()
6571
let d₁ := c.d
6672
let .add a₁ x p₁ := c.p | c.throwUnexpected
6773
if (← c.satisfied) == .false then

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,9 @@ private partial def EqCnstr.toExprProofImpl (c' : EqCnstr) : ProofM Expr := cach
360360
return mkApp6 (mkConst ``Int.Internal.Linear.eq_of_le_ge)
361361
(← getContext) (← mkPolyDecl c₁.p) (← mkPolyDecl c₂.p)
362362
eagerReflBoolTrue (← c₁.toExprProof) (← c₂.toExprProof)
363+
| .ofZeroDvd c =>
364+
return mkApp3 (mkConst ``Int.Internal.Linear.eq_of_zero_dvd)
365+
(← getContext) (← mkPolyDecl c.p) (← c.toExprProof)
363366
| .reorder c => withUnordered <| c.toExprProof
364367
| .commRingNorm c e p =>
365368
let h := mkApp4 (mkConst ``Grind.CommRing.norm_int) (← getRingContext) (← mkRingExprDecl e) (← mkRingPolyDecl p) eagerReflBoolTrue
@@ -635,6 +638,7 @@ partial def EqCnstr.collectDecVars (c' : EqCnstr) : CollectDecVarsM Unit := do u
635638
| .core0 .. | .core .. | .defn .. | .defnNat ..
636639
| .defnCommRing .. | .defnNatCommRing .. | .coreToInt .. => return () -- Equalities coming from the core never contain cutsat decision variables
637640
| .commRingNorm c .. | .reorder c | .norm c | .divCoeffs c | .div _ _ c | .mod _ _ c => c.collectDecVars
641+
| .ofZeroDvd c => c.collectDecVars
638642
| .subst _ c₁ c₂ | .ofLeGe c₁ c₂ => c₁.collectDecVars; c₂.collectDecVars
639643
| .mul _ cs => cs.forM fun (_, _, c) => c.collectDecVars
640644
| .pow _ ca? _ cb? => ca?.forM (·.collectDecVars); cb?.forM (·.collectDecVars)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ inductive EqCnstrProof where
9090
| divCoeffs (c : EqCnstr)
9191
| subst (x : Var) (c₁ : EqCnstr) (c₂ : EqCnstr)
9292
| ofLeGe (c₁ : LeCnstr) (c₂ : LeCnstr)
93+
| /-- `p = 0` derived from the divisibility constraint `c` of the form `0 ∣ p`. -/
94+
ofZeroDvd (c : DvdCnstr)
9395
| reorder (c : EqCnstr)
9496
| commRingNorm (c : EqCnstr) (e : CommRing.RingExpr) (p : CommRing.Poly)
9597
| defnCommRing (e : Expr) (p : Poly) (re : CommRing.RingExpr) (rp : CommRing.Poly) (p' : Poly)

tests/elab/grind_cutsat_loop.lean

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/-!
2+
Tests for divisibility constraints with divisor `0` in `grind`'s cutsat procedure.
3+
Cutsat used to store `0 ∣ p` constraints, but the model search assumes a nonzero
4+
divisor: it could loop forever in `DvdSolution.geAvoiding` (the original example
5+
below), or weaken bounds via `tightUsingDvd` and miss refutations. `0 ∣ p` is now
6+
converted into the equality `p = 0` when asserted.
7+
-/
8+
9+
-- Used to hang: `0 ∣ b` with `b ≠ 0` excludes the single solution of the
10+
-- divisibility constraint, and the model search looped bumping the candidate.
11+
theorem mwe {b : Nat} (p : Nat) (x1 x2 : Nat) (hb0 : b ≠ 0) (hab : 0 ∣ b) :
12+
0 / p ^ x1 ∣ b / p ^ x2 := by
13+
grind
14+
15+
-- Used to fail: the diseq was folded into the bound `b ≥ 1`, which `tightUsingDvd`
16+
-- then weakened back to `b ≥ 0` using the `0 ∣ b` constraint, producing the bogus
17+
-- model `b := 0`.
18+
example (b : Nat) (h1 : b ≠ 0) (h2 : 0 ∣ b) : False := by
19+
grind
20+
21+
example (a b : Nat) (h : 0 ∣ a + b) (h1 : a + b ≠ 0) : False := by
22+
grind

0 commit comments

Comments
 (0)