h5i

Chapter 11

Substitution, Capture, and Alpha-Equivalence

The previous chapters treated three operations on terms as black boxes: substitution, used by T-App, by Freshen, and by every computation step; weak head normalization, used to expose a Π\Pi or a universe; and definitional equality, used by Check. These operations are part of the trusted kernel, and most kernel bugs hide in them, because they must handle the names of bound variables correctly. This chapter opens the first box, substitution, shows the same naming problem in contexts, and then defines when two terms are equal up to the names of their bound variables.

Free variables and substitution

Π\Pi and λ\lambda bind their variable: in λ(x:A). t\tLam{x}{A}{t}, occurrences of xx inside tt refer to the argument. The binder's scope is the body only; xx is not bound in the annotation AA. A variable occurrence that is not bound by an enclosing Π\Pi or λ\lambda is free.

Definition 11.1 (Free variables).

FV(Sort u)=∅FV(Π(x:A). B)=FV(A)∪(FV(B)∖{x})FV(x)={x}FV(λ(x:A). t)=FV(A)∪(FV(t)∖{x})FV(t  s)=FV(t)∪FV(s)FV(c)=∅\begin{array}{rclcrcl} \FV(\Sort{u}) &=& \emptyset &\qquad& \FV(\tPi{x}{A}{B}) &=& \FV(A) \cup (\FV(B) \setminus \{x\}) \\[2pt] \FV(x) &=& \{x\} && \FV(\tLam{x}{A}{t}) &=& \FV(A) \cup (\FV(t) \setminus \{x\}) \\[2pt] \FV(t\;s) &=& \FV(t) \cup \FV(s) && \FV(c) &=& \emptyset \end{array}

For example, FV(λ(x:P). f  x  y)={P,f,y}\FV(\tLam{x}{P}{f\;x\;y}) = \{P, f, y\}, and FV(Π(P:Sort 0). P→P)=∅\FV(\tPi{P}{\Sort{0}}{P \to P}) = \emptyset, so the latter is a closed term. Constants are never bound, so they contribute nothing.

The explorer colours each bound variable like its binder and underlines free ones; hover a variable to see its binder. Try a term with shadowing, such as fun (x : A) => fun (x : A) => x.

We write t[x↦s]\subst{t}{x}{s} for the result of replacing the free occurrences of xx in tt with ss. Binders make this subtle: in (λ(y:A). x)[x↦y]\subst{(\tLam{y}{A}{x})}{x}{y}, the body means "ignore the argument and return the outer xx". Replacing xx with yy naively gives λ(y:A). y\tLam{y}{A}{y}, the identity function, because the binder has captured the formerly free yy. To preserve the meaning, substitution must rename the binder first.

Definition 11.2 (Capture-avoiding substitution). For □∈{Π,λ}\square \in \{\Pi, \lambda\}:

x[x↦s]=sy[x↦s]=y(y≠x)Sort u[x↦s]=Sort uc[x↦s]=c(t1  t2)[x↦s]=t1[x↦s]    t2[x↦s](□(x:A). B)[x↦s]=□(x:A[x↦s]). B(x is shadowed)(□(y:A). B)[x↦s]=□(y:A[x↦s]). B[x↦s](y≠x,  y∉FV(s))(□(y:A). B)[x↦s]=□(z:A[x↦s]). (B[y↦z])[x↦s](y≠x,  y∈FV(s),  z fresh)\begin{array}{rcll} \subst{x}{x}{s} &=& s \\[2pt] \subst{y}{x}{s} &=& y & (y \neq x) \\[2pt] \subst{\Sort{u}}{x}{s} &=& \Sort{u} \\[2pt] \subst{c}{x}{s} &=& c \\[2pt] \subst{(t_1\;t_2)}{x}{s} &=& \subst{t_1}{x}{s}\;\;\subst{t_2}{x}{s} \\[2pt] \subst{(\square(x{:}A).\,B)}{x}{s} &=& \square(x{:}\subst{A}{x}{s}).\,B & \text{($x$ is shadowed)} \\[2pt] \subst{(\square(y{:}A).\,B)}{x}{s} &=& \square(y{:}\subst{A}{x}{s}).\,\subst{B}{x}{s} & (y \neq x,\; y \notin \FV(s)) \\[2pt] \subst{(\square(y{:}A).\,B)}{x}{s} &=& \square(z{:}\subst{A}{x}{s}).\,\subst{(\subst{B}{y}{z})}{x}{s} & (y \neq x,\; y \in \FV(s),\; z \text{ fresh}) \end{array}

