Default value: false
enable/disable tracing for the given module and submodules
The grind tactic can be extended to a new domain by defining a mapping from the new domain to one for which there already exists a dedicated solver.
This extension consists of a homomorphism: a structure-preserving translation from the new domain to the existing domain.
In other words, grind can be made to reason about some new type T using an existing solver for U by defining a mapping T.toU from T to U that satisfies certain properties.
Lean includes homomorphism rewrite rules for Fin, BitVec, UInt8–UInt64, USize, Int8–Int64, and ISize that rewrite them to the lia solver's domain.
Additionally, the inequality relations and byte-index bounds of String.Pos are mapped, allowing grind to reason about string positions.
Homomorphism rewriting is controlled by the hom flag to grind, and it is enabled by default.
To use the feature at all, the mapping should be injective with respect to equality.
That is, given x and y of type T, it should be the case that x = y is logically equivalent to x.toU = y.toU.
Adding the grind hom attribute to a suitable injectivity theorem activates the mapping feature.
To be useful, the mapping should translate operations of interest on T into operations in U that are supported by grind's solvers.
The grind hom attribute can be added to the following kinds of theorems:
To translate f into g, it should be the case that (f x y).toU = g x.toU y.toU.
Ordering relations can be translated by showing that x ≤ y ↔ x.toU ≤ y.toU and x < y ↔ x.toU < y.toU.
Numeric literals can be translated by providing a theorem that translates them into a function into U.
This is done by adding the grind hom attribute to a theorem of the form (OfNat.ofNat n : T).toU = h n.
Conditionals can be translated by providing a theorem that shows that (if p then x else y).toU = if p then x.toU else y.toU.
Additional facts about the range of the mapping can be provided by tagging lemmas with grind hom_pred.
This is typically used to restrict the range, such as by asserting that the target of Fin.val is less than the Fin's bound.
These lemmas are instantiated when the constants that they mention are used in terms that are not themselves rewritten by grind hom rules.
Homomorphism lemmas are applied prior to adding statements to the shared “whiteboard,” rather than repeatedly while the solvers run.
This can be much more efficient.
Because the mapping is injective, disequality of terms in the new type implies disequality in the solver's domain, so grind's strategy of negating statements to derive a contradiction can be applied directly even without having a bijection.
Because they run only very early in the process, homomorphism lemmas are applied without a discharger.
This means that they do not permit conditional rewrites that require further proving (though rewrites can still be made conditional on an instance-implicit hypothesis, and propositional hypotheses are permitted when they are fully determined by the left-hand side).
When debugging, homomorphism rewrites can be observed by setting trace.grind.hom or trace.grind.hom.pred to true.
trace.grind.hom
Default value: false
enable/disable tracing for the given module and submodules
trace.grind.hom.pred
Default value: false
enable/disable tracing for the given module and submodules
Tiny numbers (that is, whole numbers from zero to three) can be represented using a four-constructor inductive type:
inductive Tiny where
| zero
| one
| two
| three
namespace Tiny
A few instances allow numeric literals to be used for Tiny:
instance : Zero Tiny where
zero := .zero
instance : One Tiny where
one := .one
instance : OfNat Tiny 2 where
ofNat := .two
instance : OfNat Tiny 3 where
ofNat := .three
They can be converted to and from Nat:
def toNat : Tiny → Nat
| .zero => 0
| .one => 1
| .two => 2
| .three => 3
def ofNat : (n : Nat) → n < 4 → Tiny
| 0, _ => 0
| 1, _ => 1
| 2, _ => 2
| 3, _ => 3
And they can be compared with each other:
instance : LE Tiny where
le x y := x.toNat ≤ y.toNat
instance : LT Tiny where
lt x y := x.toNat < y.toNat
Tiny.toNat is an injective mapping:
@[grind hom]
theorem eq_iff_toNat_eq (x y : Tiny) : x = y ↔ x.toNat = y.toNat := x:Tinyy:Tiny⊢ x = y ↔ x.toNat = y.toNat
y:Tiny⊢ zero = y ↔ zero.toNat = y.toNaty:Tiny⊢ one = y ↔ one.toNat = y.toNaty:Tiny⊢ two = y ↔ two.toNat = y.toNaty:Tiny⊢ three = y ↔ three.toNat = y.toNat y:Tiny⊢ zero = y ↔ zero.toNat = y.toNaty:Tiny⊢ one = y ↔ one.toNat = y.toNaty:Tiny⊢ two = y ↔ two.toNat = y.toNaty:Tiny⊢ three = y ↔ three.toNat = y.toNat ⊢ three = zero ↔ three.toNat = zero.toNat⊢ three = one ↔ three.toNat = one.toNat⊢ three = two ↔ three.toNat = two.toNat⊢ three = three ↔ three.toNat = three.toNat ⊢ zero = zero ↔ zero.toNat = zero.toNat⊢ zero = one ↔ zero.toNat = one.toNat⊢ zero = two ↔ zero.toNat = two.toNat⊢ zero = three ↔ zero.toNat = three.toNat⊢ one = zero ↔ one.toNat = zero.toNat⊢ one = one ↔ one.toNat = one.toNat⊢ one = two ↔ one.toNat = two.toNat⊢ one = three ↔ one.toNat = three.toNat⊢ two = zero ↔ two.toNat = zero.toNat⊢ two = one ↔ two.toNat = one.toNat⊢ two = two ↔ two.toNat = two.toNat⊢ two = three ↔ two.toNat = three.toNat⊢ three = zero ↔ three.toNat = zero.toNat⊢ three = one ↔ three.toNat = one.toNat⊢ three = two ↔ three.toNat = two.toNat⊢ three = three ↔ three.toNat = three.toNat All goals completed! 🐙
Similarly, because the LE Tiny and LT Tiny instances are defined in terms of those for Nat, they are logically equivalent:
@[grind hom]
theorem le_iff_toNat_le (x y : Tiny) : x ≤ y ↔ x.toNat ≤ y.toNat := x:Tinyy:Tiny⊢ x ≤ y ↔ x.toNat ≤ y.toNat
All goals completed! 🐙
@[grind hom]
theorem lt_iff_toNat_lt (x y : Tiny) : x < y ↔ x.toNat < y.toNat := x:Tinyy:Tiny⊢ x < y ↔ x.toNat < y.toNat
All goals completed! 🐙
Whenever a Tiny number is converted to a Nat, the resulting number is less than four.
Adding a grind hom_pred attribute to the proof causes grind to include this knowledge when Tiny.toNat is used in a term that is added to the “whiteboard” but not rewritten by a grind hom rule:
@[grind hom_pred]
theorem toNat_lt_4 (x : Tiny) : x.toNat < 4 := x:Tiny⊢ x.toNat < 4
⊢ zero.toNat < 4⊢ one.toNat < 4⊢ two.toNat < 4⊢ three.toNat < 4 ⊢ zero.toNat < 4⊢ one.toNat < 4⊢ two.toNat < 4⊢ three.toNat < 4 All goals completed! 🐙
A finite type like Tiny must decide the meaning of operations that go outside its bounds.
In this case, Tiny.succ and the addition operator are defined such that they truncate; modular arithmetic would be another valid implementation.
def succ (x : Tiny) : Tiny :=
match x with
| .zero => .one
| .one => .two
| .two => .three
| .three => .three
instance : Add Tiny where
add
| .zero, y => y
| .one, y => y.succ
| .two, y => y.succ.succ
| .three, y => y.succ.succ.succ
Natural numbers do not exhibit truncating addition, so it seems natural to require that the result of the addition does not truncate prior to mapping it to natural number addition. However, this results in an error:
@[grind hom]
theorem toNat_add_lt_4_eq_add (x y : Tiny)
(h : x.toNat + y.toNat < 4) :
(x + y).toNat = x.toNat + y.toNat := x:Tinyy:Tinyh:x.toNat + y.toNat < 4⊢ (x + y).toNat = x.toNat + y.toNat
y:Tinyh:zero.toNat + y.toNat < 4⊢ (zero + y).toNat = zero.toNat + y.toNaty:Tinyh:one.toNat + y.toNat < 4⊢ (one + y).toNat = one.toNat + y.toNaty:Tinyh:two.toNat + y.toNat < 4⊢ (two + y).toNat = two.toNat + y.toNaty:Tinyh:three.toNat + y.toNat < 4⊢ (three + y).toNat = three.toNat + y.toNat y:Tinyh:zero.toNat + y.toNat < 4⊢ (zero + y).toNat = zero.toNat + y.toNaty:Tinyh:one.toNat + y.toNat < 4⊢ (one + y).toNat = one.toNat + y.toNaty:Tinyh:two.toNat + y.toNat < 4⊢ (two + y).toNat = two.toNat + y.toNaty:Tinyh:three.toNat + y.toNat < 4⊢ (three + y).toNat = three.toNat + y.toNat h:three.toNat + zero.toNat < 4⊢ (three + zero).toNat = three.toNat + zero.toNath:three.toNat + one.toNat < 4⊢ (three + one).toNat = three.toNat + one.toNath:three.toNat + two.toNat < 4⊢ (three + two).toNat = three.toNat + two.toNath:three.toNat + three.toNat < 4⊢ (three + three).toNat = three.toNat + three.toNat h:zero.toNat + zero.toNat < 4⊢ (zero + zero).toNat = zero.toNat + zero.toNath:zero.toNat + one.toNat < 4⊢ (zero + one).toNat = zero.toNat + one.toNath:zero.toNat + two.toNat < 4⊢ (zero + two).toNat = zero.toNat + two.toNath:zero.toNat + three.toNat < 4⊢ (zero + three).toNat = zero.toNat + three.toNath:one.toNat + zero.toNat < 4⊢ (one + zero).toNat = one.toNat + zero.toNath:one.toNat + one.toNat < 4⊢ (one + one).toNat = one.toNat + one.toNath:one.toNat + two.toNat < 4⊢ (one + two).toNat = one.toNat + two.toNath:one.toNat + three.toNat < 4⊢ (one + three).toNat = one.toNat + three.toNath:two.toNat + zero.toNat < 4⊢ (two + zero).toNat = two.toNat + zero.toNath:two.toNat + one.toNat < 4⊢ (two + one).toNat = two.toNat + one.toNath:two.toNat + two.toNat < 4⊢ (two + two).toNat = two.toNat + two.toNath:two.toNat + three.toNat < 4⊢ (two + three).toNat = two.toNat + three.toNath:three.toNat + zero.toNat < 4⊢ (three + zero).toNat = three.toNat + zero.toNath:three.toNat + one.toNat < 4⊢ (three + one).toNat = three.toNat + one.toNath:three.toNat + two.toNat < 4⊢ (three + two).toNat = three.toNat + two.toNath:three.toNat + three.toNat < 4⊢ (three + three).toNat = three.toNat + three.toNat All goals completed! 🐙 h:True⊢ (zero + zero).toNat = zero.toNat + zero.toNath:True⊢ (zero + one).toNat = zero.toNat + one.toNath:True⊢ (zero + two).toNat = zero.toNat + two.toNath:True⊢ (zero + three).toNat = zero.toNat + three.toNath:True⊢ (one + zero).toNat = one.toNat + zero.toNath:True⊢ (one + one).toNat = one.toNat + one.toNath:True⊢ (one + two).toNat = one.toNat + two.toNath:True⊢ (two + zero).toNat = two.toNat + zero.toNath:True⊢ (two + one).toNat = two.toNat + one.toNath:True⊢ (three + zero).toNat = three.toNat + zero.toNat All goals completed! 🐙
This is because conditional rewrites are disallowed; the homomorphism feature is used too early in grind to support them.
Instead, addition of tiny numbers can be mapped to truncating addition of natural numbers:
@[grind hom]
theorem toNat_add_eq_add (x y : Tiny) :
(x + y).toNat = min (x.toNat + y.toNat) 3 := x:Tinyy:Tiny⊢ (x + y).toNat = min (x.toNat + y.toNat) 3
y:Tiny⊢ (zero + y).toNat = min (zero.toNat + y.toNat) 3y:Tiny⊢ (one + y).toNat = min (one.toNat + y.toNat) 3y:Tiny⊢ (two + y).toNat = min (two.toNat + y.toNat) 3y:Tiny⊢ (three + y).toNat = min (three.toNat + y.toNat) 3 y:Tiny⊢ (zero + y).toNat = min (zero.toNat + y.toNat) 3y:Tiny⊢ (one + y).toNat = min (one.toNat + y.toNat) 3y:Tiny⊢ (two + y).toNat = min (two.toNat + y.toNat) 3y:Tiny⊢ (three + y).toNat = min (three.toNat + y.toNat) 3 ⊢ (three + zero).toNat = min (three.toNat + zero.toNat) 3⊢ (three + one).toNat = min (three.toNat + one.toNat) 3⊢ (three + two).toNat = min (three.toNat + two.toNat) 3⊢ (three + three).toNat = min (three.toNat + three.toNat) 3 ⊢ (zero + zero).toNat = min (zero.toNat + zero.toNat) 3⊢ (zero + one).toNat = min (zero.toNat + one.toNat) 3⊢ (zero + two).toNat = min (zero.toNat + two.toNat) 3⊢ (zero + three).toNat = min (zero.toNat + three.toNat) 3⊢ (one + zero).toNat = min (one.toNat + zero.toNat) 3⊢ (one + one).toNat = min (one.toNat + one.toNat) 3⊢ (one + two).toNat = min (one.toNat + two.toNat) 3⊢ (one + three).toNat = min (one.toNat + three.toNat) 3⊢ (two + zero).toNat = min (two.toNat + zero.toNat) 3⊢ (two + one).toNat = min (two.toNat + one.toNat) 3⊢ (two + two).toNat = min (two.toNat + two.toNat) 3⊢ (two + three).toNat = min (two.toNat + three.toNat) 3⊢ (three + zero).toNat = min (three.toNat + zero.toNat) 3⊢ (three + one).toNat = min (three.toNat + one.toNat) 3⊢ (three + two).toNat = min (three.toNat + two.toNat) 3⊢ (three + three).toNat = min (three.toNat + three.toNat) 3 All goals completed! 🐙
Given these definitions, we might expect the following example to succeed, but it does not:
example : (2 : Tiny) + (1 : Tiny) = (3 : Tiny) := ⊢ 2 + 1 = 3 All goals completed! 🐙
Examining grind's output, the problem is that it does not successfully register the contradiction that arises from the fact that (2 : Tiny) + (1 : Tiny) = (3 : Tiny) is negated.
This is because the rewriting process does not rewrite literals like 2 (that is, OfNat.ofNat (α := Tiny) 2), which are left alone:
Setting trace.grind.hom to true demonstrates the rewriting performed by the homomorphism lemmas, as well as the added grind hom_pred facts:
set_option trace.grind.hom true in
example : (2 : Tiny) + (1 : Tiny) = (3 : Tiny) := ⊢ 2 + 1 = 3 All goals completed! 🐙
This can be fixed by providing rules for each supported literal:
@[grind hom]
theorem toNat_zero_eq_zero : (0 : Tiny).toNat = 0 := ⊢ toNat 0 = 0 All goals completed! 🐙
@[grind hom]
theorem toNat_one_eq_one : (1 : Tiny).toNat = 1 := ⊢ toNat 1 = 1 All goals completed! 🐙
@[grind hom]
theorem toNat_two_eq_two : (2 : Tiny).toNat = 2 := ⊢ toNat 2 = 2 All goals completed! 🐙
@[grind hom]
theorem toNat_three_eq_three : (3 : Tiny).toNat = 3 := ⊢ toNat 3 = 3 All goals completed! 🐙
After this, the proof is successful, as is one that exercises the truncation behavior of addition:
example : (2 : Tiny) + (1 : Tiny) = (3 : Tiny) := ⊢ 2 + 1 = 3 All goals completed! 🐙
example : (3 : Tiny) + (3 : Tiny) = (3 : Tiny) := ⊢ 3 + 3 = 3 All goals completed! 🐙
Enabling trace.grind.hom reveals that the grind hom_pred rule no longer fires, because all subterms of type Tiny are now rewritten:
set_option trace.grind.hom true in
example : (2 : Tiny) + (1 : Tiny) = (3 : Tiny) := ⊢ 2 + 1 = 3 All goals completed! 🐙
Difference lists, in which lists are represented as functions, provide an associative append operator.
Reasoning about them as lists using grind hom is very appealing; however, the default representation as a function doesn't have the right injectivity property:
namespace NotInj
def DList α := List α → List α
def DList.toList (xs : DList α) : List α := xs []
theorem DList.not_toList_inj :
(∀ (α : Type) (xs ys : DList α), xs = ys ↔ xs.toList = ys.toList) →
False := ⊢ (∀ (α : Type) (xs ys : DList α), xs = ys ↔ xs.toList = ys.toList) → False
h:∀ (α : Type) (xs ys : DList α), xs = ys ↔ xs.toList = ys.toList⊢ False
h:∀ (α : Type) (xs ys : DList α), xs = ys ↔ xs.toList = ys.toListl1:DList Nat := fun xs => 1 :: 2 :: xs⊢ False
h:∀ (α : Type) (xs ys : DList α), xs = ys ↔ xs.toList = ys.toListl1:DList Nat := fun xs => 1 :: 2 :: xsl2:DList Nat := fun x => [1, 2]⊢ False
h:∀ (α : Type) (xs ys : DList α), xs = ys ↔ xs.toList = ys.toListl1:DList Nat := fun xs => 1 :: 2 :: xsl2:DList Nat := fun x => [1, 2]this:l1 ≠ l2⊢ False
have toList_eq : l1.toList = l2.toList := rfl h:∀ (α : Type) (xs ys : DList α), xs = ys ↔ xs.toList = ys.toListl1:DList Nat := fun xs => 1 :: 2 :: xsl2:DList Nat := fun x => [1, 2]this:l1 ≠ l2toList_eq:l1.toList = l2.toList⊢ False
have := h Nat l1 l2 |>.mpr toList_eq h:∀ (α : Type) (xs ys : DList α), xs = ys ↔ xs.toList = ys.toListl1:DList Nat := fun xs => 1 :: 2 :: xsl2:DList Nat := fun x => [1, 2]this✝:l1 ≠ l2toList_eq:l1.toList = l2.toListthis:l1 = l2⊢ False
contradiction All goals completed! 🐙
end NotInj
In other words, Lean's function type includes functions that aren't really difference lists.
To use grind hom with difference lists, they need more structure to rule out these counterexamples:
@[ext]
structure DList α where
appendTail : List α → List α
wf : ∀ xs, appendTail xs = appendTail [] ++ xs
def DList.toList (xs : DList α) : List α :=
xs.appendTail []
This additional well-formedness property rules out the invalid cases:
theorem DList.ext_toList (xs ys : DList α) (h : xs.toList = ys.toList) :
xs = ys := by α:Type u_1xs:DList αys:DList αh:xs.toList = ys.toList⊢ xs = ys
have : xs.appendTail = ys.appendTail := by
funext zs α:Type u_1xs:DList αys:DList αh:xs.toList = ys.toListzs:List α⊢ xs.appendTail zs = ys.appendTail zs
rw [xs.wf, α:Type u_1xs:DList αys:DList αh:xs.toList = ys.toListzs:List α⊢ xs.appendTail [] ++ zs = ys.appendTail zs ys.wf α:Type u_1xs:DList αys:DList αh:xs.toList = ys.toListzs:List α⊢ xs.appendTail [] ++ zs = ys.appendTail [] ++ zs] α:Type u_1xs:DList αys:DList αh:xs.toList = ys.toListzs:List α⊢ xs.appendTail [] ++ zs = ys.appendTail [] ++ zs
exact congrArg (· ++ zs) h α:Type u_1xs:DList αys:DList αh:xs.toList = ys.toListthis:xs.appendTail = ys.appendTail⊢ xs = ys
cases xs mk α:Type u_1ys:DList αappendTail✝:List α → List αwf✝:∀ (xs : List α), appendTail✝ xs = appendTail✝ [] ++ xsh:{ appendTail := appendTail✝, wf := wf✝ }.toList = ys.toListthis:{ appendTail := appendTail✝, wf := wf✝ }.appendTail = ys.appendTail⊢ { appendTail := appendTail✝, wf := wf✝ } = ys; cases ys mk.mk α:Type u_1appendTail✝¹:List α → List αwf✝¹:∀ (xs : List α), appendTail✝ xs = appendTail✝ [] ++ xsappendTail✝:List α → List αwf✝:∀ (xs : List α), appendTail✝ xs = appendTail✝ [] ++ xsh:{ appendTail := appendTail✝¹, wf := wf✝¹ }.toList = { appendTail := appendTail✝, wf := wf✝ }.toListthis:{ appendTail := appendTail✝¹, wf := wf✝¹ }.appendTail = { appendTail := appendTail✝, wf := wf✝ }.appendTail⊢ { appendTail := appendTail✝¹, wf := wf✝¹ } = { appendTail := appendTail✝, wf := wf✝ }; simp_all All goals completed! 🐙
@[grind hom]
theorem DList.toList_inj (xs ys : DList α) :
xs = ys ↔ xs.toList = ys.toList := by α:Type u_1xs:DList αys:DList α⊢ xs = ys ↔ xs.toList = ys.toList
constructor mp α:Type u_1xs:DList αys:DList α⊢ xs = ys → xs.toList = ys.toListmpr α:Type u_1xs:DList αys:DList α⊢ xs.toList = ys.toList → xs = ys
. mp α:Type u_1xs:DList αys:DList α⊢ xs = ys → xs.toList = ys.toList intro h mp α:Type u_1xs:DList αys:DList αh:xs = ys⊢ xs.toList = ys.toList; rw [h mp α:Type u_1xs:DList αys:DList αh:xs = ys⊢ ys.toList = ys.toList] All goals completed! 🐙
. mpr α:Type u_1xs:DList αys:DList α⊢ xs.toList = ys.toList → xs = ys apply DList.ext_toList All goals completed! 🐙
The difference list operators can now be translated to list operators:
def DList.nil : DList α where
appendTail xs := xs
wf := by α:Type ?u.3⊢ ∀ (xs : List α), xs = [] ++ xs simp All goals completed! 🐙
@[grind hom]
theorem DList.nil_toList : (.nil : DList α).toList = [] := by α:Type u_1⊢ nil.toList = []
simp_all [nil, toList] All goals completed! 🐙
def DList.cons (x : α) (xs : DList α) : DList α where
appendTail ys := x :: xs.appendTail ys
wf ys := by α:Type ?u.4x:αxs:DList αys:List α⊢ x :: xs.appendTail ys = x :: xs.appendTail [] ++ ys
simp only [List.cons_append, List.cons.injEq, true_and] α:Type ?u.4x:αxs:DList αys:List α⊢ xs.appendTail ys = xs.appendTail [] ++ ys
apply xs.wf All goals completed! 🐙
@[grind hom]
theorem DList.cons_toList :
(DList.cons x xs).toList = x :: xs.toList := by α✝:Type u_1x:α✝xs:DList α✝⊢ (cons x xs).toList = x :: xs.toList
simp [cons, toList] All goals completed! 🐙
instance : Append (DList α) where
append xs ys := {
appendTail := xs.appendTail ∘ ys.appendTail
wf zs := by α:Type ?u.4xs:DList αys:DList αzs:List α⊢ (xs.appendTail ∘ ys.appendTail) zs = (xs.appendTail ∘ ys.appendTail) [] ++ zs
have := xs.wf α:Type ?u.4xs:DList αys:DList αzs:List αthis:∀ (xs_1 : List α), xs.appendTail xs_1 = xs.appendTail [] ++ xs_1⊢ (xs.appendTail ∘ ys.appendTail) zs = (xs.appendTail ∘ ys.appendTail) [] ++ zs
have := ys.wf α:Type ?u.4xs:DList αys:DList αzs:List αthis✝:∀ (xs_1 : List α), xs.appendTail xs_1 = xs.appendTail [] ++ xs_1this:∀ (xs : List α), ys.appendTail xs = ys.appendTail [] ++ xs⊢ (xs.appendTail ∘ ys.appendTail) zs = (xs.appendTail ∘ ys.appendTail) [] ++ zs
grind All goals completed! 🐙
}
@[grind hom]
theorem DList.append_toList {xs ys : DList α} :
(xs ++ ys).toList = xs.toList ++ ys.toList := by α:Type u_1xs:DList αys:DList α⊢ (xs ++ ys).toList = xs.toList ++ ys.toList
apply xs.wf All goals completed! 🐙
Now, grind can reason about properties of difference lists, as well as conversions between difference lists and ordinary lists:
variable (a b c : DList Nat)
example : (a ++ b) ++ c = a ++ (b ++ c) := by a:DList Natb:DList Natc:DList Nat⊢ a ++ b ++ c = a ++ (b ++ c) grind All goals completed! 🐙
example : a ++ .nil = a := by a:DList Natb:DList Natc:DList Nat⊢ a ++ DList.nil = a grind All goals completed! 🐙
example : .nil ++ a = a := by a:DList Natb:DList Natc:DList Nat⊢ DList.nil ++ a = a grind All goals completed! 🐙
example (h₁ : a.toList = [1,2]) (h₂ : b.toList = [3]) :
(a ++ b).toList = [1,2,3] := by a:DList Natb:DList Natc:DList Nath₁:a.toList = [1, 2]h₂:b.toList = [3]⊢ (a ++ b).toList = [1, 2, 3] grind All goals completed! 🐙
example (h : a.toList = []) : a = .nil := by a:DList Natb:DList Natc:DList Nath:a.toList = []⊢ a = DList.nil grind All goals completed! 🐙
Even more powerfully, additional List lemmas can be used explicitly to reason about difference lists:
example (h : a ++ b = a ++ c) : b = c := by a:DList Natb:DList Natc:DList Nath:a ++ b = a ++ c⊢ b = c
grind [List.append_cancel_left] All goals completed! 🐙
example (h : a ++ c = b ++ c) : a = b := by a:DList Natb:DList Natc:DList Nath:a ++ c = b ++ c⊢ a = b
grind [List.append_cancel_right] All goals completed! 🐙
example (h : a ++ b = .nil) : a = .nil := by a:DList Natb:DList Natc:DList Nath:a ++ b = DList.nil⊢ a = DList.nil
grind [List.append_eq_nil_iff] All goals completed! 🐙