Rustマクロ フラグメント指定子(ident, expr, item, stmt...)なんもわからん！となったので判別器作った


こちらの記事は [Rustマクロ冬期講習アドベントカレンダー](https://qiita.com/advent-calendar/2024/rust-macro) 14日目の記事です！

> アドカレまとめ記事はこちら！: [Rustマクロ作成チートシート！](https://qiita.com/namn1125/items/d601d2cbbf2cc7b5d3dc)

Rustの宣言マクロ作成を手助けするフラグメント指定子判別器を作った話です。

https://rust-fragspecs.namnium.work/

皆さんはRustでマクロを書いて活用していますか...？意外と(？)面倒ですよね。筆者は特に宣言マクロ( `macro_rules!` によるマクロ)を書く際、 `($hoge:???) => { ... }` の `???` 部分がいつもわからなくなります :dizzy_face:

```rust:なんだっけ
macro_rules! my_awesome_macro {
    ($hoge:???) => {
        // ...
    };
}
```

`$hoge:???` のことをメタ変数と言い、そして `???` は **フラグメント指定子** と呼ばれるもので、宣言マクロのパターンマッチにおいてRustコードの **どんな構文パーツを期待するか** を表すのに使われ、 **14種類** (基本12種 + ワイルドカード1種 + おまけ1種)もあります！

参考: [Fragment Specifiers - The Little Book of Rust Macros](https://veykril.github.io/tlborm/decl-macros/minutiae/fragment-specifiers.html)

よく使いそうな順で並べるとこんな感じでしょうか？

| フラグメント指定子 | 例 | 説明 |
|:---------------|:---|:-----|
| `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は判別器に出てこない** のでワイルドカードであることを覚えておいてほしいです！ |

詳細は別な回にまとめました！

[Rustマクロ フラグメント指定子 #Rust - Qiita](https://qiita.com/namn1125/items/303170b563030fd207cc)

https://qiita.com/namn1125/items/303170b563030fd207cc

が...

**<font color="red">いや多すぎ！覚えられん！なんもわからん！</font>**

こんなアバウトな表覚えてる人なんていないでしょう。そして毎回調べる羽目になってどれを指定すればいいのかわけがわからなくなります。

仮に覚えていたとしても、 `hoge` という変数名か何かがあったとして、 `expr` で捕捉するべきなのか `ident` で捕捉するべきなのかも毎回混乱します。

こんな状況を改善したく...

# 作りました

https://rust-fragspecs.namnium.work/

![Rustフラグメント指定子判別器.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/422aba56-747e-527d-a68e-d10642bf04bb.png)

URL: https://rust-fragspecs.namnium.work/
GitHub: https://github.com/anotherhollow1125/coloring_rust

JSON色付け係ならず **Rust色付け係** です！！

あなたのRustコードを解析して、然るべき **フラグメント指定子の色で着色** してくれます！宣言マクロ ( `macro_rules!` マクロ) を作成する際に、一部置き換えたい場所があれば、そこに色に対応したメタ変数が来るようにすればよいのです！例えばオレンジなら `ident` なので `$hoge:ident` という具合です！

:::note
今回は使い方のみ解説しています。どのような方法でこのツールを作成したかは他の回にまとめました！

[【手続きマクロだけではない】synクレートを活用して便利ツールを作った話【Rust】](https://qiita.com/namn1125/items/c7659e863e8b633668b4)

よかったら読んでみてください :bow:
:::

# 使い方的な何か

使い方を解説するユースケースとして、宣言マクロのよくあるケースである、「似たような関数をたくさん定義しなければならない状況」を取り上げてみたいと思います。ちょうど今回のアプリを作るのにそのようなマクロが必要だったので、その部分を取り上げます！

```rust:最終的に望む展開結果
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);
    }    
    */
    // ... まだまだ続く
}
```

こんな感じのマクロになりそうです。

```rust:宣言マクロ visit
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);
        }
    };
}
```

`???` がわからん！となるので判別器に掛けます！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/db123ac4-e5bb-7679-b991-5853038cab27.png)

オレンジ色は...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/9a982d27-ea37-20c3-861d-8db76ba4c651.png)

 `ident` です！関数名部分になっている `visit_〇〇` の部分に関しては実際識別子を要求したいので `ident` でよさそうです。 `visit_file` は手書きさせるとして、`〇〇` は構文要素 (Element) なので[^elm] メタ変数名は `elm` として、 `$elm:ident` としましょう！

 [^elm]: 本当はfragspecsとかの方が良かったかもしれないですが、分かればなんでも良いのです！

 `syn::File` などが渡される `▲▲` は型っぽいですよね、型であることを確認します！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/2f94af26-661a-6915-0e3e-e45c74194eaa.png)

型を表す `ty` 以外をオフにして、ハイライトを確かめます。(ついでにハイライトスタイルも目立つようにbgを有効にしています。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/399dfdb8-2c8b-4c54-871b-769ecf23d1c3.png)

`&'ast syn::File` は型として有効であることがわかります。 `syn::File` だけでも型として成立するので、 `&'ast $elmty` となるように、メタ変数は `$elmty:ty` で決まりでよさそうです！

最後の `self.ranges.entry($XX).or_default()` の `$XX` は `entry` の引数として渡されているので、評価される値、つまり `expr` でなければならないでしょう。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/fafbc0b1-387c-f801-c5ef-54127275279f.png)

全体をそのまま掛けてしまうと、上画像のように広範囲がマッチしてしまい該当箇所が `expr` で良いかわからないので、今回は渡す引数(例えば `FragSpecs::Item` )が `expr` で問題ないかを確かめます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/0c95d628-2e46-d4da-a8b3-d1d638aaadf3.png)

問題なさそうです！というわけで、メタ変数は(ハッシュマップのキーなので) `$key:expr` としましょう。

以上より、マクロは次のように作れて

```rust:Rust
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);
        }
    };
}
```

置き換えた結果は次のようになりました！

```rust:Rust
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

