适配器教程
本教程将你自己的表达式类型接入 type_theory。你将为一个小型 AST 实现 @syntax.BindingSyntax,然后直接在其上使用泛型的自由变量计算、避免捕获的代换和改写,而无需转换为 Term[T]。最后你将测试适配器定律。
快速入门
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",
"Luna-Flow/type_theory/rewrite",
}
下面是一个包含数字、运算符符号、变量、调用和绑定子 Fun(x, body) 的表达式语言,以及它的适配器:
priv enum Expr {
Num(Int)
Sym(String)
Var(@core.Name)
Call(Expr, Array[Expr])
Fun(@core.Name, Expr)
} derive(Eq, Debug)
impl @syntax.BindingSyntax for Expr with fn project(self) {
match self {
Num(_) | Sym(_) => Opaque
Var(x) => Variable(x)
Call(f, args) => Apply(f, args)
Fun(x, body) => Bind(x, body)
}
}
impl @syntax.BindingSyntax for Expr with fn variable(x) {
Var(x)
}
impl @syntax.BindingSyntax for Expr with fn apply(f, args) {
Call(f, args)
}
impl @syntax.BindingSyntax for Expr with fn bind(x, body) {
Fun(x, body)
}
test "quick start: free variables of an Expr" {
let x = @core.Name::new("x")
let y = @core.Name::new("y")
let e = Fun(x, Call(Sym("+"), [Var(x), Var(y)]))
let fv = @syntax.generic_free_variables(e)
assert_true(fv.contains(y))
assert_false(fv.contains(x))
}
数字和运算符符号投影为 Opaque,因为它们不含变量。该类型是 priv 的,因为这个示例只存在于单个包中;导出其 AST 的库应将它设为 pub(all),并用 pub extend 声明它所提升的方法。调用的运算符就是它的头部,因此重建调用时会保留运算符。
日常任务
无捕获地代换
GenericSubstitution::apply_once 替换自由变量,并在必要时重命名 Fun 绑定子:
test "capture-avoiding substitution on Expr" {
let x = @core.Name::new("x")
let y = @core.Name::new("y")
let e = Fun(y, Call(Sym("+"), [Var(x), Var(y)]))
let s = @substitution.GenericSubstitution::singleton(x, Var(y))
let y1 = @core.Name::new("y_1")
assert_eq(s.apply_once(e), Fun(y1, Call(Sym("+"), [Var(y), Var(y1)])))
}
部分求值
代入已知的值,其余部分保持符号形式:
test "partial evaluation on Expr" {
let x = @core.Name::new("x")
let y = @core.Name::new("y")
let e = Call(Sym("*"), [Var(x), Var(y)])
let known = @substitution.GenericSubstitution::singleton(x, Num(3))
assert_eq(known.apply_once(e), Call(Sym("*"), [Num(3), Var(y)]))
}
用改写规则化简
针对你自己的类型编写规则,让泛型遍历找出它们适用的位置:
fn fold_add(e : Expr) -> Expr? {
match e {
Call(Sym("+"), [Num(a), Num(b)]) => Some(Num(a + b))
_ => None
}
}
test "constant folding on Expr" {
let x = @core.Name::new("x")
let e = Call(Sym("*"), [Var(x), Call(Sym("+"), [Num(1), Call(Sym("+"), [Num(2), Num(3)])])])
let rule = @rewrite.RuleName::unsafe_new("fold_add")
assert_eq(
@rewrite.generic_normalize(e, rule, fold_add, 10),
NormalForm(term=Call(Sym("*"), [Var(x), Num(6)]), steps=2),
)
match @rewrite.generic_top_down_once(e, rule, fold_add) {
Reduced(path~, ..) =>
assert_eq(path.to_array(), [@rewrite.ApplyArgument(1), @rewrite.ApplyArgument(1)])
NoStep => fail("expected a step")
}
}
该路径表示:* 的第二个参数,然后是外层 + 的第二个参数。
重命名绑定子
generic_alpha_rename_bound 重命名绑定子体中的变量,并拒绝已被使用的名字:
test "rename a Fun parameter" {
let x = @core.Name::new("x")
let y = @core.Name::new("y")
let z = @core.Name::new("z")
let body = Call(Sym("+"), [Var(x), Var(y)])
assert_true(@syntax.generic_alpha_rename_bound(body, x, y) is None)
assert_eq(
@syntax.generic_alpha_rename_bound(body, x, z),
Some(Call(Sym("+"), [Var(z), Var(y)])),
)
}
进阶
测试适配器定律
只有当 project 与构造器保持一致(即适配器设计中的视图定律)时,泛型算法才是正确的。请在有代表性的节点上测试它们:
fn rebuild(e : Expr) -> Expr {
match @syntax.BindingSyntax::project(e) {
Opaque => e
Variable(x) => @syntax.BindingSyntax::variable(x)
Apply(f, args) => @syntax.BindingSyntax::apply(f, args)
Bind(x, body) => @syntax.BindingSyntax::bind(x, body)
}
}
test "view laws" {
let x = @core.Name::new("x")
let samples = [
Num(1),
Sym("+"),
Var(x),
Call(Sym("+"), [Var(x), Num(2)]),
Fun(x, Var(x)),
]
for e in samples {
assert_eq(rebuild(e), e)
if @syntax.BindingSyntax::project(e) is Opaque {
assert_eq(@syntax.generic_free_variables(e).length(), 0)
}
}
}
一个视图情形下的多个运算符
所有调用都投影为 Apply,运算符随头部一起传递,因此 apply(head, args) 可以重建任何调用。如果你的 AST 有 Add(a, b) 和 Mul(a, b) 这样彼此独立的节点种类,就给它们一个能标识种类的头部(就像这里的 Sym);否则 apply 无法知道应重建哪种节点。
桥接你的变量类型
如果你的 AST 有自己的变量类型,请在 project 中将其单射地映射到 @core.Name(不同的变量对应不同的名字),并在 variable 中映射回来。
常见陷阱
- 不透明节点中的变量。 投影为
Opaque的节点从不被搜索。如果其中含有变量,代换会遗漏它们。 - 用点语法调用 trait 方法。 请写成
@syntax.BindingSyntax::project(e);这些方法形式没有被提升。 - 在
apply中丢失节点种类。 如果两种节点都投影为Apply,而头部又不能区分它们,重建就会改变 AST。 - 期望重复代换。
apply_once只代换一次;如果替换项中提到了定义域中的名字,请自行迭代。
后续步骤
- 适配器设计:视图定律及其为何足够。
- syntax API:
BindingSyntax与泛型分析。 - 代换教程和改写教程:这里用到的算法。