The Lean Language Reference

16.15. Homomorphism Annotations🔗

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.

As a last resort, fallback rules can be provided that are applied after other rules have failed to rewrite a term. These are typically used for rules that would otherwise overlap others.

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).

Homomorphism rules are declared using three attributes: grind hom, grind hom fallback, and grind hom_pred. These attributes respectively register homomorphism rules, fallback rules to be tried when the other grind hom rules don't apply, and facts about the range of the mapping.

attributeHomomorphism Rules
attr ::= ...
    | Marks a theorem or definition for use by the `grind` tactic.

An optional modifier (e.g. `=`, `→`, `←`, `cases`, `intro`, `ext`, `inj`, etc.)
controls how `grind` uses the declaration:
* whether it is applied forwards, backwards, or both,
* whether equalities are used on the left, right, or both sides,
* whether case-splits, constructors, extensionality, or injectivity are applied,
* or whether custom instantiation patterns are used.

See the individual modifier docstrings for details.grind The `hom` modifier marks a theorem as a homomorphism rule for `grind`.

Homomorphism rules translate terms from a source domain into a target domain that has a
dedicated solver. A collection of homomorphism rules encodes an algebra homomorphism
`h : A → B`: each rule states how `h` commutes with a source-domain operation, as in
`h (f x y) = g (h x) (h y)`. Example: injecting bitvector operations into integer
arithmetic using `BitVec.toNat`:
```
@[grind hom] theorem toNat_add (x y : BitVec w) :
    (x + y).toNat = (x.toNat + y.toNat) % 2^w
```
The rules must be unconditional equations (or `Iff`s). They are applied to fixpoint
outside the E-graph, and only the final result is internalized.hom

The hom modifier marks a theorem as a homomorphism rule for grind.

Homomorphism rules translate terms from a source domain into a target domain that has a dedicated solver. A collection of homomorphism rules encodes an algebra homomorphism h : A → B: each rule states how h commutes with a source-domain operation, as in h (f x y) = g (h x) (h y). Example: injecting bitvector operations into integer arithmetic using BitVec.toNat:

@[grind hom] theorem toNat_add (x y : BitVec w) :
    (x + y).toNat = (x.toNat + y.toNat) % 2^w

The rules must be unconditional equations (or Iffs). They are applied to fixpoint outside the E-graph, and only the final result is internalized.

attributeFallback Homomorphism Rules
attr ::= ...
    | Marks a theorem or definition for use by the `grind` tactic.

An optional modifier (e.g. `=`, `→`, `←`, `cases`, `intro`, `ext`, `inj`, etc.)
controls how `grind` uses the declaration:
* whether it is applied forwards, backwards, or both,
* whether equalities are used on the left, right, or both sides,
* whether case-splits, constructors, extensionality, or injectivity are applied,
* or whether custom instantiation patterns are used.

See the individual modifier docstrings for details.grind The `hom fallback` modifier marks a homomorphism rule that is tried only when no other
`[grind hom]` rule applies to the term. It is meant for rules whose left-hand side matches
every application of an injection, such as the bridge `x.toInt = x.toBitVec.toInt` between
the two images of `Int64`: as an ordinary rule it would take precedence over the direct
rules like `Int64.toInt_add`, since the rewriter applies the first matching rule.hom fallback

The hom fallback modifier marks a homomorphism rule that is tried only when no other [grind hom] rule applies to the term. It is meant for rules whose left-hand side matches every application of an injection, such as the bridge x.toInt = x.toBitVec.toInt between the two images of Int64: as an ordinary rule it would take precedence over the direct rules like Int64.toInt_add, since the rewriter applies the first matching rule.

attributeHomomorphism Predicates
attr ::= ...
    | Marks a theorem or definition for use by the `grind` tactic.

An optional modifier (e.g. `=`, `→`, `←`, `cases`, `intro`, `ext`, `inj`, etc.)
controls how `grind` uses the declaration:
* whether it is applied forwards, backwards, or both,
* whether equalities are used on the left, right, or both sides,
* whether case-splits, constructors, extensionality, or injectivity are applied,
* or whether custom instantiation patterns are used.

See the individual modifier docstrings for details.grind The `hom_pred` modifier marks a theorem as a homomorphism predicate for `grind`.

