|
| 1 | +/- |
| 2 | +Copyright (c) 2025 Lean FRO LLC. All rights reserved. |
| 3 | +Released under Apache 2.0 license as described in the file LICENSE. |
| 4 | +Author: David Thrane Christiansen |
| 5 | +-/ |
| 6 | + |
| 7 | +import VersoManual |
| 8 | + |
| 9 | +import Lean.Parser.Term |
| 10 | + |
| 11 | +import Manual.Meta |
| 12 | +import Manual.Papers |
| 13 | + |
| 14 | + |
| 15 | +open Verso.Genre Manual |
| 16 | +open Verso.Genre.Manual.InlineLean |
| 17 | +open Verso.Doc.Elab (CodeBlockExpander) |
| 18 | +open Verso.Code.External (lit) |
| 19 | + |
| 20 | +open Lean.Elab.Tactic.GuardMsgs.WhitespaceMode |
| 21 | + |
| 22 | +#doc (Manual) "Associativity and Commutativity" => |
| 23 | +%%% |
| 24 | +tag := "grind-ac" |
| 25 | +%%% |
| 26 | + |
| 27 | +When an operator is associative, {tactic}`grind` can efficiently rewrite terms that contain the operator to a normal form, making it easier to prove equalities that involve the operator. |
| 28 | +This rewriting can make use of further facts about the operator, such as the fact that it is commutative or idempotent, or that certain values are identities. |
| 29 | +Rewriting occurs both when adding terms to the “whiteboard” and in response to facts added by other solvers. |
| 30 | +This solver is controlled using the `ac` flag, and it is on by default. |
| 31 | +The `acSteps` option, which defaults to `1000`, controls how many `ac` rewriting steps are allowed. |
| 32 | + |
| 33 | +:::example "Strings" |
| 34 | +The `ac` solver can reason about strings. |
| 35 | +It's common for string-processing code to compute a prefix, and then later append further values. |
| 36 | +Using `ac`, these can be reasoned about more conveniently: |
| 37 | +```lean |
| 38 | +example (dir file p) (h : dir ++ "/" = p) : |
| 39 | + dir ++ ("/" ++ file) = p ++ file := by grind |
| 40 | +``` |
| 41 | + |
| 42 | +```lean -show +error |
| 43 | +-- To make sure it's AC solving this |
| 44 | +example (dir file p) (h : dir ++ "/" = p) : |
| 45 | + dir ++ ("/" ++ file) = p ++ file := by grind -ac |
| 46 | +``` |
| 47 | +::: |
| 48 | + |
| 49 | +# Normal Forms |
| 50 | + |
| 51 | +:::leanSection |
| 52 | +```lean -show |
| 53 | +variable {α : Type u} {op : α → α → α} [Std.Associative op] {u x y z : α} |
| 54 | +``` |
| 55 | + |
| 56 | +The normal forms used by the `ac` solver depend on the properties that have been registered for the operator in question. |
| 57 | + |
| 58 | +: Associativity |
| 59 | + |
| 60 | + Associative operators are normalized by reducing nested trees of the operator to lists of operands. |
| 61 | + If {lean}`op` is associative, then the normal form of both {lean}`op (op x y) z` and {lean}`op x (op y z)` is {lean}`[x, y, z]`. |
| 62 | + |
| 63 | +: Commutativity |
| 64 | + |
| 65 | + Operators that are associative and commutative are normalized by sorting the operand lists. |
| 66 | + If {lean}`op` is associative and commutative, then the normal form of both {lean}`op z (op x y)` and {lean}`op (op x z) y` is {lean}`[x, y, z]`. |
| 67 | + In other words, the normal form is a multiset. |
| 68 | + |
| 69 | +: Idempotence |
| 70 | + |
| 71 | + If the operator is idempotent, then runs of duplicate elements are collapsed in the operand list. |
| 72 | + If {lean}`op` is associative and idempotent, then the normal form of {lean}`op x (op x y)` is {lean}`[x, y]`, but the normal form of {lean}`op (op x y) x` is still {lean}`[x, y, x]`. |
| 73 | + This collapsing occurs after sorting the list if the operator is commutative, so if {lean}`op` is associative, commutative, and idempotent, then the normal form of {lean}`op (op x y) x` is also {lean}`[x, y]`. |
| 74 | + In other words, the normal form of an associative, commutative, and idempotent operator is a set. |
| 75 | + |
| 76 | +: Identities |
| 77 | + |
| 78 | + If the operator has a unit, then it is removed from the normal form. |
| 79 | + That is, if {lean}`u` is a unit of the associative operator {lean}`op`, then the normal form of {lean}`op x (op u y)` is {lean}`[x, y]`. |
| 80 | + |
| 81 | +::: |
| 82 | + |
| 83 | + |
| 84 | +# Extension |
| 85 | + |
| 86 | +The `ac` solver can be extended to support new operators by providing an instance of {name}`Std.Associative` for the operator. |
| 87 | +Instances of {name}`Std.Commutative`, {name}`Std.IdempotentOp`, and {name}`Std.LawfulIdentity` further extend its capabilities by identifying more terms. |
| 88 | + |
| 89 | +{docstring Std.Associative} |
| 90 | + |
| 91 | +{docstring Std.Commutative} |
| 92 | + |
| 93 | +{docstring Std.IdempotentOp} |
| 94 | + |
| 95 | +{docstring Std.LawfulIdentity} |
| 96 | + |
| 97 | +While extending the {lit}`ac` solver, it can be useful to observe its operation. |
| 98 | +The option {option}`trace.grind.ac`, as well as the more specific options {option}`trace.grind.ac.assert`, {option}`trace.grind.ac.internalize`, and {option}`trace.grind.ac.basis`, can be used to observe its internal behavior. |
| 99 | +In particular, {option}`trace.grind.ac.assert` displays the results of normalization that are added to the “whiteboard.” |
| 100 | + |
| 101 | +{optionDocs trace.grind.ac} |
| 102 | + |
| 103 | +{optionDocs trace.grind.ac.assert} |
| 104 | + |
| 105 | +{optionDocs trace.grind.ac.internalize} |
| 106 | + |
| 107 | +{optionDocs trace.grind.ac.basis} |
| 108 | + |
| 109 | +:::example "Idempotence and Identity" |
| 110 | + |
| 111 | +{name}`Flags` tracks a set of Boolean flags, each of which corresponds to a bit position. |
| 112 | +Each flag is either set or clear, and all flags are clear when the bit value is {lean}`0`. |
| 113 | +The union of two sets of flags is found by taking the bit-wise OR of their bit representations. |
| 114 | +```lean |
| 115 | +structure Flags where |
| 116 | + bits : Nat |
| 117 | + |
| 118 | +namespace Flags |
| 119 | + |
| 120 | +def union (a b : Flags) : Flags := ⟨a.bits ||| b.bits⟩ |
| 121 | +def none : Flags := ⟨0⟩ |
| 122 | + |
| 123 | +``` |
| 124 | +The union operation on flags is associative, commutative, and idempotent, and {name}`Flags.none` is an identity: |
| 125 | +```lean |
| 126 | +instance : Std.Associative union where |
| 127 | + assoc x y z := by simp only [union, mk.injEq]; grind |
| 128 | +instance : Std.Commutative union where |
| 129 | + comm x y := by simp only [union, mk.injEq]; grind |
| 130 | +instance : Std.IdempotentOp union where |
| 131 | + idempotent x := by simp [union] |
| 132 | +instance : Std.LawfulIdentity union none where |
| 133 | + left_id a := by simp [union, none] |
| 134 | + right_id a := by simp [union, none] |
| 135 | +``` |
| 136 | +With these instances, the `ac` solver can dispatch all of the following goals. |
| 137 | +In the second example, idempotency can only be used due to commutativity, because the two identical arguments are not adjacent in the original term. |
| 138 | +```lean |
| 139 | +example : union a (union b c) = union (union c a) b := by grind |
| 140 | +example : union (union a b) a = union a b := by grind |
| 141 | +example : union a none = a := by grind |
| 142 | +example : |
| 143 | + union (union a b) (union none (union b c)) |
| 144 | + = union a (union b c) := by |
| 145 | + grind |
| 146 | +``` |
| 147 | +The negated normal-form equality that is added to the context can be seen using {option}`trace.grind.ac.assert`: |
| 148 | +```lean (name := traceAc) |
| 149 | +set_option trace.grind.ac.assert true in |
| 150 | +example : union (union a b) a = union a (union b none) := by grind |
| 151 | +``` |
| 152 | +```leanOutput traceAc |
| 153 | +[grind.ac.assert] a.union b ≠ a.union b |
| 154 | +``` |
| 155 | + |
| 156 | +```lean -show |
| 157 | +-- check solver |
| 158 | +/-- error: `grind` failed -/ |
| 159 | +#guard_msgs (substring := true) in |
| 160 | +example : union a (union b c) = union (union c a) b := by grind -ac |
| 161 | + |
| 162 | +/-- error: `grind` failed -/ |
| 163 | +#guard_msgs (substring := true) in |
| 164 | +example : union (union a b) a = union a b := by grind -ac |
| 165 | + |
| 166 | +/-- error: `grind` failed -/ |
| 167 | +#guard_msgs (substring := true) in |
| 168 | +example : union a none = a := by grind -ac |
| 169 | + |
| 170 | +/-- error: `grind` failed -/ |
| 171 | +#guard_msgs (substring := true) in |
| 172 | +example : |
| 173 | + union (union a b) (union none (union b c)) |
| 174 | + = union a (union b c) := by grind -ac |
| 175 | + |
| 176 | +``` |
| 177 | + |
| 178 | +::: |
| 179 | + |
| 180 | +::::example "Difference Lists" (tag := "difference-lists-ac") |
| 181 | + |
| 182 | +A {deftech}_difference list_ is a clever representation of lists as functions that avoids the quadratic overhead of appending to the end of lists. |
| 183 | +Because Lean supports {ref "Array"}[efficient arrays], they are typically not useful in day-to-day code, but they effectively demonstrate how to extend the `ac` solver. |
| 184 | + |
| 185 | +:::leanSection |
| 186 | +```lean -show |
| 187 | +variable {xs ys : List α} |
| 188 | +``` |
| 189 | +A difference list is a function from lists to lists. |
| 190 | +The list {lean}`xs` is represented by a function that, when applied to {lean}`ys`, returns {lean}`xs ++ ys`. |
| 191 | +::: |
| 192 | + |
| 193 | +```lean |
| 194 | +def DList α := List α → List α |
| 195 | +``` |
| 196 | + |
| 197 | +The empty list is the identity function. |
| 198 | +An item is added to the beginning of a list by creating a function that adds the element. |
| 199 | +Appending two lists is function composition. |
| 200 | +```lean |
| 201 | +def DList.empty : DList α := id |
| 202 | + |
| 203 | +def DList.cons (x : α) (xs : DList α) : DList α := |
| 204 | + fun ys => (x :: xs ys) |
| 205 | + |
| 206 | +instance : Append (DList α) where |
| 207 | + append xs ys := xs ∘ ys |
| 208 | +``` |
| 209 | + |
| 210 | +The proofs that appending two difference lists is associative and that the empty difference list is a left and right identity of the append operator succeed by reflexivity because definitional equality includes the β and η laws for functions: |
| 211 | +```lean |
| 212 | +instance : Std.Associative (α := DList α) (· ++ ·) where |
| 213 | + assoc xs ys zs := by rfl |
| 214 | + |
| 215 | +instance : Std.LawfulIdentity (α := DList α) (· ++ ·) .empty where |
| 216 | + left_id xs := by rfl |
| 217 | + right_id xs := by rfl |
| 218 | +``` |
| 219 | + |
| 220 | +Given these instances, {tactic}`grind` can dispatch many proofs. |
| 221 | +```lean |
| 222 | +variable (a b c d x y : DList Nat) |
| 223 | +``` |
| 224 | + |
| 225 | +Because append is associative, terms can be rebracketed and units vanish: |
| 226 | +```lean |
| 227 | +example : ((a ++ b) ++ c) ++ d = a ++ (b ++ (c ++ d)) := by grind |
| 228 | + |
| 229 | +example : (a ++ .empty) ++ (.empty ++ b) = a ++ b := by grind |
| 230 | + |
| 231 | +example : (.empty ++ .empty : DList Nat) = .empty := by grind |
| 232 | +``` |
| 233 | + |
| 234 | +While {ref "simp-rewrites"}[simplification lemmas] could easily solve those problems, {tactic}`grind`'s `ac` solver allows a greater degree of flexibility. |
| 235 | +To use the hypotheses in these examples, it would not be sufficient to just rewrite goals and hypotheses to a normal form that reassociated an associative operator to the right: |
| 236 | +```lean |
| 237 | +example (h : a ++ b = x) : a ++ (b ++ c) = x ++ c := by grind |
| 238 | + |
| 239 | +example (h : a ++ b = x) : |
| 240 | + (a ++ .empty) ++ (b ++ c) = x ++ c := by grind |
| 241 | + |
| 242 | +example (h₁ : a ++ b = x) (h₂ : x ++ c = y) : |
| 243 | + a ++ (b ++ c) = y := by grind |
| 244 | +``` |
| 245 | +Because appending difference lists is not commutative, order still matters: |
| 246 | +```lean +error (name := grindErr) |
| 247 | +example : a ++ b = b ++ a := by grind |
| 248 | +``` |
| 249 | +```leanOutput grindErr |
| 250 | +`grind` failed |
| 251 | +case grind |
| 252 | +a b c d x y : DList Nat |
| 253 | +h : ¬a ++ b = b ++ a |
| 254 | +⊢ False |
| 255 | +[grind] Goal diagnostics |
| 256 | + [facts] Asserted facts |
| 257 | + [eqc] False propositions |
| 258 | + [assoc] Operator `HAppend.hAppend` |
| 259 | +``` |
| 260 | +:::: |
| 261 | + |
| 262 | +# Exclusions |
| 263 | + |
| 264 | +The `ac` solver does not apply to some built-in operators, namely {name}`And`, {name}`Or`, and {name}`Iff`. |
| 265 | +They are better served by other {tactic}`grind` features. |
| 266 | +When the operand type has {name}`Lean.Grind.CommRing` and/or {name}`Lean.Grind.CommSemiring` instances, `ac` omits its rules for certain operators that are usually better solved by {ref "grind-ring"}[the `ring` solver], namely `+`, `-`, `*`, `/`, and `^`. |
| 267 | + |
| 268 | +```lean -show |
| 269 | +-- `And` and `Or` have `Std.Associative` instances, so the exclusion is |
| 270 | +-- what stops the `ac` solver from claiming them. |
| 271 | +#guard_msgs in |
| 272 | +set_option trace.grind.debug.ac.op true in |
| 273 | +example (a b c : Prop) : (a ∧ b) ∧ c ↔ a ∧ (b ∧ c) := by grind |
| 274 | + |
| 275 | +#guard_msgs in |
| 276 | +set_option trace.grind.debug.ac.op true in |
| 277 | +example (a b c : Prop) : (a ∨ b) ∨ c ↔ a ∨ (b ∨ c) := by grind |
| 278 | + |
| 279 | +-- Likewise `+` and `*`, which are left to the arithmetic solvers. |
| 280 | +#guard_msgs in |
| 281 | +set_option trace.grind.debug.ac.op true in |
| 282 | +example (a b c : Nat) : a + (b + c) = (c + a) + b := by grind |
| 283 | + |
| 284 | +#guard_msgs in |
| 285 | +set_option trace.grind.debug.ac.op true in |
| 286 | +example (a b c : Nat) : a * (b * c) = (c * a) * b := by grind |
| 287 | +``` |
0 commit comments