Skip to content

Commit 3e3b0b7

Browse files
hrmacbethBeibeiX0
authored andcommitted
feat: field tactic (leanprover-community#29089)
This PR creates a `field` tactic, i.e. an analogue of `ring` but for fields -- a finishing tactic for equality goals in fields. It is basically `field_simp; ring1`, with a few tweaks for better error reporting! We could have done this at any point in the last five years, but since `field_simp` is more performant as of leanprover-community#28658, now seems like a good time. I propose that `field` be preferred to terminal `field_simp`/`field_simp; ring` from now on. I'll follow up with a PR ([preview](leanprover-community@44c0136)) making this change widely across the library. In the current PR I also rewrote the docstrings of `field_simp` and friends to nudge users in this direction. Closes leanprover-community#4837
1 parent 5f10ca9 commit 3e3b0b7

5 files changed

Lines changed: 219 additions & 7 deletions

File tree

Mathlib.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6168,6 +6168,7 @@ import Mathlib.Tactic.ExtractLets
61686168
import Mathlib.Tactic.FBinop
61696169
import Mathlib.Tactic.FailIfNoProgress
61706170
import Mathlib.Tactic.FastInstance
6171+
import Mathlib.Tactic.Field
61716172
import Mathlib.Tactic.FieldSimp
61726173
import Mathlib.Tactic.FieldSimp.Attr
61736174
import Mathlib.Tactic.FieldSimp.Discharger

Mathlib/Tactic.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ import Mathlib.Tactic.ExtractLets
8787
import Mathlib.Tactic.FBinop
8888
import Mathlib.Tactic.FailIfNoProgress
8989
import Mathlib.Tactic.FastInstance
90+
import Mathlib.Tactic.Field
9091
import Mathlib.Tactic.FieldSimp
9192
import Mathlib.Tactic.FieldSimp.Attr
9293
import Mathlib.Tactic.FieldSimp.Discharger

Mathlib/Tactic/Field.lean

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/-
2+
Copyright (c) 2025 Heather Macbeth. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Heather Macbeth
5+
-/
6+
import Mathlib.Tactic.FieldSimp
7+
import Mathlib.Tactic.Ring.Basic
8+
9+
10+
/-! # A tactic for proving algebraic goals in a field
11+
12+
This file contains the `field` tactic, a finishing tactic which roughly consists of running
13+
`field_simp; ring1`.
14+
15+
-/
16+
17+
open Lean Meta Qq
18+
19+
namespace Mathlib.Tactic.FieldSimp
20+
21+
open Lean Elab Tactic Lean.Parser.Tactic
22+
23+
/--
24+
The `field` tactic proves equality goals in (semi-)fields. For example:
25+
```
26+
example {x y : ℚ} (hx : x + y ≠ 0) : x / (x + y) + y / (x + y) = 1 := by
27+
field
28+
example {a b : ℝ} (ha : a ≠ 0) : a / (a * b) - 1 / b = 0 := by field
29+
```
30+
The scope of the tactic is equality goals which are *universal*, in the sense that they are true in
31+
any field in which the appropriate denominators don't vanish. (That is, they are consequences purely
32+
of the field axioms.)
33+
34+
Checking the nonvanishing of the necessary denominators is done using a variety of tricks -- in
35+
particular this part of the reasoning is non-universal, i.e. can be specific to the field at hand
36+
(order properties, explicit `≠ 0` hypotheses, `CharZero` if that is known, etc). The user can also
37+
provide additional terms to help with the nonzeroness proofs. For example:
38+
```
39+
example {K : Type*} [Field K] (hK : ∀ x : K, x ^ 2 + 1 ≠ 0) (x : K) :
40+
1 / (x ^ 2 + 1) + x ^ 2 / (x ^ 2 + 1) = 1 := by
41+
field [hK]
42+
```
43+
44+
The `field` tactic is built from the tactics `field_simp` (which clears the denominators) and `ring`
45+
(which proves equality goals universally true in commutative (semi-)rings). If `field` fails to
46+
prove your goal, you may still be able to prove your goal by running the `field_simp` and `ring_nf`
47+
normalizations in some order. For example, this statement:
48+
```
49+
example {a b : ℚ} (H : b + a ≠ 0) : a / (a + b) + b / (b + a) = 1
50+
```
51+
is not proved by `field` but is proved by `ring_nf at *; field`. -/
52+
elab (name := field) "field" d:(ppSpace discharger)? args:(ppSpace simpArgs)? : tactic =>
53+
withMainContext do
54+
let disch ← parseDischarger d args
55+
let s0 ← saveState
56+
-- run `field_simp` (only at the top level, not recursively)
57+
liftMetaTactic1 (transformAtTarget ((AtomM.run .reducible ∘ reduceProp disch) ·) "field"
58+
(failIfUnchanged := False) · default)
59+
let s1 ← saveState
60+
try
61+
-- run `ring1`
62+
liftMetaFinishingTactic fun g ↦ AtomM.run .reducible <| Ring.proveEq g
63+
catch e =>
64+
try
65+
-- If `field` doesn't solve the goal, we first backtrack to the situation at the time of the
66+
-- `field_simp` call, and suggest `field_simp` if `field_simp` does anything useful.
67+
s0.restore
68+
let tacticStx ← `(tactic| field_simp $(d)? $(args)?)
69+
evalTactic tacticStx
70+
Meta.Tactic.TryThis.addSuggestion (← getRef) tacticStx
71+
catch _ =>
72+
-- If `field_simp` also doesn't do anything useful (maybe there are no denominators in the
73+
-- goal), then we backtrack to where the `ring1` call failed, and report that error message.
74+
s1.restore
75+
throw e
76+
77+
end Mathlib.Tactic.FieldSimp
78+
79+
/-! We register `field` with the `hint` tactic. -/
80+
register_hint field

Mathlib/Tactic/FieldSimp.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,10 @@ example {K : Type*} [Field K] {x : K} (hx0 : x ≠ 0) :
684684
-- new goal: `⊢ (x ^ 2 + 1) * (x ^ 2 + 1 + x) = x ^ 2`
685685
```
686686
687+
A very common pattern is `field_simp; ring` (clear denominators, then the resulting goal is
688+
solvable by the axioms of a commutative ring). The finishing tactic `field` is a shorthand for this
689+
pattern.
690+
687691
Cancelling and combining denominators will generally require checking "nonzeroness"/"positivity"
688692
side conditions. The `field_simp` tactic attempts to discharge these, and will omit such steps if it
689693
cannot discharge the corresponding side conditions. The discharger will try, among other things,

