feat: add_group tactic#37067
Conversation
PR summary 028964f2c6Import changes for modified filesNo significant changes to the import graph Import changes for all files
|
|
This pull request has conflicts, please merge |
Co-authored-by: Snir Broshi <26556598+SnirBroshi@users.noreply.github.com>
There was a problem hiding this comment.
Thanks for this! :) I think this is a good use of an LLM: closely patterning off of existing material plus human thought about the output.
The code is quite clean, but has two (fixable) issues:
- Style-wise, we should actually disprefer the pattern of single-use aux tactics that it's patterned off of! :) There are also a couple small issues re: control flow and error messages.
- It doesn't take care of cases like
(-b) + n • b = (n - 1) • b, because the trick lemmas only apply to e.g.(_ + (-b)) + n • b, which has the wrong associativity. So we just need three more lemmas. We also need to useneg_one_zsmulthe ordinary way round to handle(-1) • b = -b.
Hopefully all of these are resolved by these suggestions.
I haven't done a full audit of the simp set to ensure that every lemma is used, but it seems reasonable.
| /- | ||
| Copyright (c) 2026 Kevin Buzzard. All rights reserved. | ||
| Released under Apache 2.0 license as described in the file LICENSE. | ||
| Authors: Thomas Browning, Patrick Massot, Kevin Buzzard |
There was a problem hiding this comment.
Hmm, do we have guidelines for handling authorship when taking large inspiration from another file? On the one hand, it's nice to give credit. On the other hand, authorship answers the question "who do I ping on zulip", and is the answer this gives correct? (Have they been asked if they'd like to be "held responsible"?)
I'm tempted to say the right move is actually just
| Authors: Thomas Browning, Patrick Massot, Kevin Buzzard | |
| Authors: Kevin Buzzard |
and credit them in the module docstring, unless you've discussed this with them!
| open Lean | ||
| open Lean.Meta | ||
| open Lean.Parser.Tactic | ||
| open Lean.Elab.Tactic |
There was a problem hiding this comment.
| open Lean | |
| open Lean.Meta | |
| open Lean.Parser.Tactic | |
| open Lean.Elab.Tactic | |
| open Lean Meta Parser Elab Tactic |
| -- The next three lemmas are not general purpose lemmas, they are intended for use only by | ||
| -- the `add_group` tactic. | ||
| -- They handle the case where a negated element `-b` appears adjacent to a `zsmul` of the same | ||
| -- element, or adjacent to another negated copy. | ||
| theorem zsmul_neg_trick {G : Type*} [AddGroup G] (a b : G) (n : ℤ) : | ||
| a + n • b + (-b) = a + (n + (-1)) • b := by | ||
| rw [add_assoc, ← neg_one_zsmul b, ← add_zsmul] | ||
|
|
||
| theorem zsmul_neg_trick' {G : Type*} [AddGroup G] (a b : G) (n : ℤ) : | ||
| a + (-b) + n • b = a + ((-1) + n) • b := by | ||
| rw [add_assoc, ← neg_one_zsmul b, ← add_zsmul] | ||
|
|
||
| theorem neg_neg_trick {G : Type*} [AddGroup G] (a b : G) : | ||
| a + (-b) + (-b) = a + (-2 : ℤ) • b := by | ||
| have h : -b = (-1 : ℤ) • b := (neg_one_zsmul b).symm | ||
| rw [h, add_assoc, ← add_zsmul]; norm_num |
There was a problem hiding this comment.
The current iteration of add_group can't prove the following:
example (b : G) {n : Int} : (-b) + n • b = (n - 1) • b := by add_groupSo I believe we just need to duplicate these trick lemmas to handle the (_ + _) cases; they seem to only handle the ((_ + _) + _) cases.
We also struggle with
example (b : G) : (-b) = (-1) • b := by add_groupSince we're committing to the neg lifestyle via the other trick lemmas, I think we actually want to use neg_one_zsmul in the ordinary direction to put (-1) • in terms of -, then (in more general cases) apply our trick lemmas.
Here, I also deapostrophe'd their names to be more descriptive, and prefixed them with a _. This causes them to be filtered out from autocompletion! :)
| -- The next three lemmas are not general purpose lemmas, they are intended for use only by | |
| -- the `add_group` tactic. | |
| -- They handle the case where a negated element `-b` appears adjacent to a `zsmul` of the same | |
| -- element, or adjacent to another negated copy. | |
| theorem zsmul_neg_trick {G : Type*} [AddGroup G] (a b : G) (n : ℤ) : | |
| a + n • b + (-b) = a + (n + (-1)) • b := by | |
| rw [add_assoc, ← neg_one_zsmul b, ← add_zsmul] | |
| theorem zsmul_neg_trick' {G : Type*} [AddGroup G] (a b : G) (n : ℤ) : | |
| a + (-b) + n • b = a + ((-1) + n) • b := by | |
| rw [add_assoc, ← neg_one_zsmul b, ← add_zsmul] | |
| theorem neg_neg_trick {G : Type*} [AddGroup G] (a b : G) : | |
| a + (-b) + (-b) = a + (-2 : ℤ) • b := by | |
| have h : -b = (-1 : ℤ) • b := (neg_one_zsmul b).symm | |
| rw [h, add_assoc, ← add_zsmul]; norm_num | |
| -- The next six lemmas are not general purpose lemmas; they are intended for use only by | |
| -- the `add_group` tactic, and so are prefixed with `_` to keep them out of autocomplete. | |
| -- They handle the case where a negated element `-b` appears adjacent to a `zsmul` of the same | |
| -- element, or adjacent to another negated copy. | |
| -- This means we also want to convert `(-1) • b` to `-b` in order to apply these lemmas. | |
| theorem _zsmul_neg_trick {G : Type*} [AddGroup G] (b : G) (n : ℤ) : | |
| n • b + (-b) = (n + (-1)) • b := by | |
| rw [← neg_one_zsmul b, ← add_zsmul] | |
| theorem _neg_zsmul_trick {G : Type*} [AddGroup G] (b : G) (n : ℤ) : | |
| (-b) + n • b = ((-1) + n) • b := by | |
| rw [← neg_one_zsmul b, ← add_zsmul] | |
| theorem _neg_neg_trick {G : Type*} [AddGroup G] (b : G) : | |
| (-b) + (-b) = (-2 : ℤ) • b := by | |
| have h : -b = (-1 : ℤ) • b := (neg_one_zsmul b).symm | |
| rw [h, ← add_zsmul]; norm_num | |
| theorem _add_zsmul_neg_trick {G : Type*} [AddGroup G] (a b : G) (n : ℤ) : | |
| a + n • b + (-b) = a + (n + (-1)) • b := by | |
| rw [add_assoc, _zsmul_neg_trick] | |
| theorem _add_neg_zsmul_trick {G : Type*} [AddGroup G] (a b : G) (n : ℤ) : | |
| a + (-b) + n • b = a + ((-1) + n) • b := by | |
| rw [add_assoc, _neg_zsmul_trick] | |
| theorem _add_neg_neg_trick {G : Type*} [AddGroup G] (a b : G) : | |
| a + (-b) + (-b) = a + (-2 : ℤ) • b := by | |
| rw [add_assoc, _neg_neg_trick] |
| /-- Auxiliary tactic for the `add_group` tactic. Calls the simplifier only. -/ | ||
| syntax (name := aux_add_group₁) "aux_add_group₁" (location)? : tactic | ||
|
|
||
| macro_rules | ||
| | `(tactic| aux_add_group₁ $[at $location]?) => | ||
| `(tactic| simp -decide -failIfUnchanged only | ||
| [addCommutatorElement_def, neg_add_rev, neg_zero, zsmul_neg, ← neg_zsmul, | ||
| add_zero, zero_add, | ||
| ← natCast_zsmul, ← mul_zsmul', | ||
| Int.natCast_add, Int.natCast_mul, neg_neg, | ||
| zsmul_zero, zero_zsmul, one_zsmul, | ||
| ← add_assoc, | ||
| ← add_zsmul, ← add_one_zsmul, ← one_add_zsmul, | ||
| Mathlib.Tactic.Group.zsmul_trick, | ||
| Mathlib.Tactic.Group.zsmul_trick_zero, | ||
| Mathlib.Tactic.Group.zsmul_trick_zero', | ||
| zsmul_neg_trick, zsmul_neg_trick', | ||
| add_neg_cancel_right, neg_add_cancel_right, | ||
| neg_neg_trick, | ||
| sub_eq_add_neg, | ||
| tsub_self, sub_self, add_neg_cancel, neg_add_cancel] | ||
| $[at $location]?) | ||
|
|
||
| /-- Auxiliary tactic for the `add_group` tactic. Calls `ring_nf` to normalize scalars. -/ | ||
| syntax (name := aux_add_group₂) "aux_add_group₂" (location)? : tactic | ||
|
|
||
| macro_rules | ||
| | `(tactic| aux_add_group₂ $[at $location]?) => | ||
| `(tactic| ring_nf (ifUnchanged := .silent) $[at $location]?) | ||
|
|
||
| /-- `add_group` normalizes expressions in additive groups that occur in the goal. `add_group` does | ||
| not assume commutativity, instead using only the additive group axioms without any information about | ||
| which group is manipulated. If the goal is an equality, and after normalization the two sides are | ||
| equal, `add_group` closes the goal. | ||
|
|
||
| For multiplicative groups, use the `group` tactic instead. | ||
| For additive commutative groups, use the `abel` tactic instead. | ||
|
|
||
| * `add_group at l1 l2 ...` normalizes at the given locations. | ||
|
|
||
| Example: | ||
| ```lean | ||
| example {G : Type} [AddGroup G] (a b c d : G) (h : c = (a + 2 • b) + (-(b + b) + (-a)) + d) : | ||
| a + c + (-d) = a := by | ||
| add_group at h -- normalizes `h` which becomes `h : c = d` | ||
| rw [h] -- the goal is now `a + d + (-d) = a` | ||
| add_group -- which is then normalized and closed | ||
| ``` | ||
| -/ | ||
| syntax (name := add_group) "add_group" (location)? : tactic | ||
|
|
||
| macro_rules | ||
| | `(tactic| add_group $[$loc]?) => | ||
| `(tactic| repeat (fail_if_no_progress (aux_add_group₁ $[$loc]? <;> aux_add_group₂ $[$loc]?))) |
There was a problem hiding this comment.
There are a couple of things I think we can change here:
- We don't actually need the
aux_*tactics. We can/should just inline them inside the macro, since we use them exactly once and don't extend them downstream. (Otherwise their only purpose nowadays is basically to pollute the tactic documentation page! 😅) Thesimpcall is maybe worth isolating for readability, but we can do that with aletin the body of themacro_rulesfor the main tactic. (Honestly maybe we should do this forgrouptoo, since I see that's where this pattern is coming from.) add_groupshould probably fail with the appropriate message if no progress is made. For that, we can usefirst | repeat1 (fail_if_no_progress ...) | fail "`add_group` made no progress;repeat1ensures that the tactic is successful at least once. IdeallyifUnchangedbecomes a config option toadd_group, but we can handle that in a separate PR :)- Nit: syntax kind
(name := ...)s are usually lowerCamelCase. - (Future issue) Technically
repeat(1) (fail_if_no_progress ...)is a bad pattern, I think, becauserepeatrepeats until any failure, then rewinds. We should probably only be repeating until we stop making progress, and surface any other non-progress-related errors.repeatobscures the latter sort of error. (I'm also a little worried aboutfail_if_no_progress's main-goal-only focus when combined withrepeat.) But, this is also a problem withgroup. So let's not hold up this functionality over that.
I've added the new lemmas/names from the above suggestion, including neg_one_zsmul in the forward direction, and tweaked the docstring to try to point users to abel earlier if it better suits their needs. Feel free to tweak it further! :)
| /-- Auxiliary tactic for the `add_group` tactic. Calls the simplifier only. -/ | |
| syntax (name := aux_add_group₁) "aux_add_group₁" (location)? : tactic | |
| macro_rules | |
| | `(tactic| aux_add_group₁ $[at $location]?) => | |
| `(tactic| simp -decide -failIfUnchanged only | |
| [addCommutatorElement_def, neg_add_rev, neg_zero, zsmul_neg, ← neg_zsmul, | |
| add_zero, zero_add, | |
| ← natCast_zsmul, ← mul_zsmul', | |
| Int.natCast_add, Int.natCast_mul, neg_neg, | |
| zsmul_zero, zero_zsmul, one_zsmul, | |
| ← add_assoc, | |
| ← add_zsmul, ← add_one_zsmul, ← one_add_zsmul, | |
| Mathlib.Tactic.Group.zsmul_trick, | |
| Mathlib.Tactic.Group.zsmul_trick_zero, | |
| Mathlib.Tactic.Group.zsmul_trick_zero', | |
| zsmul_neg_trick, zsmul_neg_trick', | |
| add_neg_cancel_right, neg_add_cancel_right, | |
| neg_neg_trick, | |
| sub_eq_add_neg, | |
| tsub_self, sub_self, add_neg_cancel, neg_add_cancel] | |
| $[at $location]?) | |
| /-- Auxiliary tactic for the `add_group` tactic. Calls `ring_nf` to normalize scalars. -/ | |
| syntax (name := aux_add_group₂) "aux_add_group₂" (location)? : tactic | |
| macro_rules | |
| | `(tactic| aux_add_group₂ $[at $location]?) => | |
| `(tactic| ring_nf (ifUnchanged := .silent) $[at $location]?) | |
| /-- `add_group` normalizes expressions in additive groups that occur in the goal. `add_group` does | |
| not assume commutativity, instead using only the additive group axioms without any information about | |
| which group is manipulated. If the goal is an equality, and after normalization the two sides are | |
| equal, `add_group` closes the goal. | |
| For multiplicative groups, use the `group` tactic instead. | |
| For additive commutative groups, use the `abel` tactic instead. | |
| * `add_group at l1 l2 ...` normalizes at the given locations. | |
| Example: | |
| ```lean | |
| example {G : Type} [AddGroup G] (a b c d : G) (h : c = (a + 2 • b) + (-(b + b) + (-a)) + d) : | |
| a + c + (-d) = a := by | |
| add_group at h -- normalizes `h` which becomes `h : c = d` | |
| rw [h] -- the goal is now `a + d + (-d) = a` | |
| add_group -- which is then normalized and closed | |
| ``` | |
| -/ | |
| syntax (name := add_group) "add_group" (location)? : tactic | |
| macro_rules | |
| | `(tactic| add_group $[$loc]?) => | |
| `(tactic| repeat (fail_if_no_progress (aux_add_group₁ $[$loc]? <;> aux_add_group₂ $[$loc]?))) | |
| /-- | |
| `add_group` normalizes expressions in additive groups without assuming commutativity. Unlike | |
| `abel`, which does take advantage of commutativity, `add_group` instead only uses the additive | |
| group axioms without any information about which group is manipulated. If the goal is an equality, | |
| and after normalization the two sides are equal, `add_group` closes the goal. | |
| `add_group at l1 l2 ...` normalizes at the given locations. | |
| For additive commutative groups, use the `abel` tactic instead. | |
| For multiplicative groups, use the `group` tactic instead. | |
| Example: | |
| ```lean | |
| example {G : Type} [AddGroup G] (a b c d : G) (h : c = (a + 2 • b) + (-(b + b) + (-a)) + d) : | |
| a + c + (-d) = a := by | |
| add_group at h -- normalizes `h` which becomes `h : c = d` | |
| rw [h] -- the goal is now `a + d + (-d) = a` | |
| add_group -- which is then normalized and closed | |
| ``` | |
| -/ | |
| syntax (name := addGroup) "add_group" (location)? : tactic | |
| macro_rules | |
| | `(tactic| add_group $[$loc]?) => do | |
| let simpTacStx ← | |
| `(tactic| simp -decide -failIfUnchanged only | |
| [addCommutatorElement_def, neg_add_rev, neg_zero, zsmul_neg, ← neg_zsmul, | |
| add_zero, zero_add, | |
| ← natCast_zsmul, ← mul_zsmul', | |
| Int.natCast_add, Int.natCast_mul, | |
| neg_neg, | |
| zsmul_zero, zero_zsmul, one_zsmul, | |
| ← add_assoc, | |
| ← add_zsmul, ← add_one_zsmul, ← one_add_zsmul, | |
| neg_one_zsmul, | |
| Mathlib.Tactic.Group.zsmul_trick, | |
| Mathlib.Tactic.Group.zsmul_trick_zero, | |
| Mathlib.Tactic.Group.zsmul_trick_zero', | |
| Mathlib.Tactic.AddGroup._zsmul_neg_trick, | |
| Mathlib.Tactic.AddGroup._neg_zsmul_trick, | |
| Mathlib.Tactic.AddGroup._neg_neg_trick, | |
| Mathlib.Tactic.AddGroup._add_zsmul_neg_trick, | |
| Mathlib.Tactic.AddGroup._add_neg_zsmul_trick, | |
| Mathlib.Tactic.AddGroup._add_neg_neg_trick, | |
| add_neg_cancel_right, neg_add_cancel_right, | |
| sub_eq_add_neg, | |
| tsub_self, sub_self, add_neg_cancel, neg_add_cancel] | |
| $[$loc]?) | |
| /- First, we simplify with a custom simp set, then use `ring_nf`. If neither make progress, we | |
| stop. If we do not make any progress at all, we fail. -/ | |
| `(tactic| first | |
| | repeat1 (fail_if_no_progress ($simpTacStx <;> ring_nf (ifUnchanged := .silent) $[$loc]?)) | |
| | fail "`add_group` made no progress") |
| Other than this issue, this is a direct port from Thomas Browning and Patrick Massot's `group` | ||
| tactic. |
There was a problem hiding this comment.
Technically, after these changes, it will be a little different... :)
| Other than this issue, this is a direct port from Thomas Browning and Patrick Massot's `group` | |
| tactic. | |
| Other than this issue, the strategy parallels Thomas Browning and Patrick Massot's `group` | |
| tactic. |
| public import Mathlib.Algebra.Order.Sub.Basic | ||
| public meta import Mathlib.Tactic.FailIfNoProgress | ||
| public import Mathlib.Tactic.Group | ||
| public import Mathlib.Tactic.Ring |
There was a problem hiding this comment.
As far as I can see this is the only import we need. But maybe should keep the other two in comments if they provide the other lemmas, in case that changes in the future?
Do you know if that is indeed why they're present, or were the imports LLM-generated as well?
| public import Mathlib.Algebra.Order.Sub.Basic | |
| public meta import Mathlib.Tactic.FailIfNoProgress | |
| public import Mathlib.Tactic.Group | |
| public import Mathlib.Tactic.Ring | |
| public import Mathlib.Tactic.Group |
|
|
||
| variable {G : Type} [AddGroup G] | ||
|
|
||
| attribute [instance] addCommutatorElement |
There was a problem hiding this comment.
The fact that something like open scoped (add)commutatorElement doesn't work is surely an oversight in the source file for addCommutatorElement, right (given the docstring)? Can we fix that (possibly in another PR)?
There was a problem hiding this comment.
Aha, great! :)
So let's wait for that to be merged then change this to
| attribute [instance] addCommutatorElement | |
| open scoped addCommutatorElement |
| example (n : ℕ) (a : G) : (n - n) • a = 0 := by add_group | ||
|
|
||
| example (n : ℤ) (a : G) : (n - n) • a = 0 := by add_group | ||
|
|
There was a problem hiding this comment.
Let's add these tests, which failed without the new extra three lemmas + neg_one_zsmul added in another suggestion.
| example (b : G) {n : ℤ} : (-b) + n • b = (n - 1) • b := by add_group | |
| example (b : G) : (-b) + (-b) = (-2) • b := by add_group | |
| example (b : G) {n : ℤ} : n • b + (-b) = (n - 1) • b := by add_group | |
| example (b : G) : -b = (-1) • b := by add_group | |
| set_option linter.unusedTactic false in | ||
| example (x : G) (h : x = 0) : x = 0 := by | ||
| add_group | ||
| exact h |
There was a problem hiding this comment.
Given other suggestions:
| set_option linter.unusedTactic false in | |
| example (x : G) (h : x = 0) : x = 0 := by | |
| add_group | |
| exact h | |
| /-- | |
| error: `add_group` made no progress | |
| G : Type | |
| inst✝ : AddGroup G | |
| x : G | |
| h : x = 0 | |
| ⊢ x = 0 | |
| -/ | |
| #guard_msgs in | |
| example (x : G) (h : x = 0) : x = 0 := by | |
| add_group |
| `(-1 : ℤ) • a` because `(-1 : ℤ)` is itself `-(1 : ℤ)`, causing simp to loop (since `ℤ` is also | ||
| an `AddGroup`). Instead, we handle negation via `neg_add_rev` to distribute negation over sums, | ||
| `zsmul_neg`/`neg_zsmul` for `n • (-a)`, and custom trick lemmas for combining `-b` with adjacent | ||
| `zsmul` terms. |
There was a problem hiding this comment.
(See other comments)
| `zsmul` terms. | |
| `zsmul` terms. We also use `neg_one_zsmul` in the forward direction to normalize `(-1) • b` to `-b`. |
Written by Claude Code, additivising the
grouptactic, although there were some problems which needed human input.This is me burning Claude credits as a result of this comment #mathlib4 > MulAut and to_additive @ 💬 . Just to be clear, all comments below are human-written by me with 0 AI assistance.
To my amusement, naively additivising the
grouptactic to make anadd_grouptactic does not work. The problem is that the algorithm run bygroupinvolves adding← zpow_neg_oneto the simp set, which rewritesx⁻¹tox ^ (-1)forxin any group: the idea is thatx ^ (z : Int)is preferred in the algorithm andx⁻¹is eliminated. Amusingly, when additivised, this becomes-x -> -1 • xwhich then immediately becomes(-1 • 1) • xwhich then becomes((-1 • 1) • 1) • xetc etc, as the integers are themselves an additive group. So we need to find another way of normalising terms of the form-x.Claude found the following trick: drop
← neg_one_zsmulandneg_one_zsmul_add, and useneg_add_revandneg_zeroto reduce-(a+b)and-0, usezsmul_negto reducen • (-a)to(-n) • aand then use things likeadd_neg_cancel_rightandneg_add_cancel_rightto do-b + bandb + -btogether with some newzsmul_neg_tricklemmas which normalise(n • b) + -bto(n - 1) • b. etc. The idea is to ensure that any consecutive chains ofb's and-b's andz • b's will now all get amalgamated into something of the formz • b, but we cannot change-bto-1 • bso have to try harder than in the multiplicative case.Here is a detailed (human-written!) explanation of the difference between
Tactic/AddGroup.leanand the additivisation ofTactic/Group.Lean.Note that the
zpow_tricklemmas inTactic/Group.leanare additivised, so we importGroup.leaninAddGroup.leanto access these additivised lemmas.Mathlib.Algebra.Group.Commutatorhas no additive version (and it's not clear that it should have one), so we don't use an additive version ofcommutatorElement_def(which expands⁅g₁, g₂⁆tog₁ * g₂ * g₁⁻¹ * g₂⁻¹). Note: that means that we cannot additivise a test.Line 67 of the additive file uses
neg_add_rev, neg_zero, zsmul_neg, ← neg_zsmul,as a workaround for the looping← zpow_neg_one.Int.mul_neg, Int.neg_mulare dropped (they don't seem to be needed, all tests are passing, ring_nf is probably doing this.)sub_eq_add_negis added: people use subtraction in additive groups; division doesn't seem to be covered for multiplicative groups? Soadd_groupcan solvea + b - b = abutgroupcannot solvea * b / b = a.There is also one fewer test in the
add_grouptests, as I had to remove the commutator test.I don't know much at all about tactic-writing, the PR is all Claude code, but a lot of it is to_additive copypasta. The review of the code above is my own.