ASTにおけるマクロ
先述した通り、Rustにおけるマクロの処理は抽象構文木が構築された後に行われます。 したがって、マクロを呼び出すのに使う構文は言語の構文上正しいものでなければなりません。 実際、いくつかの「構文拡張(syntax extension)」形式がRustの構文に組み込まれています。 具体的には、以下の4つの形式があります:
# [ $arg ]
例:#[derive(Clone)]
,#[no_mangle]
, …# ! [ $arg ]
例:#![allow(dead_code)]
,#![crate_name="blang"]
, …$name ! $arg
例:println!("Hi!")
,concat!("a", "b")
, …name ! $arg0 $arg1
例:macro_rules! dummy { () => {}; }
最初の2つは属性で、アイテム、式、文にアノテーションをつけるものです。 属性は組み込み属性、proc-macro属性、derive属性に分類できます。 proc-macro属性とderive属性はRustが提供する第二のマクロシステムである手続き的マクロによって実装できます。 一方、組み込み属性はコンパイラが実装している属性です。
3つめの形式 $name ! $arg
は関数形式マクロです。これはmacro_rules!
によるマクロ、macro
によるマクロ、そして手続き的マクロを呼び出すのに使えます。
macro_rules!
で定義されたマクロだけに限定されているわけではないことに注意してください: これは一般的な構文拡張の形式なのです。
例えば、format!
はmacro_rules!
によるマクロですが、(format!
を実装するのに用いられている)format_args!
はそうではなく、コンパイラ組み込みのマクロです。
4つめの形式は原則としてマクロに対して使える形式ではありません。
実際、この形式が使われるのはmacro_rules!
構文そのものに対してのみに限られます。
さて、3番めの形式について、Rustのパーサはいかにして考えうるすべての構文拡張に対して ($name ! $arg)
の$arg
の中身がどうなっているのかを知るのでしょうか?
知る必要がない、というのが答えです。
代わりに、構文拡張の呼び出しにおける引数部分は単一のトークン木になります。
より具体的にいえば、単一の葉でないトークン木、すなわち(...)
, [...]
または {...}
になります。
この知識があれば、パーサが以下の呼び出し形式をどのように理解するのかがはっきりわかるはずです:
bitflags! {
struct Color: u8 {
const RED = 0b0001,
const GREEN = 0b0010,
const BLUE = 0b0100,
const BRIGHT = 0b1000,
}
}
lazy_static! {
static ref FIB_100: u32 = {
fn fib(a: u32) -> u32 {
match a {
0 => 0,
1 => 1,
a => fib(a-1) + fib(a-2)
}
}
fib(100)
};
}
fn main() {
use Color::*;
let colors = vec![RED, GREEN, BLUE];
println!("Hello, World!");
}
上記の呼び出し形式はありとあらゆる種類のRustコードを含んでいるように見えるかもしれませんが、パーサが見ているのはただの無意味なトークン木の集まりにすぎません。 このことをよりはっきりさせるために、すべての構文上の「ブラックボックス」を⬚に置き換えてみると、次のようになります:
bitflags! ⬚
lazy_static! ⬚
fn main() {
let colors = vec! ⬚;
println! ⬚;
}
繰り返しになりますが、パーサは⬚に関して何の仮定も置きません。⬚に含まれるトークンを覚えておくだけで、それを理解しようとすることはありません。 すなわち⬚に入るのは何であっても、無効なRustコードであってもかまわないのです! どうしてこれがいいことなのかについては、あとで説明します。
さて、このことは形式1と2における$arg
、そして形式4における2つの引数にも当てはまるのでしょうか? 大体はあっています。
形式1と2の$arg
は少し違っていて、そのままトークン木になるわけではなく、後に=
とリテラル式かトークン木が続く単純パス(simple path) になります。
これについては、手続き的マクロについてのしかるべき章で深堀りしていきます。
ここで重要なのは、この形式においても入力を表現するのにトークン木を用いているということです。
4つめの形式は概してより特別で、非常に限定された構文(とはいえ、これもトークン木を用いる)のみを受け付けます。
この形式の詳細は現時点では重要ではないので、重要になるまで脇においておきましょう。
以上のことから得られる結論は次の通りです:
- Rustには複数の種類の構文拡張が存在する。
- ただ
$name! $arg
という形をした何かを見るだけでは、それがどの種類の構文拡張なのかを知ることはできない。それはmacro_rules!
によるマクロかもしれないし、手続き的マクロ、さらには組み込みのマクロでさえありうる。 - すべての
!
がつくマクロ呼び出し (形式3) の入力は単一の葉でないトークン木である。 - 構文拡張は抽象構文木の一部としてパースされる。
最後の点が最も重要で、それはこのことが重大な意味合いを含むためです。 構文拡張は抽象構文木にパースされるため、明示的にサポートされた場所にしか現れ得ないのです。 具体的には、構文拡張は次の場所に書くことができます:
- パターン
- 文
- 式
- アイテム (
impl
アイテムを含む) - 型
このリストに含まれないものの例:
- 識別子
- match式のアーム
- 構造体のフィールド
前者のリストに含まれない場所で構文拡張を使うことは、絶対に、どう足掻こうとも不可能です。