% キャプチャと展開の再考
パーサーがキャプチャのためにトークンの消費を開始すると、停止したりバックトラックしたりすることはできません。これは、どのような入力が与えられても、次のマクロの 2 番目のルールが決してマッチできないことを意味します。
macro_rules! dead_rule {
($e:expr) => { ... };
($i:ident +) => { ... };
}
このマクロが dead_rule!(x+) として呼び出された場合に何が起こるかを考えてみましょう。インタープリターは最初のルールから開始し、入力を式としてパースしようとします。最初のトークン(x)は式として有効です。2 番目のトークンも式の中で同様に有効であり、二項加算ノードを形成します。
この時点で、加算の右辺が存在しないため、パーサーは諦めて次のルールを試す、と予想するかもしれません。しかし実際には、パーサーはパニックを起こし、構文エラーを示してコンパイル全体を中止します。
そのため、一般的にマクロルールは最も具体的なものから最も一般的なものへと記述することが重要です。
将来の構文変更によってマクロ入力の解釈が変わることを防ぐために、macro_rules! は各種キャプチャの後に続けられるものを制限しています。Rust 1.3 時点での完全な一覧は次のとおりです。
item: 任意。block: 任意。stmt:=>,;pat:=>,=ifinexpr:=>,;ty:,=>:=>;asident: 任意。path:,=>:=>;asmeta: 任意。tt: 任意。
さらに、macro_rules! は一般に、内容が衝突しない場合であっても、繰り返しの後に別の繰り返しが続くことを禁止します。
置換に関してよく人を驚かせる点の 1 つは、置換は非常にそう見えるにもかかわらず、トークンベースではないということです。簡単な実演を示します。
macro_rules! capture_expr_then_stringify { ($e:expr) => { stringify!($e) }; } fn main() { println!("{:?}", stringify!(dummy(2 * (1 + (3))))); println!("{:?}", capture_expr_then_stringify!(dummy(2 * (1 + (3))))); }
stringify! は組み込みの構文拡張であり、単に与えられたすべてのトークンを受け取り、それらを 1 つの大きな文字列に連結します。
実行時の出力は次のとおりです。
"dummy ( 2 * ( 1 + ( 3 ) ) )"
"dummy(2 * (1 + (3)))"
同じ入力であるにもかかわらず、出力が異なることに注目してください。これは、最初の呼び出しがトークンツリーの列を文字列化しているのに対し、2 番目はAST の式ノードを文字列化しているためです。
違いを別の形で可視化すると、最初のケースで stringify! マクロが呼び出される際に受け取るものは次のようになります。
«dummy» «( )»
╭───────┴───────╮
«2» «*» «( )»
╭───────┴───────╮
«1» «+» «( )»
╭─┴─╮
«3»
…そして、2 番目のケースで呼び出される際に受け取るものは次のようになります。
« »
│ ┌─────────────┐
└╴│ Call │
│ fn: dummy │ ┌─────────┐
│ args: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ LitInt │╶┘ └─────────┘ └╴│ BinOp │
│ val: 2 │ │ op: Add │
└────────┘ ┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────┐
│ LitInt │╶┘ └─────────┘ └╴│ LitInt │
│ val: 1 │ │ val: 3 │
└────────┘ └────────┘
見てのとおり、トークンツリーは正確に1 つだけであり、それには capture_expr_then_stringify! 呼び出しへの入力からパースされた AST が含まれています。したがって、出力に見えているものは文字列化されたトークンではなく、文字列化された AST ノードです。
これにはさらに別の影響があります。次の例を考えてみましょう。
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)); }
出力は次のとおりです。
got an identifier
got an addition
got something else
got something else
got something else
got something else
入力を AST ノードとしてパースすることで、置換された結果は分解不能になります。すなわち、その内容を調べたり、それに対して再びマッチングしたりすることはできません。
特に混乱しやすい別の例を示します。
macro_rules! capture_then_what_is { (#[$m:meta]) => {what_is!(#[$m])}; } macro_rules! what_is { (#[no_mangle]) => {"no_mangle attribute"}; (#[inline]) => {"inline attribute"}; ($($tts:tt)*) => {concat!("something else (", stringify!($($tts)*), ")")}; } fn main() { println!( "{}\n{}\n{}\n{}", what_is!(#[no_mangle]), what_is!(#[inline]), capture_then_what_is!(#[no_mangle]), capture_then_what_is!(#[inline]), ); }
出力は次のとおりです。
no_mangle attribute
inline attribute
something else (# [ no_mangle ])
something else (# [ inline ])
これを避ける唯一の方法は、tt または ident の種類を使ってキャプチャすることです。それ以外のものでキャプチャした場合、その後その結果に対してできる唯一のことは、それを直接出力へ置換することだけです。