15
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustマクロ冬期講習Advent Calendar 2024

Day 16

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

Last updated at Posted at 2024-12-16

こちらの記事は Rustマクロ冬期講習アドベントカレンダー 16日目の記事です!1

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

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

なんだっけ
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 で捕捉するべきなのかも毎回混乱します。

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

作りました

Rustフラグメント指定子判別器.png

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);
    }    
    */
    // ... まだまだ続く
}

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

宣言マクロ 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

オレンジ色は...

image.png

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

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

image.png

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

image.png

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

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

image.png

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

image.png

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

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

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
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

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

image.png

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

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

Whole Matcher

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

image.png

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

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

その他のTIPS

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

まとめ・所感

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

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

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

  1. 1日目から15日目がまだ未登録なのに何故16日目からかと言うと、16日に本記事のアプリが完成したためです...ここから挽回します! :muscle:

  2. 本当はfragspecsとかの方が良かったかもしれないですが、分かればなんでも良いのです!

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

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

15
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?