Homomorphism predicates are facts that `grind` instantiates eagerly for the terms it
internalizes. The conclusion of the theorem must contain an application `f a₁ … aₙ`
whose trailing arguments are exactly the theorem's explicit parameters; the head
symbol `f` becomes the trigger. Whenever `grind` internalizes a term with head `f`,
the theorem is instantiated with the term's trailing arguments, and the resulting
fact is asserted. Typical uses are range facts for injection functions, and
translations of relations into a target domain. Examples:
```
@[grind hom_pred] theorem BitVec.toNat_range (x : BitVec w) : x.toNat < 2^w
@[grind hom_pred] theorem UInt8.le_iff (a b : UInt8) : a ≤ b ↔ a.toBitVec ≤ b.toBitVec
```
The first theorem is triggered by terms of the form `BitVec.toNat x`, and the second
one by `a ≤ b` applications. `grind` uses the types of `a` and `b` to discard
irrelevant instantiations.hom_pred

The hom_pred modifier marks a theorem as a homomorphism predicate for grind.

Homomorphism predicates are facts that grind instantiates eagerly for the terms it internalizes. The conclusion of the theorem must contain an application f a₁ … aₙ whose trailing arguments are exactly the theorem's explicit parameters; the head symbol f becomes the trigger. Whenever grind internalizes a term with head f, the theorem is instantiated with the term's trailing arguments, and the resulting fact is asserted. Typical uses are range facts for injection functions, and translations of relations into a target domain. Examples:

@[grind hom_pred] theorem BitVec.toNat_range (x : BitVec w) : x.toNat < 2^w
@[grind hom_pred] theorem UInt8.le_iff (a b : UInt8) : a ≤ b ↔ a.toBitVec ≤ b.toBitVec

The first theorem is triggered by terms of the form BitVec.toNat x, and the second one by a ≤ b applications. grind uses the types of a and b to discard irrelevant instantiations.

When debugging, homomorphism rewrites can be observed by setting trace.grind.hom or trace.grind.hom.pred to true.

🔗option
trace.grind.hom

Default value: false

enable/disable tracing for the given module and submodules

🔗option
trace.grind.hom.pred

Default value: false

enable/disable tracing for the given module and submodules

Very Small Integers

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:

@[invalid `[grind hom]` theorem, `toNat_add_lt_4_eq_add` is conditional: hypothesis x.toNat + y.toNat < 4 is not determined by the left-hand side and would have to be discharged when the rule is applied. Homomorphism rules must be unconditional; use E-matching attributes such as `[grind =]` or `[grind →]` for conditional theoremsgrind 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! 🐙
invalid `[grind hom]` theorem, `toNat_add_lt_4_eq_add` is conditional: hypothesis
  x.toNat + y.toNat < 4
is not determined by the left-hand side and would have to be discharged when the rule is applied. Homomorphism rules must be unconditional; use E-matching attributes such as `[grind =]` or `[grind →]` for conditional theorems

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 `grind` failed h:¬2 + 1 = 3h_1:toNat 1 + toNat 2 ≤ 3⊢ False
[grind] Goal diagnostics
[grind] Diagnostics
  • [ematch] E-matching Diagnostics
    • [thm] Theorem Instance Count
      • [thm] Nat.min_def ↦ 1
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:

`grind` failed
h:¬2 + 1 = 3h_1:toNat 1 + toNat 2 ≤ 3⊢ False
[grind] Goal diagnostics
[grind] Diagnostics
  • [ematch] E-matching Diagnostics
    • [thm] Theorem Instance Count
      • [thm] Nat.min_def ↦ 1

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 [grind.hom] ¬2 + 1 = 3 ===> ¬min (toNat 2 + toNat 1) 3 = toNat 3[grind.hom.pred] toNat 1 < 4[grind.hom.pred] toNat 2 < 4[grind.hom.pred] toNat 3 < 4`grind` failed h:¬2 + 1 = 3h_1:toNat 1 + toNat 2 ≤ 3⊢ False
[grind] Goal diagnostics
[grind] Diagnostics
  • [ematch] E-matching Diagnostics
    • [thm] Theorem Instance Count
      • [thm] Nat.min_def ↦ 1
All goals completed! 🐙
[grind.hom] ¬2 + 1 = 3
    ===>
    ¬min (toNat 2 + toNat 1) 3 = toNat 3[grind.hom.pred] toNat 1 < 4[grind.hom.pred] toNat 2 < 4[grind.hom.pred] toNat 3 < 4

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 [grind.hom] ¬2 + 1 = 3 ===> ¬min 3 3 = 3All goals completed! 🐙
[grind.hom] ¬2 + 1 = 3
    ===>
    ¬min 3 3 = 3
Difference Lists

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 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 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 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 := α:Type u_1xs:DList αys:DList αh:xs.toList = ys.toList⊢ xs = ys α:Type u_1xs:DList αys:DList αh:xs.toList = ys.toListthis:xs.appendTail = ys.appendTail⊢ xs = ys α: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; α: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✝ }; All goals completed! 🐙 @[grind hom] theorem DList.toList_inj (xs ys : DList α) : xs = ys ↔ xs.toList = ys.toList := α:Type u_1xs:DList αys:DList α⊢ xs = ys ↔ xs.toList = ys.toList α:Type u_1xs:DList αys:DList α⊢ xs = ys → xs.toList = ys.toListα:Type u_1xs:DList αys:DList α⊢ xs.toList = ys.toList → xs = ys α:Type u_1xs:DList αys:DList α⊢ xs = ys → xs.toList = ys.toList α:Type u_1xs:DList αys:DList αh:xs = ys⊢ xs.toList = ys.toList; All goals completed! 🐙 α:Type u_1xs:DList αys:DList α⊢ xs.toList = ys.toList → xs = ys All goals completed! 🐙