MathlibTest/FieldSimp.lean

Lines changed: 133 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ Copyright (c) 2022 Jon Eugster. All rights reserved.
33
Released under Apache 2.0 license as described in the file LICENSE.
44
Authors: Jon Eugster, David Renshaw, Heather Macbeth, Michael Rothgang
55
-/
6-
import Mathlib.Tactic.FieldSimp
6+
import Mathlib.Tactic.Field
77
import Mathlib.Tactic.Positivity
88
import Mathlib.Tactic.Ring
99
import Mathlib.Data.Real.Basic
1010

1111
/-!
12-
# Tests for the `field_simp` tactic
12+
# Tests for the `field_simp` and `field` tactics
1313
-/
1414

1515
private axiom test_sorry : ∀ {α}, α
@@ -370,16 +370,17 @@ end
370370

371371
/-! ## Cancel denominators from equalities -/
372372

373-
/-! ### Most common use case: Cancel denominators to something solvable by `ring`
373+
/-! ### Finishing tactic
374374
375-
When (eventually) this is robust enough, there should be a `field` tactic
375+
The `field` tactic is a finishing tactic for equalities in fields.
376+
Effectively it runs `field_simp` to clear denominators, then hands the result to `ring1`.
376377
-/
377378

378-
macro "field" : tactic => `(tactic | (try field_simp) <;> ring1)
379-
380379
example : (1:ℚ) / 3 + 1 / 6 = 1 / 2 := by field
381380
example {x : ℚ} (hx : x ≠ 0) : x * x⁻¹ = 1 := by field
382381
example {a b : ℚ} (h : b ≠ 0) : a / b + 2 * a / b + (-a) / b + (- (2 * a)) / b = 0 := by field
382+
383+
-- example from the `field` docstring
383384
example {x y : ℚ} (hx : x + y ≠ 0) : x / (x + y) + y / (x + y) = 1 := by field
384385

385386
example {x : ℚ} : x ^ 2 / (x ^ 2 + 1) + 1 / (x ^ 2 + 1) = 1 := by field
@@ -392,14 +393,67 @@ example {K : Type*} [Field K] (a b c d x y : K) (hx : x ≠ 0) (hy : y ≠ 0) :
392393
a + b / x + c / x ^ 2 + d / x ^ 3 = a + x⁻¹ * (y * b / y + (d / x + c) / x) := by
393394
field
394395

396+
-- example from the `field` docstring
395397
example {a b : ℚ} (ha : a ≠ 0) : a / (a * b) - 1 / b = 0 := by field
398+
396399
example {x : ℚ} : x ^ 2 * x⁻¹ = x := by field
397400

401+
-- example from `field` docstring
402+
example {K : Type*} [Field K] (hK : ∀ x : K, x ^ 2 + 10) (x : K) :
403+
1 / (x ^ 2 + 1) + x ^ 2 / (x ^ 2 + 1) = 1 := by
404+
field [hK]
405+
398406
-- testing that mdata is cleared before parsing goal
399407
example {x : ℚ} (hx : x ≠ 0) : x * x⁻¹ = 1 := by
400408
have : 1 = 1 := rfl
401409
field
402410

411+
-- `field` will suggest `field_simp` on failure, if `field_simp` does anything.
412+
413+
/--
414+
info: Try this:
415+
field_simp
416+
---
417+
error: unsolved goals
418+
x y z : ℚ
419+
hx : x + y ≠ 0
420+
⊢ 1 = z
421+
-/
422+
#guard_msgs in
423+
example {x y z : ℚ} (hx : x + y ≠ 0) : x / (x + y) + y / (x + y) = z := by field
424+
425+
-- If `field` fails but `field_simp` also fails, we just throw an error.
426+
/--
427+
error: ring failed, ring expressions not equal
428+
x y z : ℚ
429+
⊢ x + y = z
430+
-/
431+
#guard_msgs in
432+
example {x y z : ℚ} : x + y = z := by field
433+
434+
/-
435+
The `field` tactic differs slightly from `field_simp; ring1` in that it clears denominators only at
436+
the top level, not recursively in subexpressions.
437+
438+
(`ring1` acts only at the top level, so for consistency we also clear denominators only at the top
439+
level.) -/
440+
/--
441+
info: Try this:
442+
field_simp
443+
---
444+
error: unsolved goals
445+
a b : ℚ
446+
f : ℚ → ℚ
447+
⊢ f (a * b) * (1 - 1) = 0
448+
-/
449+
#guard_msgs in
450+
example (a b : ℚ) (f : ℚ → ℚ) : f (a ^ 2 * b / a) - f (b ^ 2 * a / b) = 0 := by field
451+
452+
-- (Compare with the example above: this is out of scope for `field`.)
453+
example (a b : ℚ) (f : ℚ → ℚ) : f (a ^ 2 * b / a) - f (b ^ 2 * a / b) = 0 := by
454+
field_simp
455+
ring1
456+
403457
/-! ### Mid-proof use -/
404458

405459
example {K : Type*} [Semifield K] (x y : K) (hy : y + 10) : 2 * x / (y + 1) = x := by
@@ -535,9 +589,10 @@ normalize properly.
535589
It is not clear whether or not this iterated alternation always achieves the "obvious" normalization
536590
eventually. Nor is it clear whether, if so, there are any bounds on how many iterations are needed.
537591
-/
538-
section
539592

540593
-- modified from 2021 American Mathematics Competition 12B, problem 9
594+
section
595+
541596
example (P : ℝ → Prop) {x y : ℝ} (hx : 0 < x) (hy : 0 < y) :
542597
P ((4 * x + y) / x / (x / (3 * x + y)) - (5 * x + y) / x / (x / (2 * x + y))) := by
543598
ring_nf
@@ -562,6 +617,77 @@ example (P : ℝ → Prop) {x y : ℝ} (hx : 0 < x) (hy : 0 < y) :
562617

563618
end
564619

620+
section
621+
622+
-- This example is used in the `field` docstring.
623+
example {a b : ℚ} (H : b + a ≠ 0) : a / (a + b) + b / (b + a) = 1 := by
624+
ring_nf at *
625+
field
626+
627+
/--
628+
info: Try this:
629+
field_simp
630+
---
631+
error: unsolved goals
632+
a b : ℚ
633+
H : b + a ≠ 0
634+
⊢ (a + b) / (a + b) = 1
635+
-/
636+
#guard_msgs in
637+
example {a b : ℚ} (H : b + a ≠ 0) : a / (a + b) + b / (b + a) = 1 := by
638+
ring_nf
639+
field
640+
641+
/--
642+
info: Try this:
643+
field_simp
644+
---
645+
error: unsolved goals
646+
a b : ℚ
647+
H : b + a ≠ 0
648+
⊢ a * (b + a) / (a + b) + b = b + a
649+
-/
650+
#guard_msgs in
651+
example {a b : ℚ} (H : b + a ≠ 0) : a / (a + b) + b / (b + a) = 1 := by
652+
field
653+
654+
example {a b : ℚ} (H : a + b + 10) :
655+
a / (a + (b + 1) ^ 2 / (b + 1)) + (b + 1) / (b + a + 1) = 1 := by
656+
field_simp
657+
ring_nf at *
658+
field
659+
660+
/--
661+
info: Try this:
662+
field_simp
663+
---
664+
error: unsolved goals
665+
a b : ℚ
666+
H : 1 + a + b ≠ 0
667+
⊢ a * (1 + a + b) / (a + b * 2 / (1 + b) + b ^ 2 / (1 + b) + 1 / (1 + b)) + b + 1 = 1 + a + b
668+
-/
669+
#guard_msgs in
670+
example {a b : ℚ} (H : a + b + 10) :
671+
a / (a + (b + 1) ^ 2 / (b + 1)) + (b + 1) / (b + a + 1) = 1 := by
672+
ring_nf at *
673+
field
674+
675+
/--
676+
info: Try this:
677+
field_simp
678+
---
679+
error: unsolved goals
680+
a b : ℚ
681+
H : a + b + 1 ≠ 0
682+
⊢ a / (a + (b + 1)) + (b + 1) / (b + a + 1) = 1
683+
-/
684+
#guard_msgs in
685+
example {a b : ℚ} (H : a + b + 10) :
686+
a / (a + (b + 1) ^ 2 / (b + 1)) + (b + 1) / (b + a + 1) = 1 := by
687+
field
688+
689+
end
690+
565691
/-! From PluenneckeRuzsa: new `field_simp` doesn't handle variable exponents -/
566692

567693
example (x y : ℚ≥0) (n : ℕ) (hx : x ≠ 0) : y * ((y / x) ^ n * x) = (y / x) ^ (n + 1) * x * x := by

0 commit comments

Comments
 (0)