Here "zz fresh" means z∉FV(s)∪FV(B)∪{x}z \notin \FV(s) \cup \FV(B) \cup \{x\}.

The following examples exercise each binder case.

Substitution Result Why
(Π(h:P). P)[P↦Q]\subst{(\tPi{h}{P}{P})}{P}{Q} Π(h:Q). Q\tPi{h}{Q}{Q} no capture possible; the T-App step of Chapter 6
(λ(x:A). x)[x↦s]\subst{(\tLam{x}{A}{x})}{x}{s} λ(x:A). x\tLam{x}{A}{x} xx is shadowed; the body is untouched
(λ(y:x). y)[x↦s]\subst{(\tLam{y}{x}{y})}{x}{s} λ(y:s). y\tLam{y}{s}{y} the annotation is outside the scope of yy
(λ(y:A). x  y)[x↦f  y]\subst{(\tLam{y}{A}{x\;y})}{x}{f\;y} λ(z:A). f  y  z\tLam{z}{A}{f\;y\;z} y∈FV(f  y)y \in \FV(f\;y), so the binder is renamed to zz

In the last example, the free yy of the replacement still refers to the outer yy, and the bound variable, now zz, still refers to the argument. Without the renaming, the result would be λ(y:A). f  y  y\tLam{y}{A}{f\;y\;y}, which passes its argument twice. The explorer performs both substitutions side by side; the colours show whether the yy in the result is the free one from the replacement or the one the binder captured.

Variable capture in contexts

It is tempting to drop the side condition x∉dom(Γ)x \notin \dom(\Gamma) of C-Ext and let a new assumption simply shadow an old one, as most programming languages do. Doing so makes the kernel unsound. Consider

t=λ(x:Sort 0). λ(y:x). λ(x:Sort 0). y.t = \tLam{x}{\Sort{0}}{\tLam{y}{x}{\tLam{x}{\Sort{0}}{y}}}.

The variable yy has the outer xx as its type, and the innermost λ\lambda ignores its argument. So the correct type of tt is

