Rustでコンパイラプラグインを書いてみる


rustでコンパイラプラグインを書きくなったため色々試行錯誤した記録

1.22.0-nightly で確認
nightly API使ってる(?)ので要注意

## 準備

```bash
$ cargo new sample-plugin
```

```:Cargo.toml
[package]
name = "sample_plugin"
version = "0.1.0"
authors = ["UEKI Masaru <masaru.ueki@itage.co.jp>"]

[lib]
name = "sample_plugin"
path = "src/lib.rs"
plugin = true

[dependencies]
```

`plugin = true`が無いとエラーが出る。

## 文字列を返す

まずは単純に文字列を返す例

```rust:src/lib.rs
#![crate_type="dylib"]
#![feature(plugin_registrar, rustc_private)]

extern crate syntax;
extern crate syntax_pos;
extern crate rustc;
extern crate rustc_plugin;

use syntax::ast;
use syntax::tokenstream::TokenTree;
use syntax::ext::base::{ExtCtxt, MacResult, MacEager};
use syntax::ext::build::AstBuilder;
use syntax_pos::Span;
use syntax_pos::symbol::Symbol;
use rustc_plugin::Registry;

fn expand_sample(cx: &mut ExtCtxt, sp: Span, _args: &[TokenTree])
                 -> Box<MacResult + 'static> {
    MacEager::expr(cx.expr_lit(sp, ast::LitKind::Str(Symbol::intern("hoge"), ast::StrStyle::Cooked)))
}

#[plugin_registrar]
pub fn plugin_registrar(reg: &mut Registry) {
    reg.register_macro("sample", expand_sample);
}
```

`expand_sample()`がプラグイン本体。`plugin_registrar()`では`expand_sample`を`sample!()`として登録している。

プラグインではAST(抽象構文木)を構築して返す必要がある。
ここでは"hoge"という文字リテラルを`expr_lit()`を使って構築している。

テストを書いて試してみる

```rust:tests/sample_plugin_tests.rs
#![feature(plugin)]
#![plugin(sample_plugin)]

#[test] fn sample_plugin_test() {
    let hoge = sample!();
    assert_eq!(hoge, "hoge");
}
```

`let hoge = sample!();`の部分がコンパイル時に`let hoge = "hoge;"`と置き換わる想定。

`$ cargo test`で確かに動作していることが確認できる。

## ブロックを定義する

次はブロックを定義してみる。今度は`sample!()`が

```rust
{ "hoge" }
```

と置き換えられる想定。

```rust:src/lib.rs
fn expand_sample(cx: &mut ExtCtxt, sp: Span, _args: &[TokenTree])
                 -> Box<MacResult + 'static> {
    let expr = cx.expr_lit(sp, ast::LitKind::Str(Symbol::intern("hoge"), ast::StrStyle::Cooked));
    let stmt = cx.stmt_expr(expr);
    MacEager::expr(cx.expr_block(cx.block(sp, vec!(stmt))))
}
```

`$ cargo test` で確認できる。

## `let`とメソッド呼出

今度は`sample!()`が

```rust
{
  let h = "hoge".to_string();
  h
}
```

と置き換えられるようにしてみる。

```rust:src/lib.rs
fn expand_sample(cx: &mut ExtCtxt, sp: Span, _args: &[TokenTree])
                 -> Box<MacResult + 'static> {
    let ident_h = Ident::from_str("h");
    let hoge_lit = cx.expr_lit(sp, ast::LitKind::Str(Symbol::intern("hoge"), ast::StrStyle::Cooked));

    let stmt_let_h = cx.stmt_let(sp, false, ident_h, cx.expr_method_call(sp, hoge_lit, Ident::from_str("to_string"), vec!()));

    let stmt_ident = cx.stmt_expr(cx.expr_ident(sp, ident_h));
    MacEager::expr(cx.expr_block(cx.block(sp, vec!(stmt_let_h, stmt_ident))))
}
```

`expr_method_call()`によって`"hoge"`に対して`to_string()`を呼出し、`stmt_let()`により結果を`h`に束縛している。

テストはそのままでOK

## 別プラグインの呼出

試しに別のプラグインを呼び出してみたが難なく動いた。
今回は

```rust
{
  let h = "hoge".to_string();
  h.push_str(include_str!("fuga.txt"));
  h
}
```

と置き換わる想定

```:src/fuga.txt
fuga
```

```rust:src/lib.rs
fn expand_sample(cx: &mut ExtCtxt, sp: Span, _args: &[TokenTree])
                 -> Box<MacResult + 'static> {
    let fuga_str = include_str!("fuga.txt");

    let ident_h = Ident::from_str("h");
    let hoge_lit = cx.expr_lit(sp, ast::LitKind::Str(Symbol::intern("hoge"), ast::StrStyle::Cooked));

    let stmt_let_h = cx.stmt_let(sp, true, ident_h, cx.expr_method_call(sp, hoge_lit, Ident::from_str("to_string"), vec!()));

    let fuga_lit = cx.expr_lit(sp, ast::LitKind::Str(Symbol::intern(&fuga_str[0..4]), ast::StrStyle::Cooked));

    let stmt_push_str = cx.stmt_expr(cx.expr_method_call(sp, cx.expr_ident(sp,ident_h), Ident::from_str("push_str"),
                                                         vec!(fuga_lit)));

    let stmt_ident = cx.stmt_expr(cx.expr_ident(sp, ident_h));
    MacEager::expr(cx.expr_block(cx.block(sp, vec!(stmt_let_h, stmt_push_str, stmt_ident))))
}
```

本当は`include_str!()`ではなく`expand_include_str()`を呼びたかったのだが、argsの構築方法がわからなかったため上のような形になった。

**追記(2017/10/18): `include_str!()`直呼びで問題ないと思ったが、パスがlib.rsからの相対パスになってしまうため、本来の想定とはやはり異なる。**

```rust:tests/sample_plugin_tests.rs
#![feature(plugin)]
#![plugin(sample_plugin)]

#[test] fn sample_plugin_test() {
    let hoge = sample!();
    assert_eq!(hoge, "hogefuga");
}
```

## quoteマクロを使う

いままではASTを関数に地道に構築してきたがquoteマクロを使うともっと簡単にASTを作ることが出来る。
例えば「letとメソッド呼出」の例であれば

```rust:src/lib.rs
#![feature(plugin_registrar, quote, rustc-private)] // <- quote追加

fn expand_sample(cx: &mut ExtCtxt, sp: Span, _args: &[TokenTree])
                 -> Box<MacResult + 'static> {
    MacEager::expr(quote_expr!(cx, {
        let h = "hoge".to_string();
        h
    }))
}
```

このようにそのまま書き下すことが出来る。quoteマクロの使い方についてはrustソースコードの「src/test/run-pass-fulldeps/quote-tokens.rs」を見てもらうと感じがつかめると思う。

# おわりに

[ドキュメント](https://doc.rust-lang.org/beta/unstable-book/language-features/plugin.html)ではASTの作り方についてはあまり解説されていないので、rustソースのsrc/libsyntax/ext/build.rsにある関数でgrepしたりして試行錯誤した。
この記事ではコンパイラプラグインでなくても出来るような例しかあげられなかったが、コンパイラプラグイン作成の入り口として役に立てれば幸いである。