h5i

Chapter 12

Reduction and Conversion

Chapter 11 handled names. This chapter handles computation: the three kinds of reduction step, the definition of definitional equality, and the algorithms Whnf, Normalize, and IsDefEq that Algorithm 5.1 left open. The main design point is that the computation rules of inductive types are stored as data in the signature, so one reduction function serves every inductive type ever declared.

Reduction

Applying a λ\lambda-abstraction to an argument is computed by substituting the argument for the bound variable:

(λ(x:A). t)  s  →β  t[x↦s].(\tLam{x}{A}{t})\;s \;\betared\; \subst{t}{x}{s}.

This step is beta-reduction, and a subterm of the form (λ(x:A). t)  s(\tLam{x}{A}{t})\;s is a beta-redex. Part II added two more kinds of step: delta-reduction, which unfolds a definition (Chapter 7), and iota-reduction, which runs a recursor on a constructor (Figures 7.1 and 9.1, and the general rule of Chapter 8). We allow each step anywhere inside a term, and write →\to for a single step of any kind. A term in which no step applies is in normal form. For example, with Id=λ(X:Sort 0). X\mathit{Id} = \tLam{X}{\Sort{0}}{X},

λ(h:Id  P). (λ(y:P). y)  h  →β  λ(h:P). (λ(y:P). y)  h  →β  λ(h:P). h,\tLam{h}{\mathit{Id}\;P}{(\tLam{y}{P}{y})\;h} \;\betared\; \tLam{h}{P}{(\tLam{y}{P}{y})\;h} \;\betared\; \tLam{h}{P}{h},

and the last term is in normal form. Reducing the inner redex first leads to the same result.

Conversion

Definitional equality, also called conversion, identifies terms that compute to the same result, up to the names of bound variables.

Definition 12.1 (Conversion). s≡ts \conv t is the smallest equivalence relation that contains =α\alphaeq and →\to and is closed under all term constructors.

To decide s≡ts \conv t, the kernel normalizes both terms and compares the results with =α\alphaeq. This procedure relies on two facts.

  • Reduction is confluent: different reduction orders cannot lead to different normal forms. So s≡ts \conv t holds exactly when the normal forms of ss and tt are alpha-equivalent; there is no need to search for a clever sequence of steps.
  • Every well-typed term has a normal form, so normalization terminates. This holds for our system, but it is a deep theorem (strong normalization, Chapter 14) [Barendregt 1992].

A practical kernel also caps the number of reduction steps, so that a bug cannot make it loop forever; our implementation stops after 20,000 steps with the error "reduction fuel exhausted".

Computation rules as data

Figures 7.1 and 9.1 write each iota-rule as a separate equation, which suggests that every new inductive type needs new reduction code in the kernel. It does not. A kernel stores the iota-rules as data in the signature, next to the recursor, and a single piece of code applies them for every inductive type.

Each recursor entry records the position of its major premise, the argument whose constructor decides which rule applies, and the constructors of its inductive type. For each constructor the kernel knows how many leading arguments are parameters (which the rule ignores) and how many are fields (which the rule passes on), and which fields are recursive. For three of our recursors, the data and the resulting right-hand sides are as follows; type annotations on λ\lambda are omitted.

RecursorMajorConstructorParams, fieldsRight-hand sideNat.recu4thzero0,  0λM z s.  zsucc0,  1λM z s n.  s  n  (Nat.recu  M  z  s  n)Eq.recu,v6threflu2,  0λA a M m b.  mTree.recv5thleaf0,  0λM m1 m2 m3.  m1node0,  1λM m1 m2 m3 t.  m2  t  (Tree.recv  M m1 m2 m3  t)branch0,  2λM m1 m2 m3 l r.  m3  l  r  (Tree.recv  M m1 m2 m3  l)  (⋯  r)\begin{array}{llcll} \textbf{Recursor} & \textbf{Major} & \textbf{Constructor} & \textbf{Params, fields} & \textbf{Right-hand side} \\[2pt] \NatRec{u} & \text{4th} & \zero & 0,\;0 & \lambda M\,z\,s.\; z \\[2pt] & & \suc & 0,\;1 & \lambda M\,z\,s\,n.\; s\;n\;(\NatRec{u}\;M\;z\;s\;n) \\[2pt] \EqRecDep{u,v} & \text{6th} & \refl{u} & 2,\;0 & \lambda A\,a\,M\,m\,b.\; m \\[2pt] \mathsf{Tree.rec}_v & \text{5th} & \mathsf{leaf} & 0,\;0 & \lambda M\,m_1\,m_2\,m_3.\; m_1 \\[2pt] & & \mathsf{node} & 0,\;1 & \lambda M\,m_1\,m_2\,m_3\,t.\; m_2\;t\;(\mathsf{Tree.rec}_v\;M\,m_1\,m_2\,m_3\;t) \\[2pt] & & \mathsf{branch} & 0,\;2 & \lambda M\,m_1\,m_2\,m_3\,l\,r.\; m_3\;l\;r\;(\mathsf{Tree.rec}_v\;M\,m_1\,m_2\,m_3\;l)\;(\cdots\;r) \end{array}