Π(x:Sort 0). Π(y:x). Π(x′:Sort 0). x.\tPi{x}{\Sort{0}}{\tPi{y}{x}{\tPi{x'}{\Sort{0}}{x}}}.

A checker without Freshen\Freshen looks up yy in the context x:Sort 0,  y:x,  x:Sort 0x : \Sort{0},\; y : x,\; x : \Sort{0} and returns its type, the bare name xx. At that point xx refers to the inner binder, and the checker returns

Π(x:Sort 0). Π(y:x). Π(x:Sort 0). x,\tPi{x}{\Sort{0}}{\tPi{y}{x}{\tPi{x}{\Sort{0}}{x}}},

which says "from any element of any type, produce an element of every type". Given A:Sort 0A : \Sort{0} and a:Aa : A, the term t  A  at\;A\;a then has type Π(B:Sort 0). B\tPi{B}{\Sort{0}}{B}, a proof of False. Such AA and aa exist even in the empty context: A=Π(P:Sort 0). P→PA = \tPi{P}{\Sort{0}}{P \to P} is itself a proposition, and the identity function a=λ(P:Sort 0). λ(h:P). ha = \tLam{P}{\Sort{0}}{\tLam{h}{P}{h}} proves it.

With Freshen\Freshen, the innermost binder is renamed to x′x' before it enters the context, so the context becomes x:Sort 0,  y:x,  x′:Sort 0x : \Sort{0},\; y : x,\; x' : \Sort{0}, the lookup of yy returns xx, which still refers to the outer binder, and Infer\Infer returns the correct type above. The derivation explorer shows the renaming happen: the note on the innermost T-Lam node reports it, and the context of the leaf contains x′x'.

# The kernel renames the inner x to x' and infers the correct type.
# A kernel that let the inner x shadow the outer one would accept this
# as a proof of the second statement, which is false.
goal forall (x : Sort 0), forall (y : x), forall (x' : Sort 0), x
exact fun (x : Sort 0) => fun (y : x) => fun (x : Sort 0) => y
qed
goal forall (x : Sort 0), forall (y : x), forall (x' : Sort 0), x'
exact fun (x : Sort 0) => fun (y : x) => fun (x : Sort 0) => y
qed

The problem is the same as in substitution: a name is moved into a scope where it means something else. It affects any representation that identifies variables by name, whether the context is a list or a hash map. There are three common remedies.

  1. Reject shadowing outright. This is the simplest option, but the reserved name _ used by A→BA \to B must be exempt and never added to the context.
  2. Rename on entry, as Freshen\Freshen does. This is our choice.
  3. Represent bound variables by position (de Bruijn indices [de Bruijn 1972]) or use the locally nameless style, in which bound variables are indices and free variables carry unique identifiers. Lean 4 takes the latter approach.

A test suite should include tt above and check that the kernel either rejects it or assigns it the correct type.

Alpha-equivalence

The names of bound variables carry no meaning. λ(x:A). x\tLam{x}{A}{x} and λ(y:A). y\tLam{y}{A}{y} are the same function, and Π(P:Sort 0). P→P\tPi{P}{\Sort{0}}{P \to P} and Π(Q:Sort 0). Q→Q\tPi{Q}{\Sort{0}}{Q \to Q} are the same proposition. Terms that differ only in the names of bound variables are called alpha-equivalent. A kernel cannot compare types with plain syntactic equality, which would reject the second pair.

Definition 11.3 (Alpha-equivalence). =α\alphaeq is the smallest congruence on terms containing, for □∈{Π,λ}\square \in \{\Pi, \lambda\},

□(x:A). B  =α  □(y:A). B[x↦y]whenever y∉FV(B).\square(x{:}A).\,B \;\alphaeq\; \square(y{:}A).\,\subst{B}{x}{y} \qquad \text{whenever } y \notin \FV(B).

(A congruence is an equivalence relation that is preserved by every term constructor: if the parts are related, so are the wholes.) This definition is easy to state but not directly executable, since it does not say which names to choose. An implementation instead walks both terms in lockstep and remembers, for each side, the list of binders passed so far. A bound variable is then identified by which binder it refers to, not by its name.

To describe this walk, we use a judgment of the form E1;E2⊢s=αtE_1; E_2 \vdash s \alphaeq t, where E1E_1 and E2E_2 are lists of names. The list E1E_1 records the binders that enclose ss, from the outermost to the innermost, and E2E_2 records those that enclose tt. The judgment reads: "ss, placed inside the binders E1E_1, and tt, placed inside the binders E2E_2, are the same term up to the names of bound variables". Two lists are needed because the two terms may use different names for corresponding binders. For example, comparing λ(x:A). λ(y:B). x\tLam{x}{A}{\tLam{y}{B}{x}} with λ(a:A). λ(b:B). a\tLam{a}{A}{\tLam{b}{B}{a}} passes two binders on each side and arrives at

x,y;  a,b⊢x=αa,x, y;\; a, b \vdash x \alphaeq a,

which holds because xx and aa both refer to the outer of the two binders. Comparing two whole terms starts from empty lists, ⋅ ;⋅⊢s=αt\cdot\,;\cdot \vdash s \alphaeq t.

To say "the same binder" precisely, let idx(E,x)\mathrm{idx}(E, x) be the position of the last occurrence of xx in EE, counted from the right starting at 00, or undefined if x∉Ex \notin E. In the example, idx((x,y),x)=idx((a,b),a)=1\mathrm{idx}((x, y), x) = \mathrm{idx}((a, b), a) = 1. Taking the last occurrence handles shadowing: in λ(x:A). λ(x:B). x\tLam{x}{A}{\tLam{x}{B}{x}} the body refers to the inner xx.

Figure 11.1. Algorithmic alpha-equivalence. By Theorem 11.4, two terms ss and tt are alpha-equivalent iff ⋅ ;⋅⊢s=αt\cdot\,;\cdot \vdash s \alphaeq t.

E1;E2⊢Sort u=αSort u;(α-Sort)E1;E2⊢c=αc;(α-Const)\dfrac{}{E_1; E_2 \vdash \Sort{u} \alphaeq \Sort{u}}\rulename{$\alpha$-Sort} \qquad \dfrac{}{E_1; E_2 \vdash c \alphaeq c}\rulename{$\alpha$-Const}
idx(E1,x)=idx(E2,y)=iE1;E2⊢x=αy;(α-Bound)x∉E1x∉E2E1;E2⊢x=αx;(α-Free)\dfrac{\mathrm{idx}(E_1, x) = \mathrm{idx}(E_2, y) = i}{E_1; E_2 \vdash x \alphaeq y}\rulename{$\alpha$-Bound} \qquad \dfrac{x \notin E_1 \qquad x \notin E_2}{E_1; E_2 \vdash x \alphaeq x}\rulename{$\alpha$-Free}
E1;E2⊢A=αA′E1,x;  E2,y⊢B=αB′E1;E2⊢Π(x:A). B=αΠ(y:A′). B′;(α-Pi)\dfrac{E_1; E_2 \vdash A \alphaeq A' \qquad E_1, x;\; E_2, y \vdash B \alphaeq B'}{E_1; E_2 \vdash \tPi{x}{A}{B} \alphaeq \tPi{y}{A'}{B'}}\rulename{$\alpha$-Pi}
E1;E2⊢A=αA′E1,x;  E2,y⊢t=αt′E1;E2⊢λ(x:A). t=αλ(y:A′). t′;(α-Lam)E1;E2⊢t1=αt1′E1;E2⊢t2=αt2′E1;E2⊢t1  t2=αt1′  t2′;(α-App)\dfrac{E_1; E_2 \vdash A \alphaeq A' \qquad E_1, x;\; E_2, y \vdash t \alphaeq t'}{E_1; E_2 \vdash \tLam{x}{A}{t} \alphaeq \tLam{y}{A'}{t'}}\rulename{$\alpha$-Lam} \qquad \dfrac{E_1; E_2 \vdash t_1 \alphaeq t_1' \qquad E_1; E_2 \vdash t_2 \alphaeq t_2'}{E_1; E_2 \vdash t_1\;t_2 \alphaeq t_1'\;t_2'}\rulename{$\alpha$-App}

Free variables are compared by name (α\alpha-Free), bound variables by position (α\alpha-Bound), and constants by name (α\alpha-Const). A variable that is bound on one side and free on the other is never equal to anything, which is why there is no rule for that case. Because both sides descend through binders together, E1E_1 and E2E_2 always have the same length. So comparing positions counted from the right is the same as comparing positions counted from the left, and an implementation may store each list as a map from names to binding depths, provided it restores the previous entry when it leaves a binder. Using positions instead of names everywhere gives the de Bruijn representation, in which alpha-equivalent terms are syntactically identical.

Examples

The first example of Chapter 6 ended by comparing the inferred type with the stated one, where P→PP \to P abbreviates Π(_:P). P\tPi{\_}{P}{P}:

  • ⋅ ;⋅⊢Π(P:Sort 0). Π(h:P). P=αΠ(P:Sort 0). Π(_:P). P\cdot\,;\cdot \vdash \tPi{P}{\Sort{0}}{\tPi{h}{P}{P}} \alphaeq \tPi{P}{\Sort{0}}{\tPi{\_}{P}{P}} α\alpha-Pi
    • ⋅ ;⋅⊢Sort 0=αSort 0\cdot\,;\cdot \vdash \Sort{0} \alphaeq \Sort{0}: holds α\alpha-Sort
    • P; P⊢Π(h:P). P=αΠ(_:P). PP;\, P \vdash \tPi{h}{P}{P} \alphaeq \tPi{\_}{P}{P} α\alpha-Pi
      • P; P⊢P=αPP;\, P \vdash P \alphaeq P: holds, both have index 00 α\alpha-Bound
      • P,h;  P,_⊢P=αPP, h;\; P, \_ \vdash P \alphaeq P: holds, both have index 11 α\alpha-Bound

The names hh and _\_ differ, but neither is ever looked up, so they do not matter. The wrong proof of Chapter 6 fails at the last step instead:

  • ⋅ ;⋅⊢Π(P:Sort 0). Π(h:P). Sort 0=αΠ(P:Sort 0). Π(_:P). P\cdot\,;\cdot \vdash \tPi{P}{\Sort{0}}{\tPi{h}{P}{\Sort{0}}} \alphaeq \tPi{P}{\Sort{0}}{\tPi{\_}{P}{P}} α\alpha-Pi
    • ⋅ ;⋅⊢Sort 0=αSort 0\cdot\,;\cdot \vdash \Sort{0} \alphaeq \Sort{0}: holds α\alpha-Sort
    • P; P⊢Π(h:P). Sort 0=αΠ(_:P). PP;\, P \vdash \tPi{h}{P}{\Sort{0}} \alphaeq \tPi{\_}{P}{P} α\alpha-Pi
      • P; P⊢P=αPP;\, P \vdash P \alphaeq P: holds α\alpha-Bound
      • P,h;  P,_⊢Sort 0=αPP, h;\; P, \_ \vdash \Sort{0} \alphaeq P: no rule applies

Positions also tell apart terms that a careless comparison of names would confuse. λ(x:A). λ(y:A). x\tLam{x}{A}{\tLam{y}{A}{x}} and λ(a:A). λ(b:A). b\tLam{a}{A}{\tLam{b}{A}{b}} are not alpha-equivalent: the bodies reach x,y; a,b⊢x=αbx, y;\, a, b \vdash x \alphaeq b, and idx\mathrm{idx} gives 11 for xx but 00 for bb. Conversely, λ(x:A). λ(x:A). x\tLam{x}{A}{\tLam{x}{A}{x}} and λ(a:A). λ(b:A). b\tLam{a}{A}{\tLam{b}{A}{b}} are alpha-equivalent, because the last occurrence of xx in (x,x)(x, x) has index 00, the same as bb in (a,b)(a, b).

The rules of Figure 11.1 are a different definition from Definition 11.3, so we must check that they agree.

Theorem 11.4 (Correctness of algorithmic alpha-equivalence). For all terms ss and tt, s=αts \alphaeq t in the sense of Definition 11.3 if and only if ⋅ ;⋅⊢s=αt\cdot\,;\cdot \vdash s \alphaeq t is derivable by the rules of Figure 11.1.

The idea of the proof is that both sides say the same thing: ss and tt have the same de Bruijn representation. For the algorithm this is almost immediate, since its rules compare positions exactly as de Bruijn indices do. For Definition 11.3, the key fact is that renaming a bound variable does not change the de Bruijn representation. Appendix B gives the full proof.

Exercises

Compute (λ(y:A). λ(x:B). x  y)[x↦y]\subst{(\tLam{y}{A}{\tLam{x}{B}{x\;y}})}{x}{y} step by step.

Solution

The outer binder is yy, which is free in the replacement yy, so the capture case applies: rename yy to a fresh zz, giving body λ(x:B). x  z\tLam{x}{B}{x\;z}, then substitute. The inner binder is xx, the variable being replaced, so the inner body is untouched. Result: λ(z:A). λ(x:B). x  z\tLam{z}{A}{\tLam{x}{B}{x\;z}}. Note that the inner xx was never a free occurrence, so nothing was replaced at all; the only effect was the renaming forced by capture avoidance.

Decide whether λ(x:A). λ(y:A). y  x\tLam{x}{A}{\tLam{y}{A}{y\;x}} and λ(y:A). λ(x:A). x  y\tLam{y}{A}{\tLam{x}{A}{x\;y}} are alpha-equivalent by running Figure 11.1.

Solution

Yes. After two α\alpha-Lam steps, the binder lists are x,yx, y and y,xy, x, while the bodies are y  xy\;x and x  yx\;y. The two α\alpha-App comparisons match yy with xx at indices 00 and 00, then xx with yy at indices 11 and 11. The names are swapped, but their positions agree.

What to remember

  • Substitution replaces free occurrences only, and must rename a binder when the replacement mentions the binder's name (capture).
  • The same capture problem arises in contexts. A kernel that lets a binder shadow a context entry can prove False; Freshen prevents it.
  • Alpha-equivalence compares bound variables by the position of their binder and free variables by name. Figure 11.1 is the algorithm; Theorem 11.4 says it matches the definition.
  • All three live in src/substitution.rs: subst, fv, and alpha.