Many popular type-theoretic foundations for proof assistants, including the Calculus of Inductive Constructions, do not have native subtypes. Even for numeric types like , , and with a natural chain of inclusions, terms must be cast from one to another with an explicit function application. The numeral 5 : ℕ is syntactically different from 5 : ℤ and 5 : ℝ. To construct the sum of variables n : ℕ and z : ℤ, one needs either an unwieldy sum operator with type ℕ → ℤ → ℤ or a way to lift n to the larger type ℤ.
Inserting coercions is a common programming language feature, and proof assistants are no exception: many modern systems will interpret n + z in a reasonable way. Combined with type-polymorphic operations and relations like and and generic numeral expressions, subtyping concerns can largely be ignored at the input level. However, the ease of input often belies the complexity of the underlying term. Using such terms in practice can go against the grain of intuition, especially for users coming from mathematics, where one almost never makes such distinctions. It is frustrating to realize that work must be done to unify n < (5 : ℕ) with ↑n < (5 : ℤ), where ↑n denotes the cast of n into ℤ.
A more intricate example of this frustration appears in the Lean development of the -adic numbers  while proving
where padic_norm : ℕ → ℚ → ℚ. Straightforward manipulation reduces the proof to three goals: prime p ⊢ (1 : ℤ) ≤ ↑p, z ≠ (0 : ℤ) ⊢ -padic_val_rat p ↑z ≤ (0 : ℤ), and z ≠ (0 : ℤ) ⊢ ↑z ≠ (0 : ℚ). To solve these goals by hand, the user must combine knowledge of library lemmas with lemmas that manipulate casts. The latter obscure the main ideas of the proof:
We introduce a family of tactics implemented in the Lean proof assistant  that aim to remove these frustrations. The core tool, norm_cast, tries to rewrite an expression containing casts to a normal form determined by a configurable collection of rewrite rules. Variants allow the user to apply lemmas and hypotheses and rewrite the goal “modulo” the presence of casts. The tool was developed to address usability issues raised while formalizing mathematical results in Lean111Lean users previously wrote a guide to managing casts by hand, archived with our supplementary materials. ; it is incorporated into Lean’s mathematical library mathlib , where it is already invoked 221 times, and is also used heavily in external libraries . The tool is extensible: adapting it to new theories with new coercions simply requires tagging certain library lemmas.
Using our tool, the script above focuses on the relevant library lemmas:
We provide a website222https://lean-forward.github.io/norm_cast which points to our code in the mathlib repository, along with examples of norm_cast in use.
“Coercion” and “cast” are sometimes used interchangeably in the literature, and “cast” can also refer to the transport of a term t : A to the type B along a type equality A = B. In this description, we take a cast ↑ : A → B to be simply a function; it typically preserves structure, and is often injective, but neither is required. Casting a term t : A to B refers to applying the (often canonical) cast. A coercion is a cast that is automatically inserted by the elaborator. We do not consider casts along type equalities.
2 Lean Specifics
While the approach we describe can be adapted to other proof assistants, some details are specific to Lean. Here we describe some relevant features.
where the arguments a and b are implicit and the anonymous has_lift_t argument is inferred by type class resolution. An instance of the type class has_lift_t a b witnesses a transitive chain of coercions from a to b, avoiding loops caused by reflexive instances. When a function application fails to typecheck, the elaborator will insert applications of coe and try to resolve the resulting has_lift_t goal. Coercions are typically inserted at the leaf nodes of an expression. Users can also manually insert casts by using coe directly, with prefix notation ↑.
Type-polymorphic operators and relations like + and < are also implemented with type classes. Numerals build on top of these. A numeral is represented in binary by nested applications of the following terms:
Any type α that instantiates the classes has_zero α, has_one α, and has_add α supports numeral notation, e.g. (5 : α). While in this description we explicitly write the types of all numerals, in practice they are typically inferred.
Lean’s powerful metaprogramming framework  allows us to implement our tool in the language of Lean itself and include it in mathlib. The framework provides access to unification, type class resolution, simplification, and more as atomic operations. Metaprograms can query and add to a Lean environment. Declarations in an environment can be tagged with parametrized attributes, and metaprograms are able to define new attributes, use them to tag declarations, and retrieve lists of tagged declarations.
3 Outline of the Normalization Procedure
The core routine in our procedure takes as input an expression and transforms the expression to one in which applications of the cast function ↑ are normalized. It returns a proof that the resulting expression is equal to the input. In the most common case, where the expression is a proposition, the proof of equality serves as a proof of logical equivalence.
As an example of the expected behavior, we consider normalizing the expression ↑m + ↑n < (10 : ℤ), where m, n : ℕ are cast to ℤ. The expected normal form is m + n < (10 : ℕ); recall that +, <, and 1 are polymorphic. Our tool should proceed as follows:
Replace the numeral on the right with the cast of a nat: ↑m + ↑n < ↑(10 : ℕ)
Factor ↑ to the outside of the left: ↑(m + n) < ↑(10 : ℕ)
Eliminate both casts to get an inequality over ℕ: m + n < (10 : ℕ)
Steps 2 and 3 are effectively just applications of Lean’s simplification API with certain rewrite lemmas. Step 1 has a slightly different flavor, but we will still be able to use the simplification API to implement this. Since the simplifier will handle cases of these patterns nested inside larger expressions, we can focus on the atomic situation.
Each of these steps must be justified by lemmas in the library, of course: they would not be sound for arbitrary types, operations, and relations. Users of our tool tag certain declarations with the attribute norm_cast, and our tool sorts these declarations into three categories.
move lemmas equate expressions with casts at the root to expressions with casts further toward the leaves, e.g. ↑(m + n) = ↑m + ↑n. By mathlib convention, such lemmas are stated with the root cast on the left of the equation; Step 2 uses them as right-to-left rewrite rules.
elim lemmas relate expressions with casts to expressions without casts, e.g. ↑a < ↑b ↔ a < b. Such lemmas are stated with the expression containing casts on the left of the relation; Step 3 uses them as left-to-right rewrite rules. These lemmas are not restricted to propositional equivalences: they can also be used to modify polymorphic operations, e.g. ∥↑a∥ = ∥a∥ for a real valued norm function defined on all normed spaces.
squash lemmas equate expressions with one or more casts at the root to expressions with fewer casts at the root, e.g. ↑(1 : ℕ) = (1 : ℤ) and ↑↑n = ↑n. Such lemmas are stated with the expression containing the larger number of casts on the left; Step 1 uses them alongside move lemmas to justify that (10 : ℤ) = ↑(10 : ℕ)
, and they are used in the heuristic splitting step described below.
To normalize expressions where casts come from a variety of sources, we must sometimes split casts into pieces. Suppose n : ℕ and z : ℤ, and consider the goal ↑n + ↑z = (2 : ℚ). (We call the pattern P (↑x) (↑y), where P is a binary function or relation taking two arguments of the same type and x and y are of different types, the heuristic splitting pattern.) We cannot rewrite the left hand side to ↑(n + z), since the addition would not be well typed; however, move and squash lemmas justify a transformation to ↑↑n + ↑z = ↑↑(2 : ℕ), where the inner casts go ℕ → ℤ and the outer ℤ → ℚ. We transform this to ↑(↑n + z) = ↑↑(2 : ℕ) and then ↑n + z = ↑(2 : ℕ); finally, squash lemmas reduce the right hand side to the native numeral (2 : ℤ).
The core normalization routine has type expr → tactic (expr × expr), taking in an expression and returning it in normal form with a proof that the output is equal to the input. Lean’s simplifier API provides methods for traversing and rewriting an expression from the leaf nodes outward (“bottom up”) and in reverse (“top down”); our routine consists of four successive simplifier passes.
Working top down, try to replace each numeral (num : α) with ↑(num : ℕ). Justify these replacements with move lemmas to move casts down through applications of the constants bit0 and bit1, and squash lemmas to change ↑(0 : ℕ) and ↑(1 : ℕ) to (0 : α) and (1 : α).
Working bottom up, move casts upward by rewriting with move lemmas and eliminate them when possible by rewriting with elim lemmas. If no rewrite rules apply to a subexpression that matches the heuristic splitting pattern, fire the splitting procedure described below.
Working top down, clean up any unused repeated casts that were inserted by the heuristic by rewriting with squash lemmas.
Working top down, restore numerals to their natively typed form as in Step 1. This is again justified by move and squash lemmas.
The splitting procedure fires on an expression of the form P (↑x) (↑y), where P is a binary function or relation, x : X and y : Y are both cast to type Z, and X and Y are not equal. The procedure tries to find a coercion from X to Y or vice versa; since coercions are expressed as type class instances, this amounts to calling type class resolution on a goal of the form has_lift_t X Y. Supposing the former coercion is found, the procedure tries to replace ↑x with ↑↑x, where the nested coercions go from X to Y to Z. This is justified using squash lemmas.
We use Lean’s user attribute API to define an attribute norm_cast. This attribute is applied by the user to a lemma at or after the time of declaration, and tags the lemma for use in the procedure. A norm_cast lemma has the form lhs = rhs or lhs ↔ rhs
, typically preceded by a sequence of quantifiers. In nearly all cases, the attribute handler can automatically classify a lemma aselim, move, or squash.
Head casts are applications of casts that appear at the root of the expression tree, as in ↑↑(x+y), and internal casts appear elsewhere. Let and denote the number of head casts and internal casts in e. Based on the number and positions of applications of casts, we classify a lemma as
if , , and .
if , , and .
if , , and .
This classification applies to both = and ↔ lemmas. While users can override the classification by providing a parameter to the attribute, this is done for only 11 out of 343 attributions in mathlib.
We provide a suite of tactics built around the core norm_cast functionality. These try to replicate the behavior of other Lean tactics “modulo casts,” so that users can use familiar idioms while ignoring the presence of casts.
The core tactic norm_cast simplifies the current goal. Alternatively, norm_cast at h simplifies the type of a hypothesis h. A variant exact_mod_cast t simplifies both the goal and the type of the expression t, and tries to use t to close the goal; apply_mod_cast t does similar, but allows arguments to t to generate new subgoals. To close the goal with a hypothesis in the local context, assumption_mod_cast will try exact_mod_cast on all plausible candidates. Finally, rw_mod_cast [l₁, …, lₙ] will use a list of lemmas to sequentially rewrite the goal, calling norm_cast in between rewrite steps. This generalizes the behavior of Lean’s standard rewrite tactic rw.
We also add move and squash lemmas into a custom simp lemma collection and define a tactic push_cast that simplifies with this collection; note that push_cast does not directly use the norm_cast method. Calling push_cast simplifies an expression in the opposite direction to norm_cast, meaning that casts get pushed toward the leaf nodes of expressions. This does not allow casts to be eliminated over relations, but can be useful in its own right.
The norm_cast test file333https://github.com/leanprover-community/mathlib/blob/master/test/norm_cast.lean in mathlib demonstrates the tool in action. As a first example, we walk through a test where the heuristic splitting procedure is needed:
Using exact_mod_cast h will simplify h to match the goal, which is already in normal form. After changing (5 : ℚ) to ↑(5 : ℕ), norm_cast will fail to fire any move or elim rewrites. It will notice that ↑n - ↑z matches the heuristic splitting pattern, and rewrite ↑n to ↑↑n, where the inner cast goes ℕ → ℤ and the outer goes ℤ → ℚ. A move rule will then match, rewriting the expression to ↑(↑n - z) < ↑(5 : ℕ). While both sides of the < are now casts to ℚ, the left comes from ℤ and the right from ℕ, so no elim rule will fire; instead, norm_cast will match the entire expression to the heuristic splitting pattern and rewrite the right side to ↑↑(5 : ℕ). It can then rewrite with an elim lemma ↑a < ↑b ↔ a < b to obtain ↑n - z < ↑(5 : ℕ), and finally normalize the numeral on the right to (5 : ℤ).
The norm_cast family of tactics is used throughout mathlib. It is particularly useful in the development of the -adic numbers and integers . The rationals are embedded in the -adics, and the definition of requires working with a natural number embedded in and ; furthermore, is a subtype of . This development makes 64 calls to tactics in the norm_cast family.
A lemma in the development of bounds the -adic norm of an integer:
The mathlib proof of this lemma calls exact_mod_cast four times, to close subgoals 0 ≤ p ⊢ 0 ≤ ↑p, 1 ≤ p ⊢ 1 ≤ ↑p, ↑(p ^ n) ∣ z ⊢ ↑p ^ n ∣ z, and ↑z ≠ 0 ⊢ z ≠ . The proof originally written without norm_cast contains five explicit references to cast lemmas, and uses an explicit intermediate step that is unnecessary in the mathlib proof:
The tool is particularly useful alongside the lift tactic, which conditionally embeds terms in other types. In the following library lemma about the extended nonnegative reals ennreal, lifting two ennreals to the type of nonnegative reals is justified by hypotheses that they are not infinite. In the resulting goal, a b : nnreal ⊢ ennreal.to_real ↑a ≤ ennreal.to_real ↑b ↔ ↑a ≤ ↑b, the casts on the left are nnreal → ennreal; the goal is discharged immediately by norm_cast.
Buzzard, Commelin, and Massot use norm_cast 53 times in their definition of a perfectoid space . A typical use case is to match hypotheses from the units subtype of a monoid to goals stated in the monoid itself, e.g.:
While traditional formalizations often make design decisions to limit the presence of coercions, they seem to be unavoidable in deep mathematical formalizations; Buzzard, Commelin, and Massot write that norm_cast “greatly alleviates …pain” in their project.
The norm_cast family of tactics can be seen as a variant of simplification procedures, which are common tools in proof assistants. Indeed, push_cast is a straightforward application of Lean’s simplifier, and similar functionality is found in many other systems, often in the default set of simplification lemmas.
Isabelle’s standard simplifier  is more powerful than Lean’s, but to our knowledge, the system has no tool similar to norm_cast. While some theories may set up simp lemmas in a style that approximates our procedure, particularly for use with transfer , approaches to managing and eliminating casts tend to be ad hoc combinations of simplification and manual work.
In Coq, unification hints  can sometimes help to unify terms that differ in the placement of coercions. This is less general than our tool, though, as it does not seem to handle situations with serious non-convertibility or type-polymorphic relations.
The norm_cast family aims to eliminate a source of frustration found when formalizing mathematical topics. The metaprogramming features of Lean allow it to be implemented in a lightweight and extensible way. Its development was inspired by discussion between mathematical formalizers and tactic writers. We hypothesize that there are many other similarly lightweight tools that would help to move proof assistants closer to the mathematical vernacular.
The tool is inherently coupled to the mathlib library: it is only effective when the proper lemmas are tagged for its use. We thus consider it a mistake to consider tactic writing and library development separately. The norm_cast tool and its corresponding lemma attributions are part of mathlib, and despite not being themselves definitions or proofs, they constitute a different, procedural, kind of mathematical knowledge.
-  Asperti, A., Ricciotti, W., Coen, C.S., Tassi, E.: Hints in unification. In: Theorem Proving in Higher Order Logics, 22nd International Conference, TPHOLs 2009, Munich, Germany, August 17-20, 2009. Proceedings. pp. 84–98 (2009). https://doi.org/10.1007/978-3-642-03359-9_8, https://doi.org/10.1007/978-3-642-03359-9_8
-  Buzzard, K., Commelin, J., Massot, P.: Formalising perfectoid spaces. In: Proceedings of the 9th ACM SIGPLAN International Conference on Certified Programs and Proofs. p. 299–312. CPP 2020, Association for Computing Machinery, New York, NY, USA (2020). https://doi.org/10.1145/3372885.3373830, https://doi.org/10.1145/3372885.3373830
-  Ebner, G., Ullrich, S., Roesch, J., Avigad, J., de Moura, L.: A metaprogramming framework for formal verification. PACMPL 1(ICFP), 34:1–34:29 (2017). https://doi.org/10.1145/3110278, https://doi.org/10.1145/3110278
-  Huffman, B., Kunčar, O.: Lifting and Transfer: A modular design for quotients in isabelle/hol. In: Gonthier, G., Norrish, M. (eds.) Certified Programs and Proofs. pp. 131–146. Springer International Publishing, Cham (2013)
-  Lewis, R.Y.: A formal proof of Hensel’s lemma over the -adic integers. In: Proceedings of the 8th ACM SIGPLAN International Conference on Certified Programs and Proofs, CPP 2019, Cascais, Portugal, January 14-15, 2019. pp. 15–26 (2019). https://doi.org/10.1145/3293880.3294089, https://doi.org/10.1145/3293880.3294089
-  The mathlib Community: The Lean mathematical library. In: Proceedings of the 9th ACM SIGPLAN International Conference on Certified Programs and Proofs. p. 367–381. CPP 2020, Association for Computing Machinery, New York, NY, USA (2020). https://doi.org/10.1145/3372885.3373824, https://doi.org/10.1145/3372885.3373824
-  de Moura, L., Kong, S., Avigad, J., van Doorn, F., von Raumer, J.: The Lean Theorem Prover (system description) (2015), https://leanprover.github.io/papers/system.pdf
-  Nipkow, T., Paulson, L.C., Wenzel, M.: Isabelle/HOL: a proof assistant for higher-order logic, vol. 2283. Springer Science & Business Media (2002)
-  Selsam, D., Ullrich, S., de Moura, L.: Tabled typeclass resolution (2020), https://arxiv.org/abs/2001.04301
-  Wadler, P., Blott, S.: How to make ad-hoc polymorphism less ad-hoc. In: Conference Record of the Sixteenth Annual ACM Symposium on Principles of Programming Languages, Austin, Texas, USA, January 11-13, 1989. pp. 60–76 (1989). https://doi.org/10.1145/75277.75283, https://doi.org/10.1145/75277.75283