The right-hand side takes the arguments before the major premise, then the fields of the constructor. For example, Nat.recu  M  z  s  (succ  n)\NatRec{u}\;M\;z\;s\;(\suc\;n) selects the second rule and becomes

(λM z s n.  s  n  (Nat.recu  M  z  s  n))  M  z  s  n,(\lambda M\,z\,s\,n.\; s\;n\;(\NatRec{u}\;M\;z\;s\;n))\;M\;z\;s\;n,

which beta-reduces to the right-hand side of the second equation in Figure 7.1. Likewise, the only rule of Eq.recu,v\EqRecDep{u,v} fires on Eq.recu,v  A  a  M  m  b  (reflu  A′  c)\EqRecDep{u,v}\;A\;a\;M\;m\;b\;(\refl{u}\;A'\;c). The constructor reflu\refl{u} has two parameters, A′A' and cc, and no fields, so the right-hand side receives just A  a  M  m  bA\;a\;M\;m\;b and returns mm.

Weak head normal form and normalization

Algorithm 12.1 defines the three functions that Algorithm 5.1 left open. Both Whnf\Whnf and Normalize\Normalize view a term as a spine h  a1⋯anh\;a_1 \cdots a_n: a head hh that is not an application, applied to n≥0n \ge 0 arguments (Chapter 2). Every term has exactly one such decomposition.

Algorithm 12.1. Weak head normalization with beta, delta, and iota; full normalization; and definitional equality.

Whnf(t):
  loop:
    write t as a spine h a_1 ... a_n
    if h = λ(x : A). b and n ≥ 1:                               -- beta
        t ← (b[x ↦ a_1]) a_2 ... a_n
    else if h = c and (c : C := d) ∈ Σ:                         -- delta
        t ← d a_1 ... a_n
    else if h = c is a recursor with major position p ≤ n:      -- iota
        k b_1 ... b_j ← Whnf(a_p)
        if k is a constructor of c's inductive type with q parameters:
            t ← rhs_k a_1 ... a_{p-1} b_{q+1} ... b_j a_{p+1} ... a_n
        else return t                                           -- stuck, e.g. on a variable
    else return t

Normalize(t):
  t ← Whnf(t)
  if t = Π(x : A). B:  return Π(x : Normalize(A)). Normalize(B)
  if t = λ(x : A). b:  return λ(x : Normalize(A)). Normalize(b)
  else write t as a spine h a_1 ... a_n and return h Normalize(a_1) ... Normalize(a_n)

IsDefEq(s, t):
  return  · ; · ⊢ Normalize(s) =α Normalize(t)                  -- Figure 11.1

Whnf\Whnf reduces only at the head of a term, until no rule applies there; the result is the weak head normal form. It never looks inside a λ\lambda, a Π\Pi, or the arguments of a stuck head. This is enough when a caller only needs the outermost shape of a type, such as a Π\Pi in the application case of Infer\Infer, and it is cheaper than full normalization. Normalize\Normalize first computes the weak head normal form and then normalizes every immediate subterm. For example, with t=λ(h:Id  P). ht = \tLam{h}{\mathit{Id}\;P}{h} and Id\mathit{Id} as above,

Whnf(t)=t,Normalize(t)=λ(h:P). h,\Whnf(t) = t, \qquad \Normalize(t) = \tLam{h}{P}{h},

because the head of tt is a λ\lambda with no arguments, while Normalize\Normalize continues into the annotation Id  P\mathit{Id}\;P. Likewise, Whnf(Id  (Id  P→P))=Id  P→P\Whnf(\mathit{Id}\;(\mathit{Id}\;P \to P)) = \mathit{Id}\;P \to P, which already shows a Π\Pi although its domain is not yet normal. Both are visible below: no head step applies, yet the normal form differs.

The three cases of Whnf\Whnf have the following properties.

  • The iota case contains no reference to Nat\Nat, Eq\Eq{}, or Tree\mathsf{Tree}. It reads the major position and the constructor data from the signature. It is written once; after that, each new inductive type adds rows of data, not code.
  • The major premise is put in weak head normal form before the rules are consulted, because it may become a constructor only after computation. For 2+12 + 1 in Chapter 7, the major premise is the numeral 1=succ  zero1 = \suc\;\zero, already a constructor; for double  (1+1)\mathit{double}\;(1 + 1), it is 1+11 + 1, which Whnf\Whnf turns into succ  (1+0)\suc\;(1 + 0) first.
  • If the major premise does not become a constructor, for instance because it is a variable, the term is stuck and stays as it is; this is why 0+n0 + n does not reduce (Chapter 9).

Here is Whnf\Whnf at work on double  (1+1)\mathit{double}\;(1 + 1), with double\mathit{double} as in Chapter 7, computed live. Only the head is ever reduced, but reducing the major premise forces an inner computation.

def add : Nat -> Nat -> Nat := fun (m : Nat) => fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) m (fun (k : Nat) => fun (r : Nat) => Nat.succ r) n
def double : Nat -> Nat := fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) Nat.zero (fun (k : Nat) => fun (r : Nat) => Nat.succ (Nat.succ r)) n