The difference list operators can now be translated to list operators:

def DList.nil : DList α where appendTail xs := xs wf := α:Type ?u.3⊢ ∀ (xs : List α), xs = [] ++ xs All goals completed! 🐙 @[grind hom] theorem DList.nil_toList : (.nil : DList α).toList = [] := α:Type u_1⊢ nil.toList = [] All goals completed! 🐙 def DList.cons (x : α) (xs : DList α) : DList α where appendTail ys := x :: xs.appendTail ys wf ys := α:Type ?u.4x:αxs:DList αys:List α⊢ x :: xs.appendTail ys = x :: xs.appendTail [] ++ ys α:Type ?u.4x:αxs:DList αys:List α⊢ xs.appendTail ys = xs.appendTail [] ++ ys All goals completed! 🐙 @[grind hom] theorem DList.cons_toList : (DList.cons x xs).toList = x :: xs.toList := α✝:Type u_1x:α✝xs:DList α✝⊢ (cons x xs).toList = x :: xs.toList All goals completed! 🐙 instance : Append (DList α) where append xs ys := { appendTail := xs.appendTail ∘ ys.appendTail wf zs := α:Type ?u.4xs:DList αys:DList αzs:List α⊢ (xs.appendTail ∘ ys.appendTail) zs = (xs.appendTail ∘ ys.appendTail) [] ++ zs α: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 α: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 All goals completed! 🐙 } @[grind hom] theorem DList.append_toList {xs ys : DList α} : (xs ++ ys).toList = xs.toList ++ ys.toList := α:Type u_1xs:DList αys:DList α⊢ (xs ++ ys).toList = xs.toList ++ ys.toList 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) := a:DList Natb:DList Natc:DList Nat⊢ a ++ b ++ c = a ++ (b ++ c) All goals completed! 🐙 example : a ++ .nil = a := a:DList Natb:DList Natc:DList Nat⊢ a ++ DList.nil = a All goals completed! 🐙 example : .nil ++ a = a := a:DList Natb:DList Natc:DList Nat⊢ DList.nil ++ a = a All goals completed! 🐙 example (h₁ : a.toList = [1,2]) (h₂ : b.toList = [3]) : (a ++ b).toList = [1,2,3] := a:DList Natb:DList Natc:DList Nath₁:a.toList = [1, 2]h₂:b.toList = [3]⊢ (a ++ b).toList = [1, 2, 3] All goals completed! 🐙 example (h : a.toList = []) : a = .nil := a:DList Natb:DList Natc:DList Nath:a.toList = []⊢ a = DList.nil 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 := a:DList Natb:DList Natc:DList Nath:a ++ b = a ++ c⊢ b = c All goals completed! 🐙 example (h : a ++ c = b ++ c) : a = b := a:DList Natb:DList Natc:DList Nath:a ++ c = b ++ c⊢ a = b All goals completed! 🐙 example (h : a ++ b = .nil) : a = .nil := a:DList Natb:DList Natc:DList Nath:a ++ b = DList.nil⊢ a = DList.nil All goals completed! 🐙
Times of Day

A time can be represented by hours and minutes:

structure Time where hour : Fin 24 minute : Fin 60 namespace Time

Each time can be represented as the number of minutes since midnight. This representation is not unique, because more than a day's worth of minutes wrap around to a time in the next day.

