こちらの記事は Rustマクロ冬期講習アドベントカレンダー 16日目の記事です!1
Rustの宣言マクロ作成を手助けするフラグメント指定子判別器を作った話です。
皆さんはRustでマクロを書いて活用していますか...?意外と(?)面倒ですよね。筆者は特に宣言マクロ( macro_rules!
によるマクロ)を書く際、 ($hoge:???) => { ... }
の ???
部分がいつもわからなくなります
macro_rules! my_awesome_macro {
($hoge:???) => {
// ...
};
}
$hoge:???
のことをメタ変数と言い、そして ???
は フラグメント指定子 と呼ばれるもので、宣言マクロのパターンマッチにおいてRustコードの どんな構文パーツを期待するか を表すのに使われ、 14種類 (基本12種 + ワイルドカード1種 + おまけ1種)もあります!
参考: Fragment Specifiers - The Little Book of Rust Macros
よく使いそうな順で並べるとこんな感じでしょうか?
フラグメント指定子 | 例 | 説明 |
---|---|---|
ident |
hoge , self
|
変数名や関数名などの識別子全般 |
expr |
10 + 20 , funcall() , if a { b } else { c }
|
式。いわゆる評価後に値を返す何か。Rustは式指向言語なのでここに該当するものが案外多い |
literal |
10 , "hoge" , true
|
リテラル。言い換えると、ソースコード内で直接表現される固有値 |
stmt |
let a = 10; , struct Hoge; , fn foo() {}
|
ステートメント。文。何かしら意味を持つものの評価値とはならないもの。ちなみに文自体は評価されると () を返す。 |
ty |
i32 , &'a str , fn() -> ()
|
型 |
path |
std::option::Option , Turbo::<Fish> , ::std::println
|
パス。一見型と同じに見えるが、 :: で接続されたパスを表す意味が強い。例えば ::std::thread::spawn は関数であり型ではなく、これ自体はパスである |
block |
{} |
スコープを作り出す中括弧を含めた全体 |
item |
pub fn foo() {} , use std::fs; , impl Hoge {}
|
use文や関数宣言など。一部は文でもある。アイテム...という名前だとわかりにくいので、「ファイル内で最も外側に(あるいはトークン木の最上位に)配置できる要素」とおぼえておくと良さそう |
pat / pat_param
|
Some(ref hoge) , None , 0..10
|
パターンマッチとして使用できる構造。簡単に言えば match 式のアーム部分になり得るもの。 pat_param は古いエディションとの互換性を保つためなので忘れて良い |
lifetime |
'a , 'static , '_
|
ライフタイム |
meta |
derive(Debug) , path = "hoge" , serde
|
属性風マクロ等のアトリビュート(属性)、すなわち #[] の内側に書くことができる要素。めったに出てこないので忘れてよさそう |
vis |
pub , pub(crate)
|
Visibility、すなわち可視性の略。これのみをマッチさせる必要もないためめったに出てこない |
tt |
トークン木 (Token Tree) のことで、 任意の Rustコード片の塊にマッチするワイルドカード。つまりジョーカー。 ttは判別器に出てこない のでワイルドカードであることを覚えておいてほしいです! |
詳細は別な回でも掘り下げようと思います。が...
いや多すぎ!覚えられん!なんもわからん!
こんなアバウトな表覚えてる人なんていないでしょう。そして毎回調べる羽目になってどれを指定すればいいのかわけがわからなくなります。
仮に覚えていたとしても、 hoge
という変数名か何かがあったとして、 expr
で捕捉するべきなのか ident
で捕捉するべきなのかも毎回混乱します。
こんな状況を改善したく...
作りました
URL: https://rust-fragspecs.namnium.work/
GitHub: https://github.com/anotherhollow1125/coloring_rust
JSON色付け係ならず Rust色付け係 です!!
あなたのRustコードを解析して、然るべき フラグメント指定子の色で着色 してくれます!宣言マクロ ( macro_rules!
マクロ) を作成する際に、一部置き換えたい場所があれば、そこに色に対応したメタ変数が来るようにすればよいのです!例えばオレンジなら ident
なので $hoge:ident
という具合です!
使い方的な何か
使い方を解説するユースケースとして、宣言マクロのよくあるケースである、「似たような関数をたくさん定義しなければならない状況」を取り上げてみたいと思います。ちょうど今回のアプリを作るのにそのようなマクロが必要だったので、その部分を取り上げます!
impl<'ast, T> Visit<'ast> for Colored<T>
where
T: Debug,
{
fn visit_file(&mut self, i: &'ast syn::File) {
let ranges = self.ranges.entry(FragSpecs::Item).or_default();
ranges.push(i.span().byte_range());
::syn::visit::visit_file(self, i);
}
fn visit_block(&mut self, i: &'ast syn::Block) {
let ranges = self.ranges.entry(FragSpecs::Block).or_default();
ranges.push(i.span().byte_range());
::syn::visit::visit_block(self, i);
}
fn visit_expr(&mut self, i: &'ast syn::Expr) {
let ranges = self.ranges.entry(FragSpecs::Expr).or_default();
ranges.push(i.span().byte_range());
::syn::visit::visit_expr(self, i);
}
/* // こういう共通構造を持つ
fn visit_〇〇(&mut self, i: &'ast ▲▲) {
let ranges = self.ranges.entry(XX).or_default();
ranges.push(i.span().byte_range());
::syn::visit::visit_〇〇(self, i);
}
*/
// ... まだまだ続く
}
こんな感じのマクロになりそうです。
macro_rules! visit {
($〇〇:???, $▲▲:???, $XX:???) => {
fn $〇〇(&mut self, i: &'ast $▲▲) {
let ranges = self.ranges.entry($XX).or_default();
ranges.push(i.span().byte_range());
::syn::visit::$〇〇(self, i);
}
};
}
???
がわからん!となるので判別器に掛けます!
オレンジ色は...
ident
です!関数名部分になっている visit_〇〇
の部分に関しては実際識別子を要求したいので ident
でよさそうです。 visit_file
は手書きさせるとして、〇〇
は構文要素 (Element) なので2 メタ変数名は elm
として、 $elm:ident
としましょう!
syn::File
などが渡される ▲▲
は型っぽいですよね、型であることを確認します!
型を表す ty
以外をオフにして、ハイライトを確かめます。(ついでにハイライトスタイルも目立つようにbgを有効にしています。)
&'ast syn::File
は型として有効であることがわかります。 syn::File
だけでも型として成立するので、 &'ast $elmty
となるように、メタ変数は $elmty:ty
で決まりでよさそうです!
最後の self.ranges.entry($XX).or_default()
の $XX
は entry
の引数として渡されているので、評価される値、つまり expr
でなければならないでしょう。
全体をそのまま掛けてしまうと、上画像のように広範囲がマッチしてしまい該当箇所が expr
で良いかわからないので、今回は渡す引数(例えば FragSpecs::Item
)が expr
で問題ないかを確かめます。
問題なさそうです!というわけで、メタ変数は(ハッシュマップのキーなので) $key:expr
としましょう。
以上より、マクロは次のように作れて
macro_rules! visit {
($elm:ident, $elmty:ty, $key:expr) => {
fn $elm(&mut self, i: &'ast $elmty) {
let ranges = self.ranges.entry($key).or_default();
ranges.push(i.span().byte_range());
::syn::visit::$elm(self, i);
}
};
}
置き換えた結果は次のようになりました!
impl<'ast, T> Visit<'ast> for Colored<T>
where
T: Debug,
{
visit!(visit_file, syn::File, FragSpecs::Item);
visit!(visit_block, syn::Block, FragSpecs::Block);
visit!(visit_expr, syn::Expr, FragSpecs::Expr);
visit!(visit_ident, syn::Ident, FragSpecs::Ident);
visit!(visit_item, syn::Item, FragSpecs::Item);
visit!(visit_lifetime, syn::Lifetime, FragSpecs::Lifetime);
visit!(visit_lit, syn::Lit, FragSpecs::Literal);
visit!(visit_meta, syn::Meta, FragSpecs::Meta);
visit!(visit_pat, syn::Pat, FragSpecs::Pat);
visit!(visit_path, syn::Path, FragSpecs::Path);
visit!(visit_stmt, syn::Stmt, FragSpecs::Stmt);
visit!(visit_type, syn::Type, FragSpecs::Ty);
visit!(visit_visibility, syn::Visibility, FragSpecs::Vis);
}
...こんな感じで、本ツールを使うことでフラグメント指定子が正しいかを確認しながらマクロを実装することができます!
TIPS
その他、本アプリの細かい使い方を本節で解説します3!
Highlighting Priority
フラグメント指定子着色の優先順位を決定するカラムです。
- チェックボックスにチェックが入っているフラグメント指定子のみがハイライトされます。
- 上にある指定子ほど他の指定子の色を上書きします。順番は一番右にあるハンドルをD&Dすることで変えられます。
- カラーパレットアイコン 🎨 から、ハイライト色の変更や背景色でのハイライトへの変更が可能です。
思うようなハイライトにならなかった時はチェックボックスを外したり順番を変えてみたりしてほしいです。
Whole Matcher
与えられたRustコード片 全体 について、何であると解釈してパースするかを決定します。
-
file
はRustコード全体がファイルであると仮定してのパースです。file
はフラグメント指定子ではありません。 - チェックボックスにチェックが入っている構文でのみ、パースを試みます。
- 上にある構文からマッチを試みていきます。最初にマッチした構文での結果が表示され、その構文には右に丸チェックがつきます。順番は一番右にあるハンドルをD&Dすることで変えられます。
- マッチした構文はパース結果上部にも
Whole Match: xxx
という形で示されています。
- マッチした構文はパース結果上部にも
- その他にパースに成功した構文についても、右にチェックがつきます。
Whole Matcherはコード片全体のフラグメント指定子を確認したい場合に便利です。思うようにコード片が解釈されない時はこちらの順番も調整してみてください。
その他のTIPS
- 上の方にライトモード/ダークモード切り替えがあるので見やすい方を使ってください。
- Highlighting Priority、Whole Matcherともにカラムタイトルの横のチェックボックスで全体の有効/無効を切り替えられ、一番下にあるボタンで初期状態4に戻せます。
- 冒頭のフラグメント指定子の表にも書きましたが、 フラグメント指定子には
tt
がある一方、本アプリでは確認するすべがありません。 これは内部で使用しているsynクレートとの兼ね合いの問題もあるのですが、そもそもtt
はワイルドカード なのでマッチするしないは関係ないのです...ないのです。
まとめ・所感
宣言マクロを書きやすくするため、Rustコード中のフラグメント指定子を解析するツールを作った話でした!
...けど正直バリバリ使えるかと聞かれるとマクロ制作に慣れれば慣れるほど微妙なツールだったりします
ただ不慣れなうちや、ちょっと確認したい時などにこのツールを起点に色々検証して試していただけたらなという思いで作りました!ぜひ活用していただけると幸いです。