【手続きマクロだけではない】synクレートを活用して便利ツールを作った話【Rust】


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

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

14日目の記事、[Rustマクロ フラグメント指定子(ident, expr, item, stmt...)なんもわからん！となったので判別器作った #Rust - Qiita](https://qiita.com/namn1125/items/93461ddf93277b79a96f) にて、Rustコードを与えるとフラグメント指定子ごとに色分けしてくれる [判別器](https://rust-fragspecs.namnium.work/) を作成した話をしました。

この記事では話していませんでしたが、実はこのツール、三種の神器の一つの [`syn`](https://docs.rs/syn/latest/syn/) を利用したものです。というわけで、その話をしたいと思います！

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)

:::note
**本記事で伝えたいこと**

**[syn](https://docs.rs/syn/latest/syn/index.html) クレートは手続きマクロ以外でもカジュアルに使えます！** Rustの構文解析をしたいならば次の機能が便利かもしれません。

- [`syn::parse_str`](https://docs.rs/syn/latest/syn/fn.parse_str.html)
  - 対象構文要素( [`syn::parse::Parse`](https://docs.rs/syn/latest/syn/parse/trait.Parse.html) トレイトを実装した型)への文字列のパースを試みることができます
- [`proc_macro2::Span`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.Span.html)
  - 構文要素のソースコード中での位置情報が格納されています。ただし条件つきなので要ドキュメント確認
- [`syn::visit::Visit`](https://docs.rs/syn/latest/syn/visit/trait.Visit.html)
  - 構文要素を再帰的に探索するために使用できます
  - 他にも [`syn::fold`](https://docs.rs/syn/latest/syn/fold/index.html) モジュールも再帰的な処理に使えるかもです


例えばマクロのテストをする際に使えそうですね :sunglasses:
:::

# カラクリ・処理順序

本節ではこの判別器の仕組みを軽く解説します。

対象ソースコード:

- [coloring_common/src/lib.rs](https://github.com/anotherhollow1125/coloring_rust/blob/main/coloring_common/src/lib.rs)
  - お試しでCLI版も用意していたためその名残で別クレートになっています
- [coloring_wasm/src/lib.rs](https://github.com/anotherhollow1125/coloring_rust/blob/main/coloring_wasm/src/lib.rs)

構文要素を判定する機能は [`syn::parse_str`](https://docs.rs/syn/latest/syn/fn.parse_str.html) を基軸に作成し、WebAssemblyを利用して呼び出すようにしました。(ちなみにフロントエンドは色々検討したのですが結局React + muiに落ち着きました... :sweat_smile: )

処理順序になります。

(1) 様々な構文要素で `parse_str` を試す

- マッチしたものについてリストアップ。サイトでは左側の "Whole Matcher" 欄にてマッチした要素の情報を表示

(2) マッチした構文要素について、再帰的に構文要素を調べる

- 再帰処理には [`syn::visit::Visit`](https://docs.rs/syn/latest/syn/visit/trait.Visit.html) を利用
- ある構文要素 `S` がヒットした際、 [`<S as Spanned>::span`](https://docs.rs/syn/latest/syn/spanned/trait.Spanned.html#tymethod.span) を調べ、 [`byte_range`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.Span.html#method.byte_range) メソッドでソースコード位置( `Range<usize>` )を取得。ハッシュマップ `HashMap<構文要素列挙体, Vec<Range<usize>>>` に格納

(3) 然るべき加工(CLIならASCIIエスケープシーケンスによる着色。WebAssemblyならマークアップ)を施して表示

オプション等の処理を含めるともう少しいろいろ話せることはありますが、軸となる処理は [`parse_str`](https://docs.rs/syn/latest/syn/fn.parse_str.html) があったおかげでそこまで裏技的なことをせずに実装することができました。またもう一つ特に **悩むだろうなと予想されたのは、親構文要素から子構文要素を探索していく再帰処理部分** でしたが、**[`syn::visit::Visit`](https://docs.rs/syn/latest/syn/visit/trait.Visit.html)トレイト** があったおかげで案外苦労せずに実装することができました！

synクレートにおんぶに抱っこになれば、この手のツールは簡単に作れるということですね...他にも色々活用できそうです。

:::note
**[`Visit`](https://docs.rs/syn/latest/syn/visit/trait.Visit.html)トレイトはどういう仕組み？**

残りのハンズオンパートでは `Visit` トレイトの話はしないのですが、仕組みが面白かったのでここで少し解説したいと思います。

`visit` モジュールにはこの `Visit` トレイトと大量の `visit_xxx` 関数が定義されており、 `Visit` トレイトが提供するメソッド `Visit::visit_xxx` のデフォルト実装にてこの関数 `visit_xxx` が呼ばれている構造になっています。

```rust:Rust
trait Visit<'ast> {
    // ...省略...
    fn visit_xxx(&mut self, i: &'ast crate::Xxx) {
        visit_xxx(self, i);
    }
    // ...省略...
}
```

`visit_xxx` 関数は、自身より小さい要素の関数 `visit_yyy` を呼び出すようになっており(例えば `visit_stmt` なら式の時に `visit_expr` を呼ぶなど)、この仕組みによって子要素の方にまで再帰的にアクセスできるようになっています！

```rust:Rust
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` 要素に対するすべての探索にフック処理が入ります！

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

## クレート用意

まずはクレートを用意します。

```bash
crate new match_frags
```

「フラグメント指定子(Fragment Specifiers)」にマッチするかどうか、ということで `match_frags` という名前にしてみました。

次に必要なクレートを追加していきます。もちろん `syn` クレートと、今回はCLIアプリにしたいので `clap` クレートを入れます。

```bash
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`: 宣言的に引数を書けるようになるので必須です

## コマンドライン引数で文字列入力を受ける

確認したい対象となる文字列はコマンドライン引数で受けることにします。

```rust:src/main.rs
use clap::Parser;

#[derive(Parser)]
struct Input {
    /// 構文解析対象文字列
    parse_target: String,
}

fn main() {
    let Input { parse_target } = Input::parse();

    println!("{}", parse_target);
}
```

受けた文字列をそのまま出力するだけのプログラムです。

```bash
$ cargo run -q -- 'hoge(1 + 1)'
hoge(1 + 1)
```

## `syn::parse_str` でマッチ！

歌ならもうサビの部分です、引数で取り入れた解析対象文字列 `parse_target` が構文要素にマッチするか調べます！

とりあえず、式( [`syn::Expr`](https://docs.rs/syn/latest/syn/enum.Expr.html) )の場合は以下のような感じです。

```rust:Rust
if syn::parse_str::<syn::Expr>(&parse_target).is_ok() {
    println!("{} is Expr", parse_target);
}
```

```diff_rust:src/main.rs
// 省略

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` ...多い！)でマッチするか確認していきます。プログラムの変更について柔軟性を持たせるため、マッチした要素を文字列として動的配列に追加することし、面倒なので宣言マクロを利用することにします！

```rust:src/main.rs
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構文要素の何に該当するかを判別できます！

```bash
$ 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`](https://docs.rs/syn/latest/syn/fn.parse_str.html) が便利すぎた...

:::note
**`macro_rules` スコープの小ネタ**

地味に今まで紹介していなかった小ネタを今回利用しています。

[Rustマクロの事前知識③「マクロのマナー 衛生性(健全性)」](https://qiita.com/namn1125/items/61587afa264107ba7567) の結論としては、マクロ内で宣言されていない変数へのアクセスはマナー的にやめたほうが良いのでした。そして実際、大抵の場合これはエラーになります。

しかし、実は `macro_rules` でマクロを宣言しているスコープ内に定義されている変数に関しては、その限りではなく普通にアクセス出来ます！

```rust:Rust
/* // こちらだとコンパイルエラー
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`](https://docs.rs/syn/latest/syn/fn.parse_str.html) を利用した軽いハンズオンを通して、 [syn](https://docs.rs/syn/latest/syn/) クレートは手続きマクロだけでなくRust構文解析にまつわるツール作成にも用いられることを示しました。

`syn` は手続きマクロ作成専用というより、Rust構文全般のパースや操作に便利なクレートです。こうした多用途性を意識しておくと、手続きマクロの作成や応用を考えるときの解像度も上がるはずです...というわけで、皆さんもぜひ `syn` クレートを利用して面白いツールを作ってみてほしいです！

ここまで読んでいただきありがとうございました！