def toMinutes (t : Time) : Nat := t.hour.val * 60 + t.minute.val def ofMinutes (n : Nat) : Time where hour := ⟨n / 60 % 24, n:Nat⊢ n / 60 % 24 < 24 All goals completed! 🐙⟩ minute := ⟨n % 60, n:Nat⊢ n % 60 < 60 All goals completed! 🐙⟩

While it's not particularly sensible to add two points in time, it's perfectly reasonable to add minutes to a time, yielding a later time.

def addMinutes (t : Time) (n : Nat) : Time := ofMinutes (t.toMinutes + n)

One time is less than another if it occurs earlier in the day.

instance : LT Time where lt a b := a.toMinutes < b.toMinutes

Using grind hom, these operators can be mapped directly to the natural numbers.

@[grind hom] theorem eq_iff_toMinutes_eq (a b : Time) : a = b ↔ a.toMinutes = b.toMinutes := a:Timeb:Time⊢ a = b ↔ a.toMinutes = b.toMinutes a:Timeb:Time⊢ a = b → a.toMinutes = b.toMinutesa:Timeb:Time⊢ a.toMinutes = b.toMinutes → a = b a:Timeb:Time⊢ a = b → a.toMinutes = b.toMinutes a:Timeb:Timeh:a = b⊢ a.toMinutes = b.toMinutes; All goals completed! 🐙 a:Timeb:Time⊢ a.toMinutes = b.toMinutes → a = b b:Timeh₁:NatisLt✝¹:h₁ < 24m₁:NatisLt✝:m₁ < 60⊢ { hour := ⟨h₁, isLt✝¹⟩, minute := ⟨m₁, isLt✝⟩ }.toMinutes = b.toMinutes → { hour := ⟨h₁, isLt✝¹⟩, minute := ⟨m₁, isLt✝⟩ } = b h₁:NatisLt✝³:h₁ < 24m₁:NatisLt✝²:m₁ < 60h₂:NatisLt✝¹:h₂ < 24m₂:NatisLt✝:m₂ < 60⊢ { hour := ⟨h₁, isLt✝³⟩, minute := ⟨m₁, isLt✝²⟩ }.toMinutes = { hour := ⟨h₂, isLt✝¹⟩, minute := ⟨m₂, isLt✝⟩ }.toMinutes → { hour := ⟨h₁, isLt✝³⟩, minute := ⟨m₁, isLt✝²⟩ } = { hour := ⟨h₂, isLt✝¹⟩, minute := ⟨m₂, isLt✝⟩ } h₁:NatisLt✝³:h₁ < 24m₁:NatisLt✝²:m₁ < 60h₂:NatisLt✝¹:h₂ < 24m₂:NatisLt✝:m₂ < 60⊢ h₁ * 60 + m₁ = h₂ * 60 + m₂ → h₁ = h₂ ∧ m₁ = m₂ All goals completed! 🐙 @[grind hom] theorem lt_iff_toMinutes_lt (a b : Time) : a < b ↔ a.toMinutes < b.toMinutes := a:Timeb:Time⊢ a < b ↔ a.toMinutes < b.toMinutes All goals completed! 🐙 @[grind hom] theorem toMinutes_addMinutes (t : Time) (n : Nat) : (t.addMinutes n).toMinutes = (t.toMinutes + n) % 1440 := t:Timen:Nat⊢ (t.addMinutes n).toMinutes = (t.toMinutes + n) % 1440 t:Timen:Natthis:↑t.hour < 24⊢ (t.addMinutes n).toMinutes = (t.toMinutes + n) % 1440 t:Timen:Natthis✝:↑t.hour < 24this:↑t.minute < 60⊢ (t.addMinutes n).toMinutes = (t.toMinutes + n) % 1440 t:Timen:Natthis✝:↑t.hour < 24this:↑t.minute < 60⊢ (↑t.hour * 60 + ↑t.minute + n) / 60 % 24 * 60 + (↑t.hour * 60 + ↑t.minute + n) % 60 = (↑t.hour * 60 + ↑t.minute + n) % 1440 All goals completed! 🐙

However, there is no rule that relates facts about the fields of Time to minute counts. This leads to failures when using grind to reason about the fields:

example (a b : Time) (h : a.hour < b.hour) : a < b := a:Timeb:Timeh:a.hour < b.hour⊢ a < b `grind` failed a b:Timeh:a.hour < b.hourh_1:¬a < b⊢ False
[grind] Goal diagnostics
[grind] Diagnostics
  • [ematch] E-matching Diagnostics
    • [thm] Theorem Instance Count
