Rustマクロの事前知識①「入出力はトークン木」


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

- Rustマクロの入出力単位は「トークン木」であること
- マクロの置換はASTレベルで解析されるので、不完全な処理結果になることはないこと
:::

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

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

2日目は、Rustのマクロはそもそもどういう処理を行うものであるか？を軽く解説したいと思います。

# Rustマクロの入出力は「トークン木」

C/C++のマクロと同じく、直感的にはRustのマクロも「コンパイル時にソースコードの一部を書き換えるもの」です。

ただし決定的に違う部分があります。C/C++のマクロは純粋にソースコードを書き換えてしまう一方で、Rustのマクロはソースコードを解析することで得られる **抽象構文木 (Abstract Syntax Tree, AST) を置き換える** 処理をします。この違いについては後ほど例を出します。

絵に表すと次のような流れでASTが置換されます。( **※画像はイメージであり実際のコンパイルの流れを描き表したものではありません。** )

![MacroFlow3.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/205472/e9b5dd9a-2d9c-a8ce-27c7-f541b995fce4.png)

ここでASTの他に「 **トークン木 (TokenTree)** 」というものが出てきました。これこそがRustマクロの入出力でやりとりされる<ruby>キホンの木<rt><strong>・・・・・</strong></rt></ruby>です！

トークン木は、ソースコードを切り分けて得られるトークンと、ASTの中間に位置するもので、次の要素(葉と枝)からなります。

- 葉: 「識別子」「句読点」「リテラル」などのトークン
- 枝: 葉を括弧( `()` または `[]` または `{}` )で囲んだグループ

```plain:トークン木(The little bookより拝借)
a + b + (c + d[0]) + e

↓ «» で囲まれたものがトークン木の要素

«a» «+» «b» «+» «(   )» «+» «e»
          ╭────────┴──────────╮
           «c» «+» «d» «[   ]»
                        ╭─┴─╮
                         «0»
```

こういった話は話のみでは「あくまでも概念的なもので実際にマクロを書くときには関係ないんでしょ...？」ってなりがちです。わかります。そこで軽く検証してみたいと思います。

実際にRustの手続きマクロを作成する際に使う `proc-macro2` ( と `syn` & `quote` ) クレートを活用して、Rustのソースコードをトークン木に変換しデバッグ出力してみます。

```rust:Rust
use quote::ToTokens;

fn main() {
    let stmt = r#"let a = 40 + (30 * (20 - 10));"#;
    
    let stmt: syn::Stmt = syn::parse_str(stmt).unwrap(); // stmt をRustの「文」として解釈
    let token_stream: proc_macro2::TokenStream = stmt.to_token_stream(); // トークンストリームにする

    for token_tree in token_stream { // トークン木として葉やグループを一つ一つ取り出す
        dbg!(token_tree);
    }
}
```

```plain:実行結果
[src/main.rs:10:9] token_tree = Ident {
    sym: let,
    span: bytes(1..4),
}
[src/main.rs:10:9] token_tree = Ident {
    sym: a,
    span: bytes(5..6),
}
[src/main.rs:10:9] token_tree = Punct {
    char: '=',
    spacing: Alone,
    span: bytes(7..8),
}
[src/main.rs:10:9] token_tree = Literal {
    lit: 40,
    span: bytes(9..11),
}
[src/main.rs:10:9] token_tree = Punct {
    char: '+',
    spacing: Alone,
    span: bytes(12..13),
}
[src/main.rs:10:9] token_tree = Group {
    delimiter: Parenthesis,
    stream: TokenStream [
        Literal {
            lit: 30,
            span: bytes(15..17),
        },
        Punct {
            char: '*',
            spacing: Alone,
            span: bytes(18..19),
        },
        Group {
            delimiter: Parenthesis,
            stream: TokenStream [
                Literal {
                    lit: 20,
                    span: bytes(21..23),
                },
                Punct {
                    char: '-',
                    spacing: Alone,
                    span: bytes(24..25),
                },
                Literal {
                    lit: 10,
                    span: bytes(26..28),
                },
            ],
            span: bytes(20..29),
        },
    ],
    span: bytes(14..30),
}
[src/main.rs:10:9] token_tree = Punct {
    char: ';',
    spacing: Alone,
    span: bytes(30..31),
}
```

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

`Ident` は識別子、 `Punct` は句読点、 `Literal` はリテラルであることを表し、 `Group` はトークン木の枝であるグループを表しています。 `Group` によりネスト構造が生まれています。

[`proc_macro2::TokenStream`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html) は、トークンを吐き出すストリームであり、その吐き出す一つ一つの値の型([IntoIterator::Item](https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html#impl-IntoIterator-for-TokenStream))は  [`proc_macro2::TokenTree`](https://docs.rs/proc-macro2/latest/proc_macro2/enum.TokenTree.html) つまりトークン木になっています。`proc_macro2::TokenStream` はまさしくRustの手続きマクロの入出力型[^tokenstream]になっていますから、「Rustマクロの入出力はトークン木」と言えることがコードから確認できたかと思います！

[^tokenstream]: 正確には [`proc_macro::TokenStream`](https://doc.rust-lang.org/proc_macro/struct.TokenStream.html) ですがほぼ同一です。このことについては実際に手続きマクロを書く回で触れたいと思います。

# 出力トークン木が抽象構文木として不完全だとエラー

トークン木自体は、葉とグループからなるというルールを除くと、 **Rustの文法とは無縁** であり、言い換えると文法に従っているとは限らなくてよいものになっています。ただしRustマクロにより **出力されるトークン木は設置先のASTに組み込まれて有効なものである** 必要があります。

C/C++のマクロはソースコードの置き換えでありそのような制限がないため、次のように書くことができます。

```c:C言語
#include <stdio.h>

#define MYMACRO(i) + i +

int main(void) {
    printf("%d", 1 MYMACRO(2) 3); // printf("%d", 1 + 2 + 3);
    return 0;
}
```

一方でRustのマクロが出力するトークン木にはASTとして有効という縛りがあるため、トークン木としては成立していても次のようなマクロは書けません。

```rust:Rust (コンパイルエラー)
macro_rules! mymacro {
    ($i:expr) => {
        + $i +
    };
}

fn main() {
    println!("{}", 1 mymacro(2) 3);
}
```

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

あるいは、同じマクロ呼び出しでも書ける部分と書けない部分があります。

```rust:Rust (コンパイルエラー)
macro_rules! double_expr {
    ($e:expr) => {
        $e;
        $e
    };
}

fn main() {
    double_expr!(10); // OK

    let hoge = double_expr!(10); // NG
}
```

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

`let hoge = 10; 10;` みたいに展開され、文法的には一応問題ない感じになりそうですが、ここでは式(expr)が要求されているためエラーになってしまいます。

C/C++のマクロと異なり、抽象構文木まで考慮されたマクロ置き換えなので、C/C++のマクロと比べると **ユーザーが意図しないマクロ置換結果になりにくい** という特徴があり、安心して使いやすい黒魔術になっているのです！

# まとめ

色々書きましたが、

- Rustマクロの入出力は「トークン木」である
- C/C++のマクロのような不完全で下手なマクロは書けない/下手な使い方はできない

この2点を押さえていただけたなら幸いです！