Skip to content

Commit d16f714

Browse files
authored
fix: grind ring approximation (#14390)
This PR fixes an issue in the `grind` ring solver. When a ring `R` does not satisfy `[NoNatZeroDivisors R]`, polynomial simplification could lose information, affecting completeness. This PR may increase memory consumption for examples that relied on this approximation. If this becomes a performance issue in practice, we can add a flag to control whether the approximation is used. Here is the example that exposed the issue: ```lean open Lean.Grind example {R : Type u} [CommRing R] (A B : R) (H₀ : B ^ 2 = 0) (H : A * B = -2) : B + A * B = B - 2 := by grind ```
1 parent 9b9ebcb commit d16f714

7 files changed

Lines changed: 116 additions & 21 deletions

File tree

src/Lean/Meta/Tactic/Grind/Arith/CommRing/EqCnstr.lean

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,30 @@ def EqCnstr.simplifyUsingNumEq0 (c : EqCnstr) : RingM EqCnstr := do
156156
let .num k := c'.p | return c
157157
return { c with p := c.p.normEq0 k.natAbs, h := .numEq0 k.natAbs c' c }
158158

159-
/-- Simplify the given equation constraint using the current basis. -/
160-
def EqCnstr.simplify (c : EqCnstr) : RingM EqCnstr := do
159+
/--
160+
Simplify the given equation constraint using the current basis.
161+
162+
**Note**: This function (and `simplifyBasis`) runs with `checkCoeffDvd := true`. It rewrites a
163+
monomial `k'*m'` using an equation with leading term `k*m` only if `k ∣ k'` (or the ring
164+
implements `NoNatZeroDivisors`). Rewriting without this restriction requires multiplying the
165+
equation by `k/gcd k k' ≠ ±1` first, and *replacing* an equation with the result loses
166+
information: `p = 0` cannot be recovered from `(k/gcd k k') * p = 0` in an arbitrary
167+
commutative ring. The consequences of the skipped rewrites are not lost: a skipped rewrite at
168+
the leading monomial coincides with the S-polynomial computed by `superposeWith`, so it is
169+
derived non-destructively. The price is that the basis is not
170+
fully interreduced (e.g., `2*x = 0` and `3*x = 0` may coexist), and reduction is an incomplete
171+
ideal-membership test in rings without `NoNatZeroDivisors` (e.g., `x = 0` is not derived from
172+
`2*x = 0` and `3*x = 0`). The complete alternative is a strong Gröbner basis over `Int`
173+
(Kandri-Rody & Kapur): replace multiply-through rewriting with Euclidean remainder reduction
174+
on coefficients (`k' = q*k + r`, so the equation is never multiplied by a constant), and
175+
additionally superpose gcd-polynomials (Bézout combinations producing `gcd k k'` times the lcm
176+
of the leading monomials). We should consider this option in the future.
177+
178+
**Note**: If using `withCheckCoeffDvd` becomes too expensive when one does not have
179+
`NoNatZeroDivisors`, we will add a flag to perform the simplification anyway even if
180+
information is lost.
181+
-/
182+
def EqCnstr.simplify (c : EqCnstr) : RingM EqCnstr := withCheckCoeffDvd do
161183
let mut c := c
162184
repeat
163185
c ← simplifyUsingNumEq0 c
@@ -186,6 +208,14 @@ def EqCnstr.checkConstant (c : EqCnstr) : RingM Bool := do
186208
if let some c' := (← getCommRing).numEq0? then
187209
let .num m := c'.p | unreachable!
188210
let (g, a, b) := gcdExt n m
211+
if g == m then
212+
/-
213+
`n` is a multiple of the current `numEq0`; the new constraint is redundant.
214+
We must not set `numEq0Updated` here: superposition may keep regenerating the
215+
same constant, and requeueing the basis each time would loop until `maxSteps`.
216+
-/
217+
trace_goal[grind.ring.assert.trivial] "{← c.denoteExpr}"
218+
return true
189219
c := { c with p := .num g, h := .gcd a b c c' }
190220
modifyCommRing fun s => { s with numEq0? := some c, numEq0Updated := true }
191221
trace_goal[grind.ring.assert.store] "{← c.denoteExpr}"
@@ -220,6 +250,12 @@ def addToBasisCore (c : EqCnstr) : RingM Unit := do
220250

221251
def EqCnstr.addToQueue (c : EqCnstr) : RingM Unit := do
222252
if (← checkMaxSteps) then return ()
253+
/-
254+
Superposition may produce constant polynomials since the basis is not fully
255+
interreduced (see **Note** at `EqCnstr.simplify`). `checkConstant` processes them
256+
immediately; the queue must not contain constant polynomials.
257+
-/
258+
if (← c.checkConstant) then return ()
223259
trace_goal[grind.ring.assert.queue] "{← c.denoteExpr}"
224260
if (← checkMaxDegree c.p) then return () -- discard
225261
modifyCommRing fun s => { s with queue := s.queue.insert c }
@@ -265,7 +301,13 @@ def EqCnstr.toMonic (c : EqCnstr) : RingM EqCnstr := do
265301
return { c with p := c.p.mulConst (-1), h := .mul (-1) c }
266302
return c
267303

268-
def EqCnstr.simplifyBasis (c : EqCnstr) : RingM Unit := do
304+
/--
305+
Simplifies the basis using `c`. Every equation that `c` rewrites is removed from the basis
306+
and requeued in its rewritten form.
307+
308+
See **Note** at `EqCnstr.simplify` for why `checkCoeffDvd` must be enabled here.
309+
-/
310+
def EqCnstr.simplifyBasis (c : EqCnstr) : RingM Unit := withCheckCoeffDvd do
269311
trace[grind.debug.ring.simpBasis] "using: {← c.denoteExpr}"
270312
let .add _ m _ := c.p | return ()
271313
let rec go (basis : List EqCnstr) (acc : List EqCnstr) : RingM (List EqCnstr) := do
@@ -275,11 +317,20 @@ def EqCnstr.simplifyBasis (c : EqCnstr) : RingM Unit := do
275317
match c'.p with
276318
| .add _ m' _ =>
277319
if m.divides m' then
278-
let c'' ← c'.simplifyWithExhaustively c
279-
trace[grind.debug.ring.simpBasis] "simplified: {← c''.denoteExpr}"
280-
unless (← checkConstant c'') do
281-
addToQueue c''
282-
go basis acc
320+
/-
321+
**Note**: Only the first `simplifyWithCore` step is inspected because it decides basis
322+
membership: if it returns `none` (the leading monomial divides, but the rewrite is blocked by
323+
the `checkCoeffDvd` restriction), `c'` must remain in the basis untouched.
324+
-/
325+
if let some c'' ← c'.simplifyWithCore c then
326+
let c'' ← c''.simplifyWithExhaustively c
327+
trace[grind.debug.ring.simpBasis] "simplified: {← c''.denoteExpr}"
328+
unless (← checkConstant c'') do
329+
addToQueue c''
330+
go basis acc
331+
else
332+
-- The rewrite was skipped because of the `checkCoeffDvd` restriction. Keep `c'`.
333+
go basis (c' :: acc)
283334
else
284335
go basis (c' :: acc)
285336
| _ => go basis (c' :: acc)

src/Lean/Meta/Tactic/Grind/Arith/CommRing/RingM.lean

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ structure RingM.Context where
3939
the simplification anyway, we may end up with a proof that `k * q = 0`, but
4040
we cannot deduce `q = 0` since the ring does not implement `NoNatZeroDivisors`
4141
See comment at `PolyDerivation`.
42+
43+
We also need it when destructively simplifying equations, i.e., when *replacing* an
44+
equation with its simplified form (`EqCnstr.simplify` and `EqCnstr.simplifyBasis`):
45+
the rewrite multiplies the equation by `k₁ = k/gcd k k'`, and the result is weaker than
46+
the original equation when `k₁ ≠ ±1`. See **Note** at `EqCnstr.simplify`.
4247
-/
4348
checkCoeffDvd : Bool := false
4449

src/Lean/Meta/Tactic/Grind/Arith/CommRing/SafePoly.lean

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,20 @@ Simplifies polynomial `p₁` using polynomial `p₂` by rewriting.
156156
157157
This function attempts to rewrite `p₁` by eliminating the first occurrence of
158158
the leading monomial of `p₂`.
159+
160+
If `checkCoeffDvd` is `true` (and the ring does not implement `NoNatZeroDivisors`),
161+
a monomial is rewritten only if its coefficient is divisible by the leading
162+
coefficient of `p₂`, i.e., only if the rewrite does not multiply `p₁` by a
163+
constant `k₁ ≠ ±1`. See `RingM.Context.checkCoeffDvd`.
159164
-/
160165
def _root_.Lean.Grind.CommRing.Poly.simpM? (p₁ p₂ : Poly) : RingM (Option SimpResult) := do
161166
match p₂ with
162167
| .add k₂' m₂ p₂ =>
168+
let checkCoeff := (← checkCoeffDvd) && !(← noZeroDivisors)
163169
let rec go? (p₁ : Poly) : RingM (Option SimpResult) := do
164170
match p₁ with
165171
| .add k₁' m₁ p₁ =>
166-
if m₂.divides m₁ then
172+
if m₂.divides m₁ && (!checkCoeff || k₂' ∣ k₁') then
167173
let m₂ := m₁.div m₂
168174
let g := Nat.gcd k₁'.natAbs k₂'.natAbs
169175
let k₁ := k₂'/g

tests/elab/grind_ring_2.lean

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
-- In this file we use the `grobner` frontend for `grind`.
22
module
33
set_option grind.debug true
4+
set_option warn.sorry false
45
open Lean.Grind
56

67
example [CommRing α] [NoNatZeroDivisors α] (x y : α) : 3*x = 13*y = 2 → x + y = 1 := by
@@ -70,6 +71,10 @@ example [CommRing α] (a b c : α)
7071
/--
7172
trace: [grind.ring.assert.basis] a + b + c + -3 = 0
7273
[grind.ring.assert.basis] 2 * b ^ 2 + 2 * (b * c) + 2 * c ^ 2 + -6 * b + -6 * c + 4 = 0
74+
[grind.ring.assert.basis] 3 * (b ^ 2 * c) + 3 * (b * c ^ 2) + -9 * b ^ 2 + -18 * (b * c) + -9 * c ^ 2 + 27 * b +
75+
27 * c +
76+
-20 =
77+
0
7378
[grind.ring.assert.basis] 6 * c ^ 3 + -18 * c ^ 2 + 12 * c + 4 = 0
7479
-/
7580
#guard_msgs (trace) in
@@ -128,7 +133,7 @@ example (a b c : Int) (f : Int → Nat)
128133
a^2 + b^2 + c^2 = 5
129134
a^3 + b^3 + c^3 = 7
130135
f (a^4 + b^4) + f (9 - c^4) ≠ 1 := by
131-
cutsat +ring
136+
lia +ring
132137

133138
example [CommRing α] [NoNatZeroDivisors α] (a b c : α) (f : α → Nat)
134139
: a + b + c = 3
@@ -167,7 +172,7 @@ example (x y : Fin 3) (h : x = y) : ((x + y) ^ 3 : Fin 3) = - x^3 := by grobner
167172
-- Verify that `cutsat` is disabled when calling `grobner` directly.
168173
example (x : Nat) : x % 2 = 0 ∨ x % 2 = 1 := by
169174
fail_if_success grobner
170-
cutsat
175+
lia
171176

172177
-- Verify that `grobner` will not perform case splits unless explicitly asked for.
173178
example (x : Int) (h : x^2 = 0) : (if x > 0 then x else x)^3 = 0 := by

tests/elab/grind_ring_2.lean.out.expected

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module
2+
3+
/-!
4+
Tests that the `grind` ring solver does not lose basis equations to destructive
5+
pseudo-reduction. Rewriting an equation with a rule whose leading coefficient does not
6+
divide the target coefficient multiplies the equation by a constant `k ≠ ±1`; replacing
7+
the original equation with the result loses information in rings without
8+
`NoNatZeroDivisors`. For example, below, the basis contains `B ^ 2 = 0` and `B * A + 2 = 0`,
9+
superposition derives `2 * B = 0`, and inserting the latter into the basis used to destroy
10+
both original equations (`B ^ 2 = 0` became `0 = 0` and `B * A + 2 = 0` became `4 = 0`),
11+
making the goal, which follows from `H` alone, unprovable.
12+
-/
13+
14+
open Lean.Grind
15+
16+
-- Basis interreduction path (`EqCnstr.simplifyBasis`): `2 * B = 0` is derived by
17+
-- superposition and used to be applied destructively to the rest of the basis.
18+
example {R : Type u} [CommRing R] (A B : R) (H₀ : B ^ 2 = 0) (H : A * B = -2) :
19+
B + A * B = B - 2 := by
20+
grind
21+
22+
-- Assertion path (`EqCnstr.simplify`): with `2 * B = 0` already in the basis,
23+
-- `A * B = -2` used to be destroyed while being simplified during assertion.
24+
example {R : Type u} [CommRing R] (A B : R) (H₀ : 2 * B = 0) (H : A * B = -2) :
25+
B + A * B = B - 2 := by
26+
grind
27+
28+
-- With `NoNatZeroDivisors` the rewrites are information-preserving and this always worked.
29+
example {R : Type u} [CommRing R] [NoNatZeroDivisors R] (A B : R)
30+
(H₀ : B ^ 2 = 0) (H : A * B = -2) : B + A * B = B - 2 := by
31+
grind
32+
33+
-- The consequences of the skipped rewrites must still be derived via superposition:
34+
-- here `4 = 0` follows from the hypotheses, which is absurd in `Int`.
35+
example (A B : Int) (H₀ : B ^ 2 = 0) (H : A * B = -2) : False := by
36+
grind

tests/elab/grind_semiring.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ example [CommSemiring α] [AddRightCancel α] [IsCharP α 0] (x y : α) : x^2*y
3434
trace: [grind.ring.assert.basis] ↑x + ↑y + -2 = 0
3535
[grind.ring.assert.basis] ↑y ^ 3 + -4 * ↑y ^ 2 + 4 * ↑y + -1 = 0
3636
[grind.ring.assert.basis] 2 * ↑y ^ 2 + -3 * ↑y + 1 = 0
37+
[grind.ring.assert.basis] ↑y ^ 3 + -2 * ↑y + 1 = 0
38+
[grind.ring.assert.basis] 3 * ↑y ^ 2 + -5 * ↑y + 2 = 0
3739
[grind.ring.assert.basis] ↑y + -1 = 0
3840
-/
3941
#guard_msgs (drop error, trace) in

0 commit comments

Comments
 (0)