The step marked whnf is the inner computation: the recursor needs its major premise to be a constructor, so 1+11 + 1 is reduced at its head first. The final weak head normal form still contains a stuck inner recursor; the normal form below it is what Normalize\Normalize produces by continuing inside.

For simplicity, IsDefEq\IsDefEq fully normalizes both terms, as our implementation does. Practical kernels instead compare lazily: they reduce both sides to weak head normal form, compare the heads, and inspect arguments only when needed. This avoids fully normalizing large terms whose heads already differ. Strong normalization guarantees that all three functions terminate on well-typed terms.

In the code

One subtlety in normalize_with_fuel deserves mention. After normalizing the function and argument of an application, it calls whnf on the rebuilt term once more. Normalizing an argument can expose a new redex at the head: for a stuck recursor application whose major premise becomes a constructor only after its own arguments are normalized, the head must be reduced again. Without this re-check, is_def_eq would miss some equalities, and the test conversion_renormalizes_exposed_redex in src/proof.rs guards it.

Exercises

Give the weak head normal form and the normal form of (λ(f:Nat→Nat). f)  (λ(x:Nat). x)  zero(\tLam{f}{\Nat \to \Nat}{f})\;(\tLam{x}{\Nat}{x})\;\zero.

Solution

The spine has head λ(f:Nat→Nat). f\tLam{f}{\Nat \to \Nat}{f} with two arguments. Beta on the first argument gives (λ(x:Nat). x)  zero(\tLam{x}{\Nat}{x})\;\zero, which is again a beta-redex at the head and reduces to zero\zero. So both the weak head normal form and the normal form are zero\zero. (This is the term in the test conversion_renormalizes_exposed_redex.)

Why does Whnf\Whnf not reduce under a λ\lambda? What would go wrong in Infer\Infer if Whnf\Whnf returned λ(x:A). b\tLam{x}{A}{b} with bb normalized?

Solution

Nothing would be wrong: the result would still be convertible to the input. But it would be wasted work. Infer\Infer calls Whnf\Whnf only to learn the outermost shape, which is already visible once the head is a λ\lambda. Reducing inside could be arbitrarily expensive, for instance in an annotation that is never compared with anything. Keeping Whnf\Whnf shallow keeps type inference cheap on large terms.

What to remember

  • Three reduction steps: beta (apply a λ\lambda), delta (unfold a definition), iota (run a recursor on a constructor). Conversion is the equivalence they generate together with alpha-equivalence.
  • Conversion is decided by normalizing both sides and comparing up to alpha, which is correct by confluence and terminates by strong normalization.
  • Iota-rules are data: the major premise position plus constructor metadata. One whnf serves every inductive type.
  • Whnf reduces only at the head and is what Infer needs; Normalize goes everywhere and is what IsDefEq uses. A step budget guards against bugs.