Rust 宣言マクロ小ネタ集【闇編 宣言マクロ・マッチの限界】


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

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

今回も小ネタ紹介です。以下、今回の出典元です。

[Metavariables and Expansion Redux - The Little Book of Rust Macros](https://veykril.github.io/tlborm/decl-macros/minutiae/metavar-and-expansion.html)

https://veykril.github.io/tlborm/decl-macros/minutiae/metavar-and-expansion.html

今回は小ネタ【闇編】ということで、別に闇でもなんでもないのですが、「そんな制約が...」と少しガッカリしてしまうものを持ってきました。

# ガッカリ1: 宣言マクロのマッチは上のアームに吸われる！！！

```rust:Rust (コンパイルエラー)
macro_rules! checker {
    ($i:item) => { println!("アイテム"); };
    ($($t:tt)*) => { println!("その他"); };
}

fn main() {
    // ttにマッチさせるつもり
    checker!(hoge);
}
```

```plain:コンパイルエラー
error: expected one of `!` or `::`, found `<eof>`
 --> src/main.rs:8:14
  |
2 |     ($i:item) => { println!("アイテム"); };
  |      ------- while parsing argument for this `item` macro fragment
...
8 |     checker!(hoge);
  |              ^^^^ expected one of `!` or `::`

error: could not compile `playground` (bin "playground") due to 1 previous error
```

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=206ebd5fa957f30db37a716df040b994

マクロのアームは **マッチが失敗したからといって別なマッチを試してくれるとは限らない** ということです。失敗するアームに入力が吸われると言いましょうか。ともかく「えぇ...」な仕様です。

`match` 式のアームのようにもう少し直感的に書ければ良いのですけどね...

ちなみに次のように順序を変えればコンパイルは通りますが2つ目の `item` アームに意味がなくなります :upside_down:

```rust:Rust
macro_rules! checker {
    ($($t:tt)*) => { println!("その他"); };
    ($i:item) => { println!("アイテム"); };
}

fn main() {
    checker!(hoge); // => その他
    // itemにマッチさせるつもり
    checker!(fn fuga() {}); // => その他
}
```

うーん...ケチ！Little Bookには「より具体的に記述されたパターンを上に持っていくべき」みたいなこと書いていますが、もし希望通りのマッチ結果にならない時はともかく色々な順番を試した方が良さそうですし、試したところで希望通りのマッチにならないことが結構あるわけです。

# ガッカリ2: メタ変数の後ろに制約がある場合がある

[どこかの回](https://qiita.com/namn1125/items/63756af0d3cecdaa02cd)でマッチアームには「めっちゃヘンテコなパターンも書ける」と書いたのですが、嘘でしたという話です :sweat_smile:

```rust:OK
macro_rules! strange_macro {
    (@~ $name:ident ~%) => {
        println!("Hey, {}!", stringify!($name));
    };
}

fn main() {
    strange_macro!(@~ hoge ~%);
}
```

```rust:NG
macro_rules! strange_macro {
    (@~ $e:expr ~%) => { // OKの場合とはフラグメント指定子が異なるのみ
        println!("Hey, {}!", stringify!($e));
    };
}

fn main() {
    strange_macro!(@~ 10 ~%);
}
```

```plain:エラー内容
error: `$e:expr` is followed by `~`, which is not allowed for `expr` fragments
 --> src/main.rs:2:17
  |
2 |     (@~ $e:expr ~%) => { // OKの場合とはフラグメント指定子が異なるのみ
  |                 ^ not allowed after `expr` fragments
  |
  = note: allowed there are: `=>`, `,` or `;`

error: could not compile `playground` (bin "playground") due to 1 previous error
```

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8edf80c158376ca2502a7372851c7e71

コンパイルエラーに「`expr` の後続は `=>` か `,` か `;` のみが許されています」と記載されているように、特定のフラグメント指定子について後続に許されるトークンに縛りがあります！

|フラグメント指定子|許容されるトークン|
|:-|:-|
|[`stmt`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E6%96%87-stmt) または [`expr`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E5%BC%8F-expr)| `=>`, `,`, `;` |
|[`pat`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3-pat--pat_param)| `=>`, `,`, `=`, `if`, `in`, (`｜`) (※1) |
|[`pat_param`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3-pat--pat_param)| `=>`, `,`, `=`, `if`, `in`, `｜` |
| [`path`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E3%83%91%E3%82%B9-path) または [`ty`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E5%9E%8B-ty) | `=>`, `,`, `=`, `｜`, `;`, `:`, `>`, `>>`, `[`, `{`, `as`, `where`, ブロック([`block`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF-block))要素 |
|[`vis`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E5%8F%AF%E8%A6%96%E6%80%A7-vis)| `,`, `priv` (※2) 以外の識別子, 型([`ty`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E5%9E%8B-ty))を始められるトークン(`<` など), 「[`ident`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E8%AD%98%E5%88%A5%E5%AD%90-ident), [`ty`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E5%9E%8B-ty), [`path`](https://qiita.com/namn1125/items/303170b563030fd207cc#%E3%83%91%E3%82%B9-path)」のうちのどれかのメタ変数 |
|その他| とくに制限なし |

※1: `|` なのですがMarkdownが崩れてしまうので`｜`で代替しています。なお、 `pat` で `|` が許されるのは最新エディション(2021)以降です。互換性維持のために `pat_param` というものがあります。
※2: `priv` は現在は使われていない予約語です。` ` (スペースではなく無)が可視性 `vis` としてマッチする都合上、可視性に関わってそうな `priv` がマッチすると都合が悪そうです。

Little Bookには後続制限は「マクロ入力解釈を変更する将来の構文変更に備えて〜」みたいなことが書いていますが、確かに任意のフラグメント指定子について後続をなんでも許可すると、そもそもマッチングが複雑になり読みにくくなってしまいます。

```rust:Rust (コンパイルエラー)
macro_rules! compare {
    ($a:expr < $b:expr) => {};
}

fn main() {
    compare!(10 < 20);
}
```

`10 < 20` はこれはこれで式です。 `10` まで読めば良いのか `<` も読まなければならないのか不明瞭なので、これがコンパイルエラーになってもあまり文句はありません。もし無理やり式として解釈できたとして、将来式として解釈される構文が増えたりした時に同じように働く保証はありません。そのためにエラーになる方に倒しているのだと思います。

もし後続トークンが原因で怒られたら素直に別な表現に変えましょう。

# ガッカリ3: 先読みなど高度なマッチングはしてくれない

高度な正規表現みたいなことはしてくれないらしく次のような書き方はできません。

```rust:Rust (コンパイルエラー)
macro_rules! ambiguity {
    ($($i:ident)* $i2:ident) => { };
}

fn main() {
    ambiguity!(an_identifier);
}
```

```plain:エラー
error: local ambiguity when calling macro `ambiguity`: multiple parsing options: built-in NTs ident ('i') or ident ('i2').
 --> src/main.rs:6:16
  |
6 |     ambiguity!(an_identifier);
  |                ^^^^^^^^^^^^^

error: could not compile `playground` (bin "playground") due to 1 previous error
```

Playground: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=60f806218646c30824382e9809c004c8

マクロ終わりの `)` が来ていることを先読みできれば `i2` のみにマッチして `i` は0個だったと判定してくれますが、「曖昧だ 💢」と怒られてしまっています。

ここまでよりわかることとして、定義するパターンはこっちが忖度して決める必要がありそうです。

# ガッカリ4: 一度マッチしてしまうとそのフラグメント指定子で固定されてしまう

[Little Book](https://veykril.github.io/tlborm/decl-macros/minutiae/metavar-and-expansion.html)にて "One aspect of substitution that often surprises people is that substitution is not token-based, despite very much looking like it." 、「置換はトークンベースに見えるが実際はそうではない」と記述されている部分です。

複数のマクロがネストしている時に問題になります。Little Bookにある例です。

```rust:Rust
macro_rules! capture_then_match_tokens {
    ($e:expr) => {match_tokens!($e)};
}

macro_rules! match_tokens {
    ($a:tt + $b:tt) => {"got an addition"};
    (($i:ident)) => {"got an identifier"};
    ($($other:tt)*) => {"got something else"};
}

fn main() {
    println!("{}\n{}\n{}\n",
        match_tokens!((caravan)),
        match_tokens!(3 + 6),
        match_tokens!(5));
    println!("{}\n{}\n{}",
        capture_then_match_tokens!((caravan)),
        capture_then_match_tokens!(3 + 6),
        capture_then_match_tokens!(5));
}
```

```plain:出力結果
got an identifier
got an addition
got something else

got something else
got something else
got something else
```

`capture_then_match_tokens` マクロでキャプチャされてしまうと、その中身がなんであれ全て `expr` の塊であったと見做されてしまい、改めて分解してのマッチングができなくなってしまっています！

> By parsing the input into an AST node, the substituted result becomes un-destructible; i.e. you cannot examine the contents or match against it ever again.
>
> 入力を AST ノードに解析すると、置換された結果は破壊不可能になります。 つまり、その内容を調べたり、再度一致させたりすることはできません。

- `tt`
- `ident`
- `lifetime`

でのマッチングだとフラグメント指定子固定がないので、別なマクロで改めて分解したいものはこちらでマッチングすると良いらしいです。宣言マクロどんな闇仕様してるんだよ...

# まとめ・所感

「宣言マクロ罠多すぎ...」って気持ちになる小ネタ集を紹介しました！コンパイルがうまくいかない時はパターンをゴニョゴニョするしかなさそうですね。

どうしてもマクロで実現したい一方で宣言マクロに限界を感じたら、さっさと手続きマクロに移行してしまうのもありだと思います。