All goals completed! 🐙

One way around this is to use the defining equation of toMinutes as a grind hom rule. Using this rule causes all applications of toMinutes to be rewritten in terms of the time's field values.

theorem toMinutes_eq (t : Time) : t.toMinutes = t.hour.val * 60 + t.minute.val := t:Time⊢ t.toMinutes = ↑t.hour * 60 + ↑t.minute All goals completed! 🐙 attribute [local grind hom] toMinutes_eq in example (a b : Time) (h : a.hour < b.hour) : a < b := a:Timeb:Timeh:a.hour < b.hour⊢ a < b All goals completed! 🐙

Unfortunately, this rule is applied in situations where one of the more specific rules would have been better. It's a useful fallback, but it is not the best choice when the other rules could have been used. In particular, it takes precedence over toMinutes_addMinutes, which causes this proof to fail:

attribute [local grind hom] toMinutes_eq in set_option trace.grind.hom true in example (t : Time) (h : t.hour < 23) : t < t.addMinutes 60 := t:Timeh:t.hour < 23⊢ t < t.addMinutes 60 [grind.hom.pred] ↑t.hour < 24[grind.hom] t.hour < 23 ===> ↑t.hour ≤ 22[grind.hom.pred] ↑t.minute < 60[grind.hom.pred] ↑(t.addMinutes 60).hour < 24[grind.hom.pred] ↑(t.addMinutes 60).minute < 60[grind.hom] t < t.addMinutes 60 ===> 60 * ↑t.hour + ↑t.minute + 1 ≤ 60 * ↑(t.addMinutes 60).hour + ↑(t.addMinutes 60).minute`grind` failed t:Timeh:t.hour < 23h_1:¬t < t.addMinutes 60h_2:↑t.hour = ↑(t.addMinutes 60).hour⊢ False
[grind] Goal diagnostics
[grind] Diagnostics
  • [ematch] E-matching Diagnostics
    • [thm] Theorem Instance Count
All goals completed! 🐙
[grind.hom.pred] ↑t.hour < 24[grind.hom] t.hour < 23
    ===>
    ↑t.hour ≤ 22[grind.hom.pred] ↑t.minute < 60[grind.hom.pred] ↑(t.addMinutes 60).hour < 24[grind.hom.pred] ↑(t.addMinutes 60).minute < 60[grind.hom] t < t.addMinutes 60
    ===>
    60 * ↑t.hour + ↑t.minute + 1 ≤ 60 * ↑(t.addMinutes 60).hour + ↑(t.addMinutes 60).minute

The solution is to add toMinutes_eq as a fallback rule:

attribute [grind hom fallback] toMinutes_eq set_option trace.grind.hom true in example (t : Time) (h : t.hour < 23) : t < t.addMinutes 60 := t:Timeh:t.hour < 23⊢ t < t.addMinutes 60 [grind.hom.pred] ↑t.hour < 24[grind.hom] t.hour < 23 ===> ↑t.hour ≤ 22[grind.hom.pred] ↑t.minute < 60[grind.hom] t < t.addMinutes 60 ===> 60 * ↑t.hour + ↑t.minute + 1 ≤ (60 * ↑t.hour + ↑t.minute + 60) % 1440All goals completed! 🐙
[grind.hom.pred] ↑t.hour < 24[grind.hom] t.hour < 23
    ===>
    ↑t.hour ≤ 22[grind.hom.pred] ↑t.minute < 60[grind.hom] t < t.addMinutes 60
    ===>
    60 * ↑t.hour + ↑t.minute + 1 ≤ (60 * ↑t.hour + ↑t.minute + 60) % 1440

With the fallback in place, all of these proofs succeed:

example (t : Time) : t.addMinutes 1440 = t := t:Time⊢ t.addMinutes 1440 = t All goals completed! 🐙 example (t : Time) (m n : Nat) : (t.addMinutes m).addMinutes n = t.addMinutes (m + n) := t:Timem:Natn:Nat⊢ (t.addMinutes m).addMinutes n = t.addMinutes (m + n) All goals completed! 🐙 example (t : Time) (h : t.hour = 23) (h' : t.minute = 59) : t.addMinutes 1 = ⟨0, 0⟩ := t:Timeh:t.hour = 23h':t.minute = 59⊢ t.addMinutes 1 = { hour := 0, minute := 0 } All goals completed! 🐙