アジョブジ星通信

日常系バンザイ。

Rustの構文拡張をつくろう

最近書くネタがなくて困ってますが、 Rust をいじり始めたので、そろそろなにか書いておきたいなと。

今回は強力な構文拡張機能(syntax extension)を使っていろいろと変な構文をぶち込む方法を紹介します。なお、サクッとマクロを作りたいだけな場合は macro_rules! を使えばいいだけなので、こんなアホみたいなことはする必要ありません。

対象バージョン

$ rustc --version
rustc 1.0.0-nightly (4be79d6ac 2015-01-23 16:08:14 +0000)

1.0 alpha が出たからもう破壊的変更なんて起こらないだろうと思ってましたが、そんなことはなかったのでいつ動かぬコードになってもおかしくはありません。

公式ドキュメントの紹介

大体これ読めば僕の記事なんて必要ないと思うんですよ。

コンパイラプラグインとして動くクレートをつくる

1. Cargo.toml の設定

ライブラリをコンパイラプラグインとして使えるようにするには、ライブラリの種類を dylib にする必要があります。

[package]
# ……

[lib]
# ……
crate-type = ["dylib"]
plugin = true #なくても動く

2. plugin_registrar の実装

プラグインでは #[plugin_registrar] デコレーターが指定された関数が最初に実行されます。まずはそれの実装から。

#![feature(plugin_registrar)]

extern crate rustc;
use rustc::plugin::Registry;

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    println!("みんなで行った");
}

ポイントとして、 #![feature(plugin_registrar)] がないと試験的機能なためエラーになるのと、関数は pub が指定されていないとエラーになります。

3. プラグインを使用する

利用する側のクレートでマクロクレートを依存に加え、 #[plugin] を指定して extern crate で読み込みます。

#![feature(plugin)]

#[plugin]
#[no_link] //プラグイン以外の用途で使わないため
extern crate macros;

fn main() {
    println!("千葉滋賀佐賀");
}

これを実行すると

