% 展開
展開は比較的単純な処理です。AST の構築が終わった後、かつコンパイラーがプログラムの意味的な理解を構築し始める前のどこかの時点で、コンパイラーはすべてのマクロを展開します。
これには、AST を走査し、マクロ呼び出しを見つけ、それらをその展開結果で置き換えることが含まれます。非マクロの構文拡張の場合、これがどのように行われるかは、その特定の構文拡張次第です。とはいえ、構文拡張は、その展開が完了すると、マクロが通るプロセスとまったく同じプロセスを通ります。
コンパイラーが構文拡張を実行すると、その結果は、コンテキストに基づいて、限られた構文要素のいずれかとしてパース可能であることを期待します。たとえば、モジュールスコープでマクロを呼び出した場合、コンパイラーはその結果をアイテムを表す AST ノードへとパースします。式位置でマクロを呼び出した場合、コンパイラーはその結果を式の AST ノードへとパースします。
実際には、構文拡張の結果を次のいずれかに変換できます。
- 式、
- パターン、
- 0 個以上のアイテム、
- 0 個以上の
implアイテム、または - 0 個以上の文。
言い換えると、マクロを呼び出せる場所によって、その結果が何として解釈されるかが決まります。
コンパイラーはこの AST ノードを受け取り、マクロの呼び出しノードを出力ノードで完全に置き換えます。これは構造上の操作であり、テキスト上の操作ではありません!
たとえば、次を考えてみましょう。
let eight = 2 * four!();
この部分的な AST は次のように可視化できます。
┌─────────────┐
│ Let │
│ name: eight │ ┌─────────┐
│ init: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────────┐
│ LitInt │╶┘ └─────────┘ └╴│ Macro │
│ val: 2 │ │ name: four │
└────────┘ │ body: () │
└────────────┘
コンテキストから、four!() は式へ展開される必要があります(初期化子は式であることしかできません)。したがって、実際の展開結果が何であれ、それは完全な式として解釈されます。この場合、four! は式 1 + 3 へ展開されるように定義されていると仮定します。その結果、この呼び出しを展開すると AST は次のように変化します。
┌─────────────┐
│ Let │
│ name: eight │ ┌─────────┐
│ init: ◌ │╶─╴│ BinOp │
└─────────────┘ │ op: Mul │
┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌─────────┐
│ LitInt │╶┘ └─────────┘ └╴│ BinOp │
│ val: 2 │ │ op: Add │
└────────┘ ┌╴│ lhs: ◌ │
┌────────┐ │ │ rhs: ◌ │╶┐ ┌────────┐
│ LitInt │╶┘ └─────────┘ └╴│ LitInt │
│ val: 1 │ │ val: 3 │
└────────┘ └────────┘
これは次のように書き表せます。
let eight = 2 * (1 + 3);
展開結果には含まれていなかったにもかかわらず、括弧を追加している点に注意してください。コンパイラーは常に、マクロの展開結果を単なるトークン列としてではなく、完全な AST ノードとして扱うことを思い出してください。別の言い方をすれば、複雑な式を明示的に括弧で囲まなかったとしても、コンパイラーがその結果を「誤解釈」したり、評価順序を変えたりすることはありません。
マクロ展開が AST ノードとして扱われることを理解するのは重要です。この設計にはさらに 2 つの意味があるためです。
- 呼び出し位置の数が限られていることに加えて、マクロはその位置でパーサーが期待する種類の AST ノードにしか展開できません。
- 上記の結果として、マクロは不完全な構造や構文的に不正な構造へ展開することは絶対にできません。
展開について、さらにもう 1 つ注意すべきことがあります。構文拡張が、別の構文拡張呼び出しを含むものへ展開された場合に何が起こるかです。たとえば、four! の別の定義を考えてみましょう。これが 1 + three!() へ展開されたらどうなるでしょうか?
let x = four!();
これは次へ展開されます。
let x = 1 + three!();
これは、コンパイラーが展開結果に追加のマクロ呼び出しがないかを確認し、それらを展開することで解決されます。したがって、2 回目の展開ステップによって、上記は次のようになります。
let x = 1 + 3;
ここでの要点は、展開は「パス」で行われるということです。すべての呼び出しを完全に展開するために必要なだけ実行されます。
ただし、厳密にはそうではありません。実際には、コンパイラーは、諦める前に実行するこのような再帰的パスの回数に上限を課しています。これはマクロ再帰制限として知られており、デフォルトは 32 です。32 回目の展開にマクロ呼び出しが含まれている場合、コンパイラーは再帰制限を超えたことを示すエラーで中止します。
この制限は #![recursion_limit="…"] 属性を使用して引き上げることができますが、クレート全体に対して行われる必要があります。一般に、可能な限りマクロをこの制限未満に保つようにすることが推奨されます。