h5i

Chapter 2

The Language of Terms

Before a kernel can check anything, it needs a precise language of terms. Ours has five kinds of term, and that is enough to express propositional logic with quantification over propositions. This chapter fixes the grammar, the notational conventions used for the rest of the book, and the Rust type that represents terms.

Five kinds of terms

The core language is a small dependent type theory. Its terms are generated by the grammar

t,A,B  ::=  Sort u  ∣  x  ∣  Π(x:A). B  ∣  λ(x:A). t  ∣  t  st, A, B \;::=\; \Sort{u} \;\mid\; x \;\mid\; \tPi{x}{A}{B} \;\mid\; \tLam{x}{A}{t} \;\mid\; t\;s

where uu is a natural number and xx is a variable name. Each form has a role.

  • Sort u\Sort{u} is a universe: a type whose elements are themselves types. The number uu is its level. Sort 0\Sort{0} is the universe of propositions. Chapter 13 explains why there is a whole hierarchy rather than a single universe.
  • xx is a variable. Its meaning comes from the context in which it appears, either a λ\lambda or Π\Pi binder enclosing it or an assumption in the typing context.
  • Π(x:A). B\tPi{x}{A}{B} is a Π\Pi-type, the dependent function type from Chapter 1. It is the type of functions that take an x:Ax : A and return something of type BB, where BB may mention xx.
  • λ(x:A). t\tLam{x}{A}{t} is a λ\lambda-abstraction, a function that takes an argument xx of type AA and returns tt.
  • t  st\;s is application of tt to ss.

The grammar makes no distinction between types and terms. The proposition Π(P:Sort 0). P→P\tPi{P}{\Sort{0}}{P \to P} and its proof λ(P:Sort 0). λ(p:P). p\tLam{P}{\Sort{0}}{\tLam{p}{P}{p}} are both terms built from the same five constructors:

λ(P:Sort 0). λ(p:P). p⏟proof:Π(P:Sort 0). P→P⏟proposition, the type of the proof.\underbrace{\tLam{P}{\Sort{0}}{\tLam{p}{P}{p}}}_{\text{proof}} \quad : \quad \underbrace{\tPi{P}{\Sort{0}}{P \to P}}_{\text{proposition, the type of the proof}}.

The same two terms, seen as trees, make the shared grammar visible. Every node is one of the five constructors; the name on a Π\Pi or λ\lambda node is the variable it binds. Click a node to see the subterm it stands for, or type another term.

Whether a term is a valid type is not a syntactic property. It is decided by the type checker: a term is a type exactly when its own type is a universe (Chapter 5).

Conventions

The book uses the following letters consistently.

Letters Stand for
s,t,a,b,fs, t, a, b, f and A,B,C,TA, B, C, T (possibly primed or subscripted) terms
x,y,zx, y, z and any name bound by Π\Pi or λ\lambda variable names
u,v,wu, v, w universe levels
Γ\Gamma typing contexts (Chapter 3)
Σ\Sigma the signature of constants (Chapter 7)

Three abbreviations keep terms readable.

Arrows. When BB does not mention xx, we write A→BA \to B for Π(x:A). B\tPi{x}{A}{B}. Ordinary function types are thus a special case of dependent function types, and implication is a special case of universal quantification. In the implementation the binder of an arrow is the reserved name _.

Associativity. Application associates to the left and arrows to the right:

f  a  b  =  (f  a)  b,A→B→C  =  A→(B→C).f\;a\;b \;=\; (f\;a)\;b, \qquad A \to B \to C \;=\; A \to (B \to C).

Currying. A function of two arguments is a function that takes the first argument and returns a function waiting for the second. For example, λ(a:A). λ(b:B). a\tLam{a}{A}{\tLam{b}{B}{a}} has type A→B→AA \to B \to A, and applying it to x:Ax : A and y:By : B is written

(λ(a:A). λ(b:B). a)  x  y  =  ((λ(a:A). λ(b:B). a)  x)  y.(\tLam{a}{A}{\tLam{b}{B}{a}})\;x\;y \;=\; ((\tLam{a}{A}{\tLam{b}{B}{a}})\;x)\;y.

The two associativity conventions are designed to fit together: a curried function of type A→B→CA \to B \to C applied as f  a  bf\;a\;b needs no parentheses on either side.

Binding and scope

In λ(x:A). t\tLam{x}{A}{t} and Π(x:A). B\tPi{x}{A}{B}, the variable xx is bound, and its scope is the body tt or BB. The annotation AA is outside the scope: in λ(x:x). x\tLam{x}{x}{x} the xx in the annotation refers to something outside the term, while the xx in the body refers to the argument. A variable occurrence that is not inside the scope of a binder for it is free. For example, in λ(x:P). f  x  y\tLam{x}{P}{f\;x\;y} the variables PP, ff, and yy are free and xx is bound.