$ cargo run
   Compiling macros v0.0.1 (file:///home/azyobuzin/repo/syntaxext)
macros/src/lib.rs:3:1: 3:20 warning: use of unstable item, #[warn(unstable)] on by default
macros/src/lib.rs:3 extern crate rustc;
                    ^~~~~~~~~~~~~~~~~~~
macros/src/lib.rs:7:25: 7:28 warning: unused variable: `reg`, #[warn(unused_variables)] on by default
macros/src/lib.rs:7 pub fn plugin_registrar(reg: &mut Registry) {
                                            ^~~
   Compiling syntaxext v0.0.1 (file:///home/azyobuzin/repo/syntaxext)
みんなで行った
     Running `target/syntaxext`
千葉滋賀佐賀

と、コンパイル時に plugin_registrar が実行されることがわかります。

#[plugin] デコレーターの引数を読み取る

reg.args() で #[plugin] に指定された情報を取得できます。というと想像がつきにくいですが、 #[plugin(foo, bar)]#[plugin="homu"] といったように書くことができます。

Show で出力するようにしておいて、適当に引数にぶちこむとこんな感じに。

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    println!("{:?}", reg.args());
}
#[plugin="三日連続"]
   Compiling syntaxext v0.0.1 (file:///home/azyobuzin/repo/syntaxext)
Spanned { node: MetaNameValue(plugin, Spanned { node: LitStr(三日連続, CookedStr), span: Span { lo: BytePos(30u32), hi: BytePos(44u32), expn_id: ExpnId(4294967295u32) } }), span: Span { lo: BytePos(23u32), hi: BytePos(45u32), expn_id: ExpnId(4294967295u32) } }

args って名前なのに返ってくる値はデコレーターの AST ノードそのものじゃねーかというツッコミがしたくて仕方ない。

使うときはパターンマッチでも使ってがんばってください。

あー長かった。この先はそれぞれの SyntaxExtension の種類ごとに説明していきます。

NormalTT

NormalTT は通常のマクロ、 homu!() を表します*1。この拡張を登録するには、

reg.register_macro("homu", expand_homu);

または

//extern crate syntax;
//use syntax::ext::base;
//use syntax::parse::token;

let expander: base::MacroExpanderFn = expand_homu;
reg.register_syntax_extension(
    token::intern("homu"),
    base::NormalTT(box expander, None)
);

と書きます。

expand_homu の定義は

//use syntax::ast;
//use syntax::codemap::Span;
fn expand_homu(cx: &mut base::ExtCtxt, sp: Span, args: &[ast::TokenTree])
    -> Box<base::MacResult + 'static>

となります。

それぞれの引数の意味は以下の通りです。

cx*2
パース状況やエラー・警告の出力用オブジェクト ExtCtxt
sp
この構文拡張の開始から終わりまでの位置情報を表す Span。エラー出力時に使うと便利かも
args
NormalTT の括弧開きの後ろから括弧閉じの前までのトークンの一覧を表す &[TokenTree]。カンマ区切りを認識してくれるわけではなく、生のデータが入ってくることに注意してください。

戻り値にはアイテム(グローバルに置けるもの)や式を表す MacResult を使います。トレイトなので自分で実装してもいいのですが MacExpr, MacItems(struct, trait など),
MacPat(パターン, 引数名)が用意されているので通常はこれらを使います。またエラー時には cx.span_err でエラー情報をコンパイラーに渡し、
DummyResult を返します。

それでは簡単な実装例として、 homu!(grief_seed)"grief_seed" として扱えるようにしてみます。ブログにコードをそのまま貼り付けるとごちゃごちゃするので gist においておきます。そのまま clone すれば cargo run で実行できるはずです。
https://gist.github.com/azyobuzin/bfa77d9cc3ab25a3843b

実行するとこうなります:

grief_seed

かなり手抜きな実装になってしまいましたが、コードを文字列でつくって cx.parse_expr に食わせるのが、エラーメッセージもわかりやすくなって扱いやすかったりします。

IdentTT

IdentTT は NormalTT の括弧の前に識別子を入れられるものです。これには登録用の便利関数は用意されていないので、関数を
IdentMacroExpanderFn に代入してから渡さないとエラーによって殺されます。いい書き方ないかなぁ…。

let expander: base::IdentMacroExpanderFn = expand_homu;
reg.register_syntax_extension(
    token::intern("homu"),
    base::IdentTT(box expander, None)
);

展開関数の定義はこのようにします。

fn expand_homu(cx: &mut base::ExtCtxt, sp: Span, ident: ast::Ident,
    args: Vec<ast::TokenTree>) -> Box<base::MacResult + 'static>

引数はほとんど NormalTT と同じですが、括弧前の識別子を表す ident が追加されています。あと argsVec なのはなんでなんだろう。

試しにメンバーがすべて大文字になる enum を作ってみます。
https://gist.github.com/azyobuzin/507010037e6945cc1b69

MECCHA, SOREHODODEMONAI

TokenTree をそのまま解析するには少し面倒な構文なので cx.new_parser_from_tts(&args[])Parser を作成しました。 Parser は特に Result を返したりしませんが、エラーレポートは勝手に出力されるのでなかなか楽です。

Decorator

Decorator はアイテムに #[homu] の形でくっつき、任意のアイテムをそこに追加できるというものです。 Decorator は展開関数を普通に突っ込んでもエラーにならないので汚いコードにならないはず(だから関数の型も用意されていないっぽい)。

reg.register_syntax_extension(
    token::intern("homu"),
    base::Decorator(box expand_homu)
);

展開関数の定義

fn expand_homu(cx: &mut base::ExtCtxt, sp: Span, meta_item: &ast::MetaItem,
    item: &ast::Item, mut push: Box<FnMut(P<ast::Item>)>)

新しい引数の説明

meta_item
このデコレーターを表す MetaItem。使い方は #[plugin] の引数を取得した時と同じです。
item
このデコレーターが指定されているアイテムを表す Item
push
新しいアイテムを出力するための関数。 mut をつけ忘れると呼び出せないので注意。

例として、 fmt::String*3 を簡単に実装できるデコレーターをつくってみます。
https://gist.github.com/azyobuzin/ecdae200c7469e1bb16c

ムクホーク: Lv 100

#![feature(quote)] という便利ツール

quote_tokens!quote_item! というのを使っていますが、これは第一引数に ExtCtxt を取り、その後ろのコードを TokenTree として扱い、 quote_tokens! では Vec<TokenTree>を、 quote_item! では Option<P<Item>> を返します。$ から始まる識別子は該当する変数をそのまま埋め込んでくれます(ToTokens を実装している必要あり)。

同様に quote_ty!, quote_method!, quote_pat!, quote_arm!, quote_stmt! が存在しています。

詳しくは libsyntax のソースに定義展開関数が入っているので読んでみてください。ついでに僕に詳細を教えてください。

Modifier と MultiModifier

Modifier は Decorator と同じ形でアイテムにくっつき、そのアイテム自体を加工するものです。 MultiModifier はアイテム以外に traitimpl 内のメソッドや型も加工することが出来ます。

登録は Decorator のように普通に突っ込めます。

reg.register_syntax_extension(
    token::intern("foo"),
    base::Modifier(box expand_foo)
);

展開関数は Modifier では

fn expand_foo(cx: &mut base::ExtCtxt, sp: Span,
    meta_item: &ast::MetaItem, item: P<ast::Item>) -> P<ast::Item>

MultiModifier では

fn expand_bar(cx: &mut base::ExtCtxt, sp: Span,
    meta_item: &ast::MetaItem, item: base::Annotatable) -> base::Annotatable

となります。 item 引数は、くっついているアイテムを表す P<Item>
Annotatable です。このため MultiModifier では大量の enum を相手にしなければならないので対応の幅を広げようとすると結構厄介なコードになりそうです。

というわけで、両方の例を一気に用意しました(面倒になった)。 IdentTT でやったすべて大文字の enum を作るやつの Modifier 版と、 #[instance_method] をつけると勝手に引数に self を追加してくれる MultiModifier です。
https://gist.github.com/azyobuzin/dfc2e57c0980650a5d37

実行結果

MECCHA
SOREHODODEMONAI

サンプルつくるの飽きたのでそんなに目新しいことはしてないはずです。

まとめ

Rust はソースコードがドキュメントなので頑張ってソース読みながら言語仕様を把握していきましょう。こちらからは以上です

*1:括弧は [], {} でも同じように扱われます。識別する方法があるかと探しているのですがいまだ見つからず。

*2:ecx と表記されることも

*3:もうすぐ fmt::Display になりそう