The Lean Language Reference

16.14. 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, UInt8UInt64, USize, Int8Int64, 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.

🔗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:Tinyx = y x.toNat = y.toNat y:Tinyzero = y zero.toNat = y.toNaty:Tinyone = y one.toNat = y.toNaty:Tinytwo = y two.toNat = y.toNaty:Tinythree = y three.toNat = y.toNat y:Tinyzero = y zero.toNat = y.toNaty:Tinyone = y one.toNat = y.toNaty:Tinytwo = y two.toNat = y.toNaty:Tinythree = y three.toNat = y.toNat three = zero three.toNat = zero.toNatthree = one three.toNat = one.toNatthree = two three.toNat = two.toNatthree = three three.toNat = three.toNat zero = zero zero.toNat = zero.toNatzero = one zero.toNat = one.toNatzero = two zero.toNat = two.toNatzero = three zero.toNat = three.toNatone = zero one.toNat = zero.toNatone = one one.toNat = one.toNatone = two one.toNat = two.toNatone = three one.toNat = three.toNattwo = zero two.toNat = zero.toNattwo = one two.toNat = one.toNattwo = two two.toNat = two.toNattwo = three two.toNat = three.toNatthree = zero three.toNat = zero.toNatthree = one three.toNat = one.toNatthree = two three.toNat = two.toNatthree = 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:Tinyx 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:Tinyx < 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:Tinyx.toNat < 4 zero.toNat < 4one.toNat < 4two.toNat < 4three.toNat < 4 zero.toNat < 4one.toNat < 4two.toNat < 4three.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 3False
[grind] Goal diagnostics
[grind] Diagnostics
  • [ematch] E-matching Diagnostics
    • [thm] Theorem Instance Count
      • [thm] Nat.min_def1
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  3False
[grind] Goal diagnostics
[grind] Diagnostics
  • [ematch] E-matching Diagnostics
    • [thm] Theorem Instance Count
      • [thm] Nat.min_def1

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 3False
[grind] Goal diagnostics
[grind] Diagnostics
  • [ematch] E-matching Diagnostics
    • [thm] Theorem Instance Count
      • [thm] Nat.min_def1
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 (2 + 1) 3 = 3All goals completed! 🐙
[grind.hom] ¬2 + 1 = 3
    ===>
    ¬min (2 + 1) 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.toListFalse h: (α : Type) (xs ys : DList α), xs = ys xs.toList = ys.toListl1:DList Nat := fun xs => 1 :: 2 :: xsFalse 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 l2False 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.toListFalse 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 = l2False 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.toListxs = ys α:Type u_1xs:DList αys:DList αh:xs.toList = ys.toListthis:xs.appendTail = ys.appendTailxs = 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 = ysxs.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_1nil.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 Nata ++ b ++ c = a ++ (b ++ c) All goals completed! 🐙 example : a ++ .nil = a := a:DList Natb:DList Natc:DList Nata ++ DList.nil = a All goals completed! 🐙 example : .nil ++ a = a := a:DList Natb:DList Natc:DList NatDList.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 ++ cb = c All goals completed! 🐙 example (h : a ++ c = b ++ c) : a = b := a:DList Natb:DList Natc:DList Nath:a ++ c = b ++ ca = b All goals completed! 🐙 example (h : a ++ b = .nil) : a = .nil := a:DList Natb:DList Natc:DList Nath:a ++ b = DList.nila = DList.nil All goals completed! 🐙