substitution tutorial
This tutorial replaces variables by terms: instantiating a template, swapping
variables, substituting under binders without capturing anything, composing
substitutions and evaluating partially. The examples encode arithmetic as
Term[String], with operators as values: is
Apply(Value("+"), [Variable(x), Value("1")]).
Quick start
moon add Luna-Flow/type_theory@0.2.0
import {
"Luna-Flow/type_theory/core",
"Luna-Flow/type_theory/syntax",
"Luna-Flow/type_theory/substitution",
}
Substitute for in :
test "quick start: x + 1 with x := 2" {
let x = @core.Name::new("x")
let term : @syntax.Term[String] = Apply(Value("+"), [Variable(x), Value("1")])
let s = @substitution.Substitution::singleton(x, @syntax.Value("2"))
assert_eq(s.apply(term), Apply(Value("+"), [Value("2"), Value("1")]))
}
Everyday tasks
The examples below share two helpers:
fn name(text : String) -> @core.Name {
@core.Name::new(text)
}
fn plus(a : @syntax.Term[String], b : @syntax.Term[String]) -> @syntax.Term[String] {
@syntax.Apply(@syntax.Value("+"), [a, b])
}
Swap two variables
All entries of a substitution apply at the same time, so a swap needs no temporary variable:
test "swap x and y" {
let x = name("x")
let y = name("y")
let swap = @substitution.Substitution::singleton(x, @syntax.Term::Variable(y))
.set(y, @syntax.Term::Variable(x))
assert_eq(swap.apply(plus(Variable(x), Variable(y))), plus(Variable(y), Variable(x)))
}
Substitute under a binder
Substituting for in must not turn the inserted
into the bound one. The binder is renamed to y_1:
test "no capture under a binder" {
let x = name("x")
let y = name("y")
let term : @syntax.Term[String] = Bind(y, plus(Variable(x), Variable(y)))
let result = @substitution.Substitution::singleton(x, @syntax.Term::Variable(y))
.apply(term)
let y1 = name("y_1")
let expected : @syntax.Term[String] = Bind(y1, plus(Variable(y), Variable(y1)))
assert_eq(result, expected)
}
A binder for the substituted variable itself shadows it, so nothing happens inside:
test "a binder shadows the substituted variable" {
let x = name("x")
let term : @syntax.Term[String] = Bind(x, Variable(x))
let s = @substitution.Substitution::singleton(x, @syntax.Value("0"))
assert_eq(s.apply(term), term)
}
Evaluate partially
Substitute the variables you know and leave the others in place:
test "partial evaluation" {
let x = name("x")
let y = name("y")
let known = @substitution.Substitution::singleton(x, @syntax.Value("3"))
let result = known.apply(plus(Variable(x), Variable(y)))
assert_eq(result, plus(Value("3"), Variable(y)))
assert_true(@syntax.free_variables(result).contains(y))
}
Compose substitutions
first.then(second) is one substitution that does first and then
second. It is applied in a single traversal:
test "compose two steps into one" {
let x = name("x")
let y = name("y")
let first = @substitution.Substitution::singleton(x, plus(Variable(y), Value("1")))
let second = @substitution.Substitution::singleton(y, @syntax.Value("5"))
let both = first.then(second)
let term = plus(Variable(x), Variable(y))
assert_eq(both.apply(term), plus(plus(Value("5"), Value("1")), Value("5")))
assert_true(@syntax.alpha_equal(both.apply(term), second.apply(first.apply(term))))
}
Going further
Implement beta reduction
Beta reduction is one substitution. This
is how @lambda.beta_rule works:
fn beta(t : @syntax.Term[String]) -> @syntax.Term[String]? {
match t {
Apply(Bind(x, body), [arg]) =>
Some(@substitution.Substitution::singleton(x, arg).apply(body))
_ => None
}
}
test "beta via substitution" {
let x = name("x")
let y = name("y")
let k : @syntax.Term[String] = Bind(x, Bind(y, Variable(x)))
let result = beta(Apply(k, [Variable(y)]))
let expected : @syntax.Term[String] = Bind(name("y_1"), Variable(y))
assert_eq(result, Some(expected))
}
Iterate to a fixed point yourself
A substitution is applied once. If replacements may mention variables of the domain and you want those replaced too, apply repeatedly, with a bound, because the process need not terminate (think of ):
fn apply_until_stable(
s : @substitution.Substitution[String],
t : @syntax.Term[String],
limit : Int,
) -> @syntax.Term[String] {
let mut current = t
for _ in 0..<limit {
let next = s.apply(current)
if next == current {
break
}
current = next
}
current
}
test "resolve a chain of definitions" {
let x = name("x")
let y = name("y")
let defs = @substitution.Substitution::singleton(x, @syntax.Term::Variable(y))
.set(y, @syntax.Value("7"))
assert_eq(apply_until_stable(defs, Variable(x), 10), Value("7"))
}
Substitute in your own AST
GenericSubstitution runs the same algorithm on any AST that implements
@syntax.BindingSyntax. The adapter tutorial defines such an
AST and substitutes in it with apply_once.
Common pitfalls
- Ambiguous constructors.
@syntax.Variable(y)is ambiguous when no type is expected, becauseBindingViewalso has aVariablecase. Write@syntax.Term::Variable(y), or annotate the expected type. - Expecting repeated substitution.
applied to gives , not . Use
thenor iterate explicitly. - Comparing with
==. Results may contain renamed binders (y_1). Compare with@syntax.alpha_equalunless you know the exact names. - Variables inside values.
Valuepayloads are not substituted into.
Next steps
- substitution API for every function.
- substitution design for the definition and the composition and substitution lemmas.
- rewrite tutorial to apply rules, built from substitution, anywhere in a term.