こちらの記事は Rustマクロ冬期講習アドベントカレンダー 24日目の記事です!
アドカレまとめ記事はこちら!: Rustマクロ作成チートシート!
14日目の記事、Rustマクロ フラグメント指定子(ident, expr, item, stmt...)なんもわからん!となったので判別器作った #Rust - Qiita にて、Rustコードを与えるとフラグメント指定子ごとに色分けしてくれる 判別器 を作成した話をしました。
この記事では話していませんでしたが、実はこのツール、三種の神器の一つの syn
を利用したものです。というわけで、その話をしたいと思います!
本記事で伝えたいこと
syn クレートは手続きマクロ以外でもカジュアルに使えます! Rustの構文解析をしたいならば次の機能が便利かもしれません。
-
syn::parse_str
- 対象構文要素(
syn::parse::Parse
トレイトを実装した型)への文字列のパースを試みることができます
- 対象構文要素(
-
proc_macro2::Span
- 構文要素のソースコード中での位置情報が格納されています。ただし条件つきなので要ドキュメント確認
-
syn::visit::Visit
- 構文要素を再帰的に探索するために使用できます
- 他にも
syn::fold
モジュールも再帰的な処理に使えるかもです
例えばマクロのテストをする際に使えそうですね
カラクリ・処理順序
本節ではこの判別器の仕組みを軽く解説します。
対象ソースコード:
-
coloring_common/src/lib.rs
- お試しでCLI版も用意していたためその名残で別クレートになっています
- coloring_wasm/src/lib.rs
構文要素を判定する機能は syn::parse_str
を基軸に作成し、WebAssemblyを利用して呼び出すようにしました。(ちなみにフロントエンドは色々検討したのですが結局React + muiに落ち着きました... )
処理順序になります。
(1) 様々な構文要素で parse_str
を試す
- マッチしたものについてリストアップ。サイトでは左側の "Whole Matcher" 欄にてマッチした要素の情報を表示
(2) マッチした構文要素について、再帰的に構文要素を調べる
- 再帰処理には
syn::visit::Visit
を利用 - ある構文要素
S
がヒットした際、<S as Spanned>::span
を調べ、byte_range
メソッドでソースコード位置(Range<usize>
)を取得。ハッシュマップHashMap<構文要素列挙体, Vec<Range<usize>>>
に格納
(3) 然るべき加工(CLIならASCIIエスケープシーケンスによる着色。WebAssemblyならマークアップ)を施して表示
オプション等の処理を含めるともう少しいろいろ話せることはありますが、軸となる処理は parse_str
があったおかげでそこまで裏技的なことをせずに実装することができました。またもう一つ特に 悩むだろうなと予想されたのは、親構文要素から子構文要素を探索していく再帰処理部分 でしたが、syn::visit::Visit
トレイト があったおかげで案外苦労せずに実装することができました!
synクレートにおんぶに抱っこになれば、この手のツールは簡単に作れるということですね...他にも色々活用できそうです。
Visit
トレイトはどういう仕組み?
残りのハンズオンパートでは Visit
トレイトの話はしないのですが、仕組みが面白かったのでここで少し解説したいと思います。
visit
モジュールにはこの Visit
トレイトと大量の visit_xxx
関数が定義されており、 Visit
トレイトが提供するメソッド Visit::visit_xxx
のデフォルト実装にてこの関数 visit_xxx
が呼ばれている構造になっています。
trait Visit<'ast> {
// ...省略...
fn visit_xxx(&mut self, i: &'ast crate::Xxx) {
visit_xxx(self, i);
}
// ...省略...
}
visit_xxx
関数は、自身より小さい要素の関数 visit_yyy
を呼び出すようになっており(例えば visit_stmt
なら式の時に visit_expr
を呼ぶなど)、この仕組みによって子要素の方にまで再帰的にアクセスできるようになっています!
pub fn visit_xxx<'ast, V>(v: &mut V, node: &'ast crate::Xxx)
where
V: Visit<'ast> + ?Sized,
{
// ...省略... crate::Yyy型のyを得る
v.visit_yyy(&y);
}
フックとして、 Visit
トレイトのデフォルト実装に処理を追加してあげれば、 Xxx
要素に対するすべての探索にフック処理が入ります!
struct Counter {
xxx_counter: usize,
}
impl<'ast> Visit<'ast> for Counter {
fn visit_xxx(&mut self, i: &'ast syn::Xxx) {
// フック処理
self.xxx_counter += 1;
syn::visit::visit_xxx(self, i);
}
}
playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=96c6b7abc292aa287b8d682874b4ec2f
Visit
トレイトが採用しているような仕組みは、再帰的に探索する時にフックを仕掛けたい時に使えるパターンみたいですね...覚えておくと便利かもしれません(多分)
軽くハンズオン
さらなる詳細について工夫した点のみピックアップしようと思ったのですが、却って機能説明がわかりにくくなりそうだったため、「Whole Matcher機能」すなわち「与えられた文字列がRust構文要素の何に対応するかを調べる」機能だけハンズオンで解説することにしました!というわけでLet's 実装!
クレート用意
まずはクレートを用意します。
crate new match_frags
「フラグメント指定子(Fragment Specifiers)」にマッチするかどうか、ということで match_frags
という名前にしてみました。
次に必要なクレートを追加していきます。もちろん syn
クレートと、今回はCLIアプリにしたいので clap
クレートを入れます。
cargo add syn --features extra-traits --features full
cargo add clap --features derive
入れたfeatureの捕捉です。
-
syn
クレート- featuresは使わないかもしれないですが適当に入れています
-
--features extra-traits
:Debug
等を利用できるようになります -
--features full
: 「アレ...?この機能ない...」となりがちなので入れちゃいます- 裏を返せば軽量にしたければ省きやすい構造になっていてよいですね
-
clap
クレート-
--features derive
: 宣言的に引数を書けるようになるので必須です
-
コマンドライン引数で文字列入力を受ける
確認したい対象となる文字列はコマンドライン引数で受けることにします。
use clap::Parser;
#[derive(Parser)]
struct Input {
/// 構文解析対象文字列
parse_target: String,
}
fn main() {
let Input { parse_target } = Input::parse();
println!("{}", parse_target);
}
受けた文字列をそのまま出力するだけのプログラムです。
$ cargo run -q -- 'hoge(1 + 1)'
hoge(1 + 1)
syn::parse_str
でマッチ!
歌ならもうサビの部分です、引数で取り入れた解析対象文字列 parse_target
が構文要素にマッチするか調べます!
とりあえず、式( syn::Expr
)の場合は以下のような感じです。
if syn::parse_str::<syn::Expr>(&parse_target).is_ok() {
println!("{} is Expr", parse_target);
}
// 省略
fn main() {
let Input { parse_target } = Input::parse();
- println!("{}", parse_target);
+ if syn::parse_str::<syn::Expr>(&parse_target).is_ok() {
+ println!("{} is Expr", parse_target);
+ }
}
これを全部の構文要素(順不同で syn::File
、syn::Block
、syn::Expr
、syn::Ident
、syn::Item
、syn::Lifetime
、syn::Lit
、syn::Meta
、syn::Path
、syn::Stmt
、syn::Type
、syn::Visibility
...多い!)でマッチするか確認していきます。プログラムの変更について柔軟性を持たせるため、マッチした要素を文字列として動的配列に追加することし、面倒なので宣言マクロを利用することにします!
use clap::Parser;
#[derive(Parser)]
struct Input {
/// 構文解析対象文字列
parse_target: String,
}
fn main() {
let Input { parse_target } = Input::parse();
let mut parsable_frags = Vec::new();
macro_rules! check_parsable {
($frag:ty) => {
if syn::parse_str::<$frag>(&parse_target).is_ok() {
let s = stringify!($frag).to_string();
parsable_frags.push(s);
}
};
}
check_parsable!(syn::File); // 正確には構文要素ではないが、ついでに確認
check_parsable!(syn::Item);
check_parsable!(syn::Block);
check_parsable!(syn::Stmt);
check_parsable!(syn::Expr);
check_parsable!(syn::Type);
check_parsable!(syn::Path);
check_parsable!(syn::Visibility);
check_parsable!(syn::Ident);
check_parsable!(syn::Lifetime);
check_parsable!(syn::Lit);
check_parsable!(syn::Meta);
println!(
"parsable fragments for `{}`: {:?}",
parse_target, parsable_frags
);
}
実はこれでもう完成です、与えられた文字列がRust構文要素の何に該当するかを判別できます!
$ cargo run -q -- 'hoge(1 + 1)'
parsable fragments for `hoge(1 + 1)`: ["syn::Expr", "syn::Meta"]
$ cargo run -q -- 'if a { b } else { c }'
parsable fragments for `if a { b } else { c }`: ["syn::Stmt", "syn::Expr"]
$ cargo run -q -- 'fn hoge() { /*...*/ }'
parsable fragments for `fn hoge() { /*...*/ }`: ["syn::File", "syn::Item", "syn::Stmt"]
そんなに難しい部分はなかったかと思います。 syn::parse_str
が便利すぎた...
macro_rules
スコープの小ネタ
地味に今まで紹介していなかった小ネタを今回利用しています。
Rustマクロの事前知識③「マクロのマナー 衛生性(健全性)」 の結論としては、マクロ内で宣言されていない変数へのアクセスはマナー的にやめたほうが良いのでした。そして実際、大抵の場合これはエラーになります。
しかし、実は macro_rules
でマクロを宣言しているスコープ内に定義されている変数に関しては、その限りではなく普通にアクセス出来ます!
/* // こちらだとコンパイルエラー
macro_rules! p {
() => { println!("{}", hoge); };
}
*/
fn main() {
let hoge = "hoge!";
// ここで定義するとhogeが見える
macro_rules! p {
() => { println!("{}", hoge); };
}
p!();
}
というわけで、一々マクロに parse_target
や parsable_frags
変数を渡すのは面倒だったので、 main
関数内で定義するようにしていました。
まとめ・所感
syn::parse_str
を利用した軽いハンズオンを通して、 syn クレートは手続きマクロだけでなくRust構文解析にまつわるツール作成にも用いられることを示しました。
syn
は手続きマクロ作成専用というより、Rust構文全般のパースや操作に便利なクレートです。こうした多用途性を意識しておくと、手続きマクロの作成や応用を考えるときの解像度も上がるはずです...というわけで、皆さんもぜひ syn
クレートを利用して面白いツールを作ってみてほしいです!
ここまで読んでいただきありがとうございました!