substitution design
Design goal
Substitution is the operation every other part of the library is built on:
beta reduction substitutes an argument for a parameter, rewriting rules
instantiate their variables, and partial evaluation in downstream packages
replaces known variables by values. substitution provides one
capture-avoiding, simultaneous substitution for Term[T] and the same
algorithm for any BindingSyntax AST, with laws that hold up to
alpha-equivalence.
Mathematical background
Terms, , and are those of the syntax design.
Substitutions
A substitution is a map from names to terms that is the identity outside a finite domain . Its support is , and removes from the domain. For a set of names , the relevant range of on is
Capture-avoiding simultaneous substitution
Substitution::apply computes by
where and is the checked bound renaming of the syntax design. The case split is exact: the binder is renamed only when a replacement that is actually inserted into mentions free.
Design decisions
Simultaneous, one pass
Problem. Should applied to give or ?
Options. Sequential substitution (apply the entries one after another), iterated substitution (repeat until nothing changes), or simultaneous substitution (every variable is looked up once in the original term).
Choice. Simultaneous. The variable case looks up once and never visits the inserted term again:
Simultaneous substitution is the one with an algebra (composition below), it
always terminates, and it expresses swaps such as
directly. Sequential application is still available as composition with
then, and iteration to a fixed point is a policy of the caller. The
generic version is called apply_once to make this explicit.
Rename binders only when needed
Problem. Capture happens when a binder lies above an inserted replacement in which is free. Renaming every binder avoids it but makes results unreadable.
Choice. Rename only when , and then choose the fresh name with the hint . The fresh name must avoid three sets, each for its own reason:
- of the relevant replacements, or the renamed binder would capture them again;
- , or the occurrences that were renamed from to would themselves be substituted (regression test “fresh binders avoid the substitution domain”);
- , so that the checked bound renaming cannot fail, and the renamed variable cannot be confused with an inner binder of the same name.
covers the first two sets. The abort in the
implementation is therefore unreachable: by the freshness lemma
, which is exactly the side condition of
alpha_rename_bound.
Composition as a first-class operation
then builds with
so that sequential application can be expressed as one simultaneous substitution, which is cheaper (one traversal) and has the laws below.
Correctness / invariants
Free variables
Lemma 1. .
By induction on . The variable, value and application cases are immediate. For without renaming, let and assume :
Step : for , contributes , which is removed. For , , and : either and , or . With renaming, the same computation applies to and , where makes the side condition hold.
Corollary (no capture). A variable free in an inserted replacement , , is free in .
Only the free variables matter
Lemma 2. , where
is restrict(S).
The variable case is the definition. In the binder case, the renaming test already restricts to , so both sides rename the same binders, and only the variables in are looked up. The fresh names may differ, because they avoid the support of different substitutions, hence equality up to .
Alpha-invariance
Lemma 3. If then .
It suffices to check one alpha step with . Both sides become binders over the same body up to the bound name, and by Lemma 1 their bodies have the same free variables outside the binder, so they are alpha-equivalent. As a consequence substitution is well defined on alpha-equivalence classes, and the choice of fresh names never matters semantically.
Composition
Lemma 4. .
By Lemma 3 we may choose a representative of in which no binder lies in ; then no binder is renamed on either side and both substitutions pass under binders unchanged. The variable case splits as in the definition of :
The application and value cases follow by induction.
Corollary (substitution lemma). For and ,
Both sides are single simultaneous substitutions by Lemma 4. The left is . The right is , and because (Lemma 1). The two maps are equal, so the results are alpha-equivalent.11 Barendregt, The Lambda Calculus, Lemma 2.1.16. This is the lemma that makes beta reduction compatible with substitution in the lambda calculus packages.
Renamings embed into substitutions
For a renaming and a list of names ,
from_renaming(L, ρ) is
(as variables), and : both replace each free
by the variable , and both freshen a binder exactly when it would
capture.
Cost
Each binder computes the free variables of its body, so apply costs
hash-set operations for a term of size and binder depth
, plus one extra body traversal per renamed binder. then costs one
apply per entry of self.
Generic substitution
GenericSubstitution::apply_once is the same definition with every
constructor replaced by its BindingSyntax counterpart, so Lemmas 1–3 hold
for any implementation that satisfies the view laws of the
adapter design. On Term[T] the two algorithms coincide.
Alternatives rejected
- The Barendregt variable convention. Assuming that bound names are distinct from all free names would remove the renaming case, but terms come from users and from other algorithms, and the convention is not preserved by reduction. The library renames explicitly instead.
- Substitution on De Bruijn terms only. Index-based substitution needs no renaming and is provided by debruijn, but the shared interface for downstream ASTs is named, so named substitution must be correct on its own.
- Iterated substitution. Repeating until no domain variable occurs can diverge () and has no composition law. Callers that want a fixed point iterate explicitly.
Boundaries
- No unification, matching or occurs check: substitutions are given, not solved for.
GenericSubstitutionhas nothenorrestrict; compose generic substitutions by applying them in turn.- Results are equal to the textbook definition only up to ; use
@syntax.alpha_equalto compare them. - Values are never entered, so a
Valuepayload that contains variables is not substituted into.
Footnotes
-
Barendregt, The Lambda Calculus, Lemma 2.1.16. ↩