その他、本アプリの細かい使い方を本節で解説します[^readme]！

[^readme]: この記事を書いたのちにアプリにREADME.mdを作成しようと考えており、よって本来書くべき場所にまだこの説明がないのです...

## Highlighting Priority

フラグメント指定子着色の優先順位を決定するカラムです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/bba0a30f-982f-f178-2fb6-673918a89978.png)

- チェックボックスにチェックが入っているフラグメント指定子のみがハイライトされます。
- 上にある指定子ほど他の指定子の色を上書きします。順番は一番右にあるハンドルをD&Dすることで変えられます。
- カラーパレットアイコン 🎨 から、ハイライト色の変更や背景色でのハイライトへの変更が可能です。

思うようなハイライトにならなかった時はチェックボックスを外したり順番を変えてみたりしてほしいです。

## Whole Matcher

与えられたRustコード片 **全体** について、何であると解釈してパースするかを決定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/0f3d56ae-b1f2-5b4c-a693-0500b6d833c0.png)

- `file` はRustコード全体がファイルであると仮定してのパースです。 `file` はフラグメント指定子ではありません。
- チェックボックスにチェックが入っている構文でのみ、パースを試みます。
- 上にある構文からマッチを試みていきます。最初にマッチした構文での結果が表示され、その構文には右に丸チェックがつきます。順番は一番右にあるハンドルをD&Dすることで変えられます。
  - マッチした構文はパース結果上部にも `Whole Match: xxx` という形で示されています。
- その他にパースに成功した構文についても、右にチェックがつきます。

Whole Matcherはコード片全体のフラグメント指定子を確認したい場合に便利です。思うようにコード片が解釈されない時はこちらの順番も調整してみてください。

## その他のTIPS

- 上の方にライトモード/ダークモード切り替えがあるので見やすい方を使ってください。
- Highlighting Priority、Whole Matcherともにカラムタイトルの横のチェックボックスで全体の有効/無効を切り替えられ、一番下にあるボタンで初期状態[^init]に戻せます。
- 冒頭のフラグメント指定子の表にも書きましたが、 **フラグメント指定子には `tt` がある一方、本アプリでは確認するすべがありません。** これは内部で使用しているsynクレートとの兼ね合いの問題もあるのですが、そもそも **`tt` はワイルドカード** なのでマッチするしないは関係ないのです...ないのです。

[^init]: といっても初期状態の順番は何か明確な根拠に基づくものではなく筆者のカンで良さげな並びにしたものです。もっと良い並びがあったらPR欲しいです。

# まとめ・所感

宣言マクロを書きやすくするため、Rustコード中のフラグメント指定子を解析するツールを作った話でした！

...けど正直バリバリ使えるかと聞かれるとマクロ制作に慣れれば慣れるほど微妙なツールだったりします :sweat_smile:

ただ不慣れなうちや、ちょっと確認したい時などにこのツールを起点に色々検証して試していただけたらなという思いで作りました！ぜひ活用していただけると幸いです。