1
0

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 2

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

Last updated at Posted at 2024-12-20

この記事で伝えたいこと

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

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

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

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

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

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

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

MacroFlow3.drawio.png

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

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

  • 葉: 「識別子」「句読点」「リテラル」などのトークン
  • 枝: 葉を括弧( () または [] または {} )で囲んだグループ
トークン木(The little bookより拝借)
a + b + (c + d[0]) + e

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

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

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

実際にRustの手続きマクロを作成する際に使う proc-macro2 ( と syn & quote ) クレートを活用して、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);
    }
}
実行結果
[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 は、トークンを吐き出すストリームであり、その吐き出す一つ一つの値の型(IntoIterator::Item)は proc_macro2::TokenTree つまりトークン木になっています。proc_macro2::TokenStream はまさしくRustの手続きマクロの入出力型1になっていますから、「Rustマクロの入出力はトークン木」と言えることがコードから確認できたかと思います!

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

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

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 (コンパイルエラー)
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 (コンパイルエラー)
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点を押さえていただけたなら幸いです!

  1. 正確には proc_macro::TokenStream ですがほぼ同一です。このことについては実際に手続きマクロを書く回で触れたいと思います。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?