syntax チュートリアル
このチュートリアルでは、Term[T] で束縛子を持つ項を構築し、表示し、どの変数が自由かを調べ、束縛変数の名前を除いて項を比較し、捕獲なしに変数をリネームする。最後に、BindingSyntax を実装する任意の AST で動く解析を書く。
クイックスタート
moon add Luna-Flow/type_theory@0.2.0
import {
"Luna-Flow/type_theory/core",
"Luna-Flow/type_theory/syntax",
}
恒等関数 は自由変数を持たず、 は 1 つ持つ。
test "quick start: free variables" {
let x = @core.Name::new("x")
let y = @core.Name::new("y")
let id : @syntax.Term[Int] = Bind(x, Variable(x))
let k : @syntax.Term[Int] = Bind(x, Variable(y))
assert_eq(@syntax.free_variables(id).length(), 0)
assert_true(@syntax.free_variables(k).contains(y))
}
日常的な作業
小さなヘルパーで項を構築する
@core.Name::new をあちこちに書くと煩雑になる。ヘルパーを一度定義しておく。
fn v(name : String) -> @syntax.Term[Int] {
@syntax.Variable(@core.Name::new(name))
}
fn lam(name : String, body : @syntax.Term[Int]) -> @syntax.Term[Int] {
@syntax.Bind(@core.Name::new(name), body)
}
fn app(f : @syntax.Term[Int], args : Array[@syntax.Term[Int]]) -> @syntax.Term[Int] {
@syntax.Apply(f, args)
}
test "the K combinator" {
let k = lam("x", lam("y", v("x")))
assert_true(k is Bind(_, Bind(_, Variable(_))))
assert_eq(app(k, [v("a")]), @syntax.Apply(k, [v("a")]))
}
項を表示する
Term は独自のテキスト形式を持たない。適切な記法はそれがエンコードする言語によって決まるからである。ラムダ記法のプリンタは短い再帰関数で書ける。
fn show(t : @syntax.Term[Int]) -> String {
match t {
Value(n) => n.to_string()
Variable(x) => x.text()
Apply(f, args) => {
let parts = [show(f), ..args.map(show)]
"(" + parts.join(" ") + ")"
}
Bind(x, body) => "λ" + x.text() + ". " + show(body)
}
}
test "print λf. f 1" {
let f = @core.Name::new("f")
let t : @syntax.Term[Int] = Bind(f, Apply(Variable(f), [Value(1)]))
inspect(show(t), content="λf. (f 1)")
}
束縛された名前を除いて項を比較する
== は項を字面どおりに比較する。alpha_equal は束縛変数の名前を無視し、ほとんどの場合こちらが望む比較である。
test "λx. x and λy. y" {
let x = @core.Name::new("x")
let y = @core.Name::new("y")
let a : @syntax.Term[Int] = Bind(x, Variable(x))
let b : @syntax.Term[Int] = Bind(y, Variable(y))
assert_false(a == b)
assert_true(@syntax.alpha_equal(a, b))
}
自由変数を安全にリネームする
で x を y にリネームしたとき、 になってはならない。rename_free はまず束縛子をリネームする。
test "renaming avoids capture" {
let x = @core.Name::new("x")
let y = @core.Name::new("y")
let t : @syntax.Term[Int] = Bind(y, Apply(Variable(x), [Variable(y)]))
let r = t.rename_free(@core.Renaming::singleton(x, y))
let y1 = @core.Name::new("y_1")
let expected : @syntax.Term[Int] = Bind(y1, Apply(Variable(y), [Variable(y1)]))
assert_eq(r, expected)
}
自由な x は y になり、束縛子は y_1 になったので、新しい y は自由なままである。
定数の型を変える
map_values は定数を変換し、束縛構造には手を触れない。ここでは整数リテラルが浮動小数点リテラルになる。
test "map Int constants to Double" {
let x = @core.Name::new("x")
let t : @syntax.Term[Int] = Bind(x, Apply(Variable(x), [Value(2)]))
let d : @syntax.Term[Double] = t.map_values(n => n.to_double())
assert_true(d is Bind(_, Apply(_, [Value(2.0)])))
}
さらに進んで
束縛を扱うすべての AST に対する解析を書く
BindingSyntax に対して書いた関数は、Term[T] にも、このトレイトを実装する任意の下流 AST にも使える。次の関数は束縛子を数える。
fn[N : @syntax.BindingSyntax] count_binders(node : N) -> Int {
match @syntax.BindingSyntax::project(node) {
Opaque | Variable(_) => 0
Apply(head, args) =>
args.fold(init=count_binders(head), (acc, a) => acc + count_binders(a))
Bind(_, body) => 1 + count_binders(body)
}
}
test "count binders generically" {
let x = @core.Name::new("x")
let t : @syntax.Term[Int] = Bind(x, Apply(Bind(x, Variable(x)), [Value(0)]))
assert_eq(count_binders(t), 2)
}
adapter チュートリアルでは独自の AST に BindingSyntax を実装する。そうすれば count_binders、汎用の代入、汎用の書き換えがすべて適用できる。
束縛子を手動でリネームする
部品から束縛子を構築する場合、alpha_rename_bound はその変数の名前を変更し、本体ですでに使われている名前は拒否する。
test "rename the variable of a binder" {
let x = @core.Name::new("x")
let y = @core.Name::new("y")
let z = @core.Name::new("z")
let body : @syntax.Term[Int] = Apply(Variable(x), [Variable(y)])
assert_true(body.alpha_rename_bound(x, y) is None)
match body.alpha_rename_bound(x, z) {
Some(renamed) => {
let before : @syntax.Term[Int] = Bind(x, body)
let after : @syntax.Term[Int] = Bind(z, renamed)
assert_true(@syntax.alpha_equal(before, after))
}
None => fail("z is unused")
}
}
よくある落とし穴
==は α同値ではない。 代入や正規化の結果にはy_1のようにリネームされた束縛子が含まれうる。alpha_equalで比較すること。- 値の内部の変数。
Value(T)の中身は決して調べられない。定数が変数を含むなら、Termで包むのではなく、独自の AST にBindingSyntaxを実装すること。 - 束縛子に対して
alpha_rename_boundを呼ぶ。 これは本体を期待する。Bind(x, body).alpha_rename_bound(...)ではなくbody.alpha_rename_bound(x, z)と書く。前者ではリネームすべき自由なxが見つからない。 - メソッド形式のトレイト呼び出し。
term.project()は非推奨である。@syntax.BindingSyntax::project(term)と書くこと。
次のステップ
- syntax API:すべての関数とその契約。
- syntax の設計:α同値と捕獲回避リネームの背後にある定義と証明。
- substitution チュートリアル:変数を項で置き換える。