Two facts about bound variables matter throughout the book, and both are formalized in Chapter 11.

  1. The name of a bound variable carries no meaning: λ(x:A). x\tLam{x}{A}{x} and λ(y:A). y\tLam{y}{A}{y} are the same function. Terms that differ only in the names of bound variables are called alpha-equivalent, and a kernel must treat them as equal.
  2. Replacing a variable by a term, which the typing rules and computation both require, must not accidentally bring a free variable under a binder of the same name. Getting this right is called capture-avoiding substitution.

Both facts are easy to state and both have caused real bugs in real proof checkers, which is why they get their own chapter.

In the code

The same file provides small constructor functions so that terms can be written without nested Box::new calls: var("x"), const_term("Nat"), app(f, a), pi("x", a, b), lam("x", a, b), and arrow(a, b), which builds pi("_", a, b). Three helpers encode the conventions of this chapter directly.

/// f a_1 ... a_n   (left-associated application)
pub fn apps(f: Term, args: impl IntoIterator<Item = Term>) -> Term {
    args.into_iter().fold(f, app)
}

/// Pi (x_1 : A_1) ... Pi (x_k : A_k). tail   (a telescope of binders)
pub fn telescope(binders: &[(String, Term)], tail: Term) -> Term {
    binders.iter().rev().fold(tail, |body, (name, ty)| pi(name, ty.clone(), body))
}

/// Decomposes t into its spine: a head that is not an application, and its arguments.
/// f a b  becomes  (f, [a, b]);  a non-application becomes  (t, []).
pub fn spine(t: &Term) -> (Term, Vec<Term>) {
    match t {
        Term::App(f, a) => {
            let (head, mut args) = spine(f);
            args.push((**a).clone());
            (head, args)
        }
        _ => (t.clone(), vec![]),
    }
}

apps and spine are inverses of each other: apps(h, args) builds the left-nested application h  a1⋯anh\;a_1 \cdots a_n, and spine takes it apart again. The spine view is how the reduction engine of Chapter 12 looks at a term, so get comfortable with it now. The nested term ((f  a)  b)  c((f\;a)\;b)\;c has head ff and arguments [a,b,c][a, b, c]; walking down the function side of each application reaches the head, and the arguments collected on the way, read from the inside out, are in the right order.

The surface syntax of the .leanmini scripts used throughout the book writes the same terms as Sort 0, forall (x : A), B, A -> B, fun (x : A) => b, and juxtaposition for application. Appendix E gives the full grammar.

Exercises

Write the proposition "for all propositions PP and QQ, PP implies QQ implies PP" as a term, and give a proof term for it.

Solution

The proposition is Π(P:Sort 0). Π(Q:Sort 0). P→Q→P\tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{P \to Q \to P}}, which unfolds to Π(P:Sort 0). Π(Q:Sort 0). Π(_:P). Π(_:Q). P\tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{\tPi{\_}{P}{\tPi{\_}{Q}{P}}}}. A proof is λ(P:Sort 0). λ(Q:Sort 0). λ(p:P). λ(q:Q). p\tLam{P}{\Sort{0}}{\tLam{Q}{\Sort{0}}{\tLam{p}{P}{\tLam{q}{Q}{p}}}}. In leanmini syntax:

goal forall (P : Sort 0), forall (Q : Sort 0), P -> Q -> P
exact fun (P : Sort 0) => fun (Q : Sort 0) => fun (p : P) => fun (q : Q) => p
qed

Fully parenthesize f  a  (g  b)  cf\;a\;(g\;b)\;c and A→(B→C)→DA \to (B \to C) \to D, then give the spine of the first term.

Solution

f  a  (g  b)  c=((f  a)  (g  b))  cf\;a\;(g\;b)\;c = ((f\;a)\;(g\;b))\;c, with head ff and arguments [a,  g  b,  c][a,\; g\;b,\; c]. Note that g  bg\;b stays a single argument because of the parentheses. A→(B→C)→D=A→((B→C)→D)A \to (B \to C) \to D = A \to ((B \to C) \to D), which is Π(_:A). Π(_:(Π(_:B). C)). D\tPi{\_}{A}{\tPi{\_}{(\tPi{\_}{B}{C})}{D}}.

In λ(x:A). λ(y:x). λ(x:Sort 0). y\tLam{x}{A}{\tLam{y}{x}{\tLam{x}{\Sort{0}}{y}}}, which occurrences of xx refer to which binder? Which variables are free?

Solution

The xx in the annotation of yy refers to the outer binder λ(x:A)\lambda(x : A), because the inner λ(x:Sort 0)\lambda(x : \Sort{0}) has not been entered yet. The innermost xx is bound by the inner binder but never used. The only free variable is AA. This term reappears in Chapter 11, where it breaks a type checker that handles shadowing carelessly.

What to remember

  • Terms are universes Sort u\Sort{u}, variables, Π\Pi-types, λ\lambda-abstractions, and applications. Types and terms share one grammar.
  • A→BA \to B abbreviates a Π\Pi-type whose body ignores its variable. Application is left-associative, arrows are right-associative, and functions are curried.
  • Bound variable names are arbitrary, and substitution must avoid capture. Both are handled in Chapter 11.
  • In Rust, Term is an enum with one variant per form, and spine/apps convert between nested applications and a head with an argument list.