Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions Mathlib/Tactic/AddGroup.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/-
Copyright (c) 2020 Patrick Massot. All rights reserved.
Comment thread
kbuzzard marked this conversation as resolved.
Outdated
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Thomas Browning, Patrick Massot
Comment thread
kbuzzard marked this conversation as resolved.
Outdated
-/
module

public import Mathlib.Algebra.Order.Sub.Basic -- shake: keep (tactic dependency)
public meta import Mathlib.Tactic.FailIfNoProgress
public import Mathlib.Tactic.Group -- for zsmul_trick lemmas generated by @[to_additive]
public import Mathlib.Tactic.Ring
Comment on lines +8 to +11
Copy link
Copy Markdown
Contributor

@thorimur thorimur Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Suggested change
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


/-!
# `add_group` tactic

Normalizes expressions in the language of additive groups. The basic idea is to use the simplifier
to put everything into a sum of integer scalar multiples (`zsmul` which takes an integer and an
additive group element), then simplify the scalars using the `ring` tactic. The process needs to be
repeated since `ring` can normalize a scalar to zero, leading to a summand that can be removed
before collecting scalars again. The simplifier step also uses some extra lemmas to avoid
some `ring` invocations.

Note: Unlike the multiplicative `group` tactic which uses `← zpow_neg_one` to convert `a⁻¹` to
`a ^ (-1 : ℤ)`, the additive version cannot use `← neg_one_zsmul` to convert `-a` to
`(-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.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(See other comments)

Suggested change
`zsmul` terms.
`zsmul` terms. We also use `neg_one_zsmul` in the forward direction to normalize `(-1) • b` to `-b`.


## Tags

group theory, additive group
-/

public meta section

namespace Mathlib.Tactic.AddGroup

open Lean
open Lean.Meta
open Lean.Parser.Tactic
open Lean.Elab.Tactic
Comment on lines +42 to +45
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Comment on lines +47 to +62
Copy link
Copy Markdown
Contributor

@thorimur thorimur Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current iteration of add_group can't prove the following:

example (b : G) {n : Int} : (-b) + n • b = (n - 1) • b := by add_group

So 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_group

Since 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! :)

Suggested change
-- 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
[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,
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 -failIfUnchanged $[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]?)))
Comment on lines +64 to +117
Copy link
Copy Markdown
Contributor

@thorimur thorimur Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a couple of things I think we can change here:

  1. 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! 😅) The simp call is maybe worth isolating for readability, but we can do that with a let in the body of the macro_rules for the main tactic. (Honestly maybe we should do this for group too, since I see that's where this pattern is coming from.)
  2. add_group should probably fail with the appropriate message if no progress is made. For that, we can use first | repeat1 (fail_if_no_progress ...) | fail "`add_group` made no progress; repeat1 ensures that the tactic is successful at least once. Ideally ifUnchanged becomes a config option to add_group, but we can handle that in a separate PR :)
  3. Nit: syntax kind (name := ...)s are usually lowerCamelCase.
  4. (Future issue) Technically repeat(1) (fail_if_no_progress ...) is a bad pattern, I think, because repeat repeats until any failure, then rewinds. We should probably only be repeating until we stop making progress, and surface any other non-progress-related errors. repeat obscures the latter sort of error. (I'm also a little worried about fail_if_no_progress's main-goal-only focus when combined with repeat.) But, this is also a problem with group. 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! :)

Suggested change
/-- 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")


end Mathlib.Tactic.AddGroup

/-!
We register `add_group` with the `hint` tactic.
-/

register_hint 900 add_group
56 changes: 56 additions & 0 deletions MathlibTest/AddGroup.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
module
import Mathlib.Tactic.AddGroup

variable {G : Type} [AddGroup G]

example (a b c : G) : c + (a + b) + (-b + -a) + c = c + c := by add_group

example (a b c : G) : (b + -c) + c + (a + b) + (-b + -a) + c = b + c := by add_group

example (a b c : G) : -c + (b + -c) + c + (a + b) + (-b + -a + -b) + c = 0 := by add_group

-- NB the Hall-Witt identity is here in the Group.lean test, but this involves commutators
-- which have no additive analogue, so this test was not additivised.
Comment thread
kbuzzard marked this conversation as resolved.
Outdated

example (a : G) : 2 • a + a = 3 • a := by add_group

example (n m : ℕ) (a : G) : n • a + m • a = (n + m) • a := by add_group

example (a b c : G) : c + (a + 2 • b) + (-(b + b) + -a) + c = c + c := by add_group

example (n : ℕ) (a : G) : n • a + n • (-a) = 0 := by add_group

example (a : G) : 2 • a + -a + -a = 0 := by add_group

example (n m : ℕ) (a : G) : n • a + m • a = (m + n) • a := by add_group

example (n : ℕ) (a : G) : (n - n) • a = 0 := by add_group

example (n : ℤ) (a : G) : (n - n) • a = 0 := by add_group

Copy link
Copy Markdown
Contributor

@thorimur thorimur Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add these tests, which failed without the new extra three lemmas + neg_one_zsmul added in another suggestion.

Suggested change
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

example (n : ℤ) (a : G) (h : (n * (n + 1) - n - n ^ 2) • a = a) : a = 0 := by
add_group at h
exact h.symm

example (a b c d : G) (h : c = (a + 2 • b) + (-(b + b) + -a) + d) : a + c + -d = a := by
add_group at h
rw [h]
add_group

-- The next example can be expanded to require an arbitrarily high number of alternations
-- between simp and ring

example (n m : ℤ) (a b : G) : (m - n) • a + (m - n) • b + (n - m) • b + (n - m) • a = 0 := by
add_group

example (n : ℤ) (a b : G) :
n • a + n • b + n • a + n • a + (-n) • a + (-n) • a + (-n) • b + (-n) • a = 0 := by add_group

-- Test that add_group deals with `-(0)` properly

example (x y : G) : -((-x) + (x + y) + (-y)) = 0 := by add_group

set_option linter.unusedTactic false in
example (x : G) (h : x = 0) : x = 0 := by
add_group
exact h
Comment on lines +59 to +62
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given other suggestions:

Suggested change
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

Loading