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
が追加されています。あと args
が Vec
なのはなんでなんだろう。
試しにメンバーがすべて大文字になる 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 はアイテム以外に trait
や impl
内のメソッドや型も加工することが出来ます。
登録は 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 はソースコードがドキュメントなので頑張ってソース読みながら言語仕様を把握していきましょう。こちらからは以上です。