マクロ展開
Rustには非常に強力なマクロシステムがあります。 前の章では、 パーサーが(一時的なプレースホルダーを使って)展開対象のマクロを取り分ける方法を見ました。 この章では、それらのマクロを反復的に展開し、 未展開のマクロがない(またはコンパイルエラーになる)クレートの完全な 抽象構文木 (AST)を得るまでのプロセスについて説明します。
まず、マクロの出力を展開しASTへ統合するアルゴリズムについて説明します。 次に、ハイジーンデータがどのように収集されるかを見ていきます。 最後に、さまざまな種類のマクロの展開に関する詳細を見ていきます。
以下で説明するアルゴリズムやデータ構造の多くはrustc_expandにあり、
基礎的なデータ構造はrustc_expand::baseにあります。
また、cfgとcfg_attrは他のマクロとは特別に扱われ、
rustc_expand::configで処理されます。
展開とAST統合
まず、展開はクレートレベルで行われます。
クレートの生のソースコードが与えられると、
コンパイラーは、すべてのマクロが展開され、すべての
モジュールがインライン化された巨大なASTなどを生成します。このプロセスの主なエントリポイントは
MacroExpander::fully_expand_fragmentメソッドです。
いくつかの例外を除き、
このメソッドはクレート全体に対して使用します(エッジケースの展開問題に関するより詳しい議論については、
下記の「先行展開」を参照してください)。
大まかに言うと、fully_expand_fragmentは反復で動作します。
未解決のマクロ呼び出し(つまり、まだ定義が見つかっていないマクロ)の
キューを保持します。
キューからマクロを1つ選び、それを解決し、展開し、元に統合し直すことを繰り返し試みます。
反復で進捗がない場合、これはコンパイルエラーを表します。
以下がアルゴリズムです:
- 未解決マクロの
queueを初期化します。 queueが空になるまで繰り返します(または進捗がなくなるまで。これはエラーです):- 部分的に構築されたクレート内のインポートを、可能な限り解決します。
- 部分的に構築されたクレートから、マクロの
Invocation(fn風、属性、derive)をできるだけ多く収集し、それらをキューに追加します。 - 先頭の要素をデキューし、それを解決しようとします。
- 解決された場合:
TokenStreamまたは ASTを消費し、(マクロの種類に応じて)TokenStreamまたはAstFragmentを生成する、マクロのエキスパンダー関数を実行します。 (TokenStreamはTokenTreeのコレクションであり、 それぞれはトークン(区切り記号、識別子、リテラル)または 区切り付きグループ(()/[]/{}の内側にあるもの)です)。- この時点で、マクロ自体についてはすべて把握しており、
set_expn_dataを呼び出して、そのプロパティをグローバル データ内に埋めることができます。それはExpnIdに関連付けられた ハイジーンデータです(下記の ハイジーンを参照)。
- この時点で、マクロ自体についてはすべて把握しており、
- そのAST断片を、現在存在しているものの
部分的に構築されたASTに統合します。
ここは本質的に、「トークンのような塊」が
サイドテーブルを伴う、適切で確定したASTになる場所です。
これは次のように行われます:
- マクロがトークンを生成する場合(例: 手続き型マクロ)、それを ASTへパースしますが、これによりパースエラーが発生することがあります。
- 展開中に、
SyntaxContext(階層2)を作成します(下記の ハイジーンを参照)。 - 次の2つのパスは、マクロから新たに展開されたすべてのAST断片に対して
順番に実行されます:
NodeIdはInvocationCollectorによって割り当てられます。 これにより、この新しいAST部分から新しいマクロ呼び出しも収集され、 キューに追加されます。DefCollectorは「Defパス」を作成し、 対応するDefIdを割り当て、縮約グラフも 構築します(リゾルバーの観点からモジュールに名前を配置します)。
- 1つのマクロを展開し、その出力を統合した後、
fully_expand_fragmentの次の反復に進みます。
- 解決されない場合:
- マクロをキューに戻します。
- 次の反復へ進みます…
エラーリカバリー
ある反復で進捗がない場合、コンパイルエラーに到達したことになります
(例: 未定義のマクロ)。診断を生成する意図で、失敗(つまり
未解決のマクロやインポート)からのリカバリーを試みます。
失敗からのリカバリーは、未解決のマクロを
ExprKind::Errへ展開することで行われ、最初のエラーを越えてコンパイルを続行できるようにし、
rustcが元の失敗だけでなく、より多くのエラーを報告できるようにします。
名前解決
ここでは名前解決が関与していることに注意してください。つまり、上記のアルゴリズムではインポートと
マクロ名を解決する必要があります。
これは rustc_resolve::macros で行われます。ここではマクロパスを解決し、それらの
解決結果を検証し、さまざまなエラー(例: 「not found」「found, but
it’s unstable」「expected x, found y」)を報告します。
ただし、この時点ではまだ他の名前の解決は試みません。
これは後で行われます。章: 名前解決 で説明します。
先行展開
先行展開 とは、マクロ呼び出し自体を展開する前に、 そのマクロ呼び出しの引数を展開することを意味します。 これは、リテラルを期待するいくつかの特殊な 組み込みマクロに対してのみ実装されています。これらのマクロの一部では、 引数を先に展開することで、ユーザー体験がより滑らかになります。 例として、次を考えてみましょう。
macro bar($i: ident) { $i }
macro foo($i: ident) { $i }
foo!(bar!(baz));
遅延展開では、まず foo! を展開します。
先行展開では、まず bar! を展開します。
先行展開は、Rust で一般に利用可能な機能ではありません。
先行展開をより一般的に実装することは難しいため、ユーザー体験のために、
いくつかの特殊な組み込みマクロに対して実装しています。
組み込みマクロは rustc_builtin_macros に実装されており、標準ライブラリのインポートの注入や
テストハーネスの生成など、いくつかの他の
初期コード生成機能もそこにあります。
AST フラグメントを構築するための追加のヘルパーが rustc_expand::build にいくつかあります。
先行展開は一般に、遅延(通常)展開が行う処理のサブセットを実行します。
これは、通常のようにクレート全体に対してではなく、クレートの一部のみに対して
fully_expand_fragment を呼び出すことで行われます。
その他のデータ構造
展開と統合に関与する、その他の注目すべきデータ構造をいくつか挙げます。
ResolverExpand- クレートの依存関係を分断するために使われるtrait。 これにより、rustc_resolveや それ以外のほぼすべてがrustc_astに依存しているにもかかわらず、resolver サービスをrustc_astで使用できます。ExtCtxt/ExpansionData- さまざまな中間的な展開インフラストラクチャデータを保持します。Annotatable- 属性の対象になれる AST の一部です。型やパターンを除けばAstFragmentとほぼ同じものです。型やパターンは マクロによって生成できますが、属性で注釈付けすることはできません。MacResult- 「多相的な」AST フラグメントです。これは、そのAstFragmentKindに応じて 別のAstFragmentに変換できるものです(つまり、アイテム、 式、パターンなど)。
衛生性と階層
C/C++ のプリプロセッサマクロを使ったことがあれば、 厄介でデバッグが難しい落とし穴がいくつかあることをご存じでしょう。 たとえば、次の C コードを考えてみてください。
#define DEFINE_FOO struct Bar {int x;}; struct Foo {Bar bar;};
// そして、別のどこかで
struct Bar {
...
};
DEFINE_FOO
多くの人はこのような C の書き方を避けます。そして、それには十分な理由があります。これはコンパイルできません。
マクロによって定義された struct Bar が、コード内で定義された struct Bar と名前衝突します。
次の例も考えてみてください。
#define DO_FOO(x) {\
int y = 0;\
foo(x, y);\
}
// そして別の場所で
int y = 22;
DO_FOO(y);
問題がわかりますか?
私たちは foo(22, 0) という呼び出しを生成したかったのですが、実際には
マクロが独自の y を定義したため、foo(0, 0) になってしまいました。
これらはいずれも マクロ衛生性 の問題の例です。 衛生性 は、マクロ内 で定義された名前をどのように扱うかに関係します。 特に、衛生的なマクロシステムは、マクロ内で導入された名前に起因するエラーを防ぎます。 Rust のマクロは衛生的であり、上記のような種類のバグを書けないようになっています。
大まかに言うと、Rust コンパイラ内の衛生性は、 名前が導入され、使用されるコンテキストを追跡することで実現されています。 その後、そのコンテキストに基づいて名前を曖昧さなく区別できます。 マクロシステムの将来の反復では、 そのコンテキストを使用するためのより大きな制御をマクロ作者に提供する予定です。 たとえば、 マクロ作者は、マクロが呼び出されたコンテキストに新しい名前を導入したい場合があります。 あるいは、マクロ作者は マクロ内でのみ使用する変数を定義している場合があります(つまり、その変数はマクロの外から見えてはなりません)。
コンテキストは AST ノードに付加されます。
マクロによって生成されたすべての AST ノードにはコンテキストが付加されています。
さらに、いくつかの脱糖された構文のように、コンテキストが
付加されている他のノードがある場合もあります(マクロ展開されていないノードは、
後述するように、単に「root」コンテキストを持つものと見なされます)。
コンパイラ全体を通じて、コード位置を参照するために rustc_span::Span を使用します。
後で見るように、この構造体にも衛生性情報が付加されています。
マクロ呼び出しと定義はネストできるため、 ノードの構文コンテキストは階層でなければなりません。 たとえば、あるマクロを展開し、生成された出力の中に 別のマクロ呼び出しや定義がある場合、構文 コンテキストはそのネストを反映する必要があります。
しかし実際には、目的に応じて追跡したいコンテキストには いくつかの種類があることがわかっています。 したがって、クレートの衛生性情報を構成する展開階層は、 1 つではなく 3 つ あります。
これらすべての階層には、展開の連鎖内の個々の
要素を識別するための、何らかの「マクロ ID」が必要です。
この ID が ExpnId です。
すべてのマクロは整数 ID を受け取り、新しいマクロ
呼び出しを発見するにつれて 0 から連続して割り当てられます。
すべての階層は ExpnId::root から始まり、これはそれ自身を親に持ちます。
rustc_span::hygiene クレートには、衛生性に関連するすべてのアルゴリズム
(Resolver::resolve_crate_root 内のいくつかのハックを除く)と、
グローバルデータに保持される衛生性および展開に関連する構造体が含まれています。
実際の階層は HygieneData に格納されます。
これは、任意の Ident からコンテキストなしでアクセスできる、衛生性と展開情報を含む
グローバルデータです。
展開順序の階層
最初の階層は、展開の順序、つまり、あるマクロ呼び出しが 別のマクロの出力内にある場合を追跡します。
ここで、階層内の子は「最も内側」のトークンになります。
ExpnData 構造体自体には、グローバルデータを通じて利用できる
マクロ定義とマクロ呼び出しの両方のプロパティの一部が含まれます。
ExpnData::parent は、この階層における子から親へのリンクを追跡します。
例:
macro_rules! foo { () => { println!(); } }
fn main() { foo!(); }
このコードでは、最終的に生成される AST ノードは
root -> id(foo) -> id(println) という階層を持ちます。
マクロ定義の階層
2 つ目の階層は、マクロ定義の順序、つまり、あるマクロを展開しているときに 別のマクロ定義がその出力内に現れる場合を追跡します。 これは少し扱いが難しく、他の 2 つの階層よりも複雑です。
SyntaxContext は、この階層内のチェーン全体を ID によって表します。
SyntaxContextData には、指定された
SyntaxContext に関連付けられたデータが含まれます。主に、そのチェーンをさまざまな方法でフィルタリングした結果のキャッシュです。
SyntaxContextData::parent はここでの子から親への
リンクであり、SyntaxContextData::outer_expns はチェーン内の個々の要素です。
「連結演算子」は、コンパイラコード内の SyntaxContext::apply_mark です。
上で述べた Span は、実際には
コード位置と SyntaxContext のコンパクトな表現にすぎません。
同様に、Ident はインターン化された
Symbol + Span(つまり、インターン化された文字列 + 衛生性データ)にすぎません。
組み込みマクロについては、次のコンテキストを使用します:
SyntaxContext::empty().apply_mark(expn_id)。また、そのようなマクロは
階層のルートで定義されているとみなされます。
proc macro についても同じことを行います。これは、まだクロスクレート衛生性を実装していないためです。
トークンがマクロによって生成される前にコンテキスト X を持っていた場合、
マクロによって生成された後はコンテキスト X -> macro_id を持ちます。
いくつかの例を示します:
例 0:
macro m() { ident }
m!();
ここで、最初はコンテキスト SyntaxContext::root を持っていた ident は、
m によって生成された後、コンテキスト ROOT -> id(m) を持ちます。
例 1:
macro m() { macro n() { ident } }
m!();
n!();
この例では、ident は最初にコンテキスト ROOT を持ち、その後、最初の展開後に ROOT -> id(m)、
さらに ROOT -> id(m) -> id(n) になります。
例 2:
これらのチェーンは最後の要素だけで完全に決定されるわけではないことに注意してください。
言い換えると、ExpnId は SyntaxContext と同型ではありません。
macro m($i: ident) { macro n() { ($i, bar) } }
m!(foo);
すべての展開後、foo はコンテキスト ROOT -> id(n) を持ち、bar はコンテキスト
ROOT -> id(m) -> id(n) を持ちます。
現在、マクロ定義を追跡するためのこの階層は、いわゆる “context transplantation hack” の対象になっています。現代的な(つまり実験的な) マクロは、レガシーな “Macros By Example” (MBE) システムよりも強い衛生性を持っており、その結果、両者の間で奇妙な相互作用が発生する可能性があります。 このハックは、現時点で物事が「そのまま動作する」ようにすることを意図しています。
呼び出し箇所の階層
3 つ目で最後の階層は、マクロ呼び出しの位置を追跡します。
この階層では、ExpnData::call_site が child -> parent リンクです。
例を示します:
macro bar($i: ident) { $i }
macro foo($i: ident) { $i }
foo!(bar!(baz));
最終的な出力内の baz AST ノードについて、展開順序の階層は
ROOT -> id(foo) -> id(bar) -> baz であり、一方で呼び出し箇所の階層は ROOT -> baz です。
マクロバックトレース
マクロバックトレースは、rustc_span 内で、rustc_span::hygiene の衛生性機構を使って
実装されています。
マクロ出力の生成
上では、マクロの出力がクレートの AST にどのように統合されるかを見ました。 また、クレートの衛生性データがどのように生成されるかも見ました。 しかし、実際にはどのようにマクロの出力を生成するのでしょうか。 それはマクロの種類によって異なります。
Rust には 2 種類のマクロがあります:
macro_rules!マクロ(別名 “Macros By Example” (MBE))、および、- 手続き型マクロ(proc macros)。custom derives を含みます。
パース段階では、通常の Rust パーサーは マクロとその呼び出しの内容を取り分けておきます。 後で、これらのコード部分を使用してマクロが展開されます。 ここにある重要なデータ構造/インターフェイス:
SyntaxExtension- 低水準化されたマクロ表現で、TokenStreamまたは AST を別のTokenStreamまたは AST へ変換する展開関数と、安定性やマクロ内で許可される 不安定機能のリストなどの追加データを含みます。SyntaxExtensionKind- 展開関数は複数の異なるシグネチャを持つことがあります (1 つのトークンストリーム、2 つのトークンストリーム、AST の一部などを受け取ります)。 これはそれらを列挙するenumです。BangProcMacro/TTMacroExpander/AttrProcMacro/MultiItemModifier- 展開関数のシグネチャを表すtraitです。
例によるマクロ
MBE には、Rust パーサーとは異なる独自のパーサーがあります。
マクロが展開されるとき、マクロを解析して展開するために MBE パーサーを呼び出すことがあります。
さらに MBE パーサーは、マクロ呼び出しの内容を解析している間に
メタ変数(例: $my_expr)を束縛する必要がある場合、Rust パーサーを呼び出すことがあります。
マクロ展開のコードは compiler/rustc_expand/src/mbe/ にあります。
例
macro_rules! printer {
(print $mvar:ident) => {
println!("{}", $mvar);
};
(print twice $mvar:ident) => {
println!("{}", $mvar);
println!("{}", $mvar);
};
}
ここで $mvar は メタ変数 と呼ばれます。
通常の変数とは異なり、メタ変数は
実行時 に値へ束縛されるのではなく、コンパイル時 に トークン のツリーへ束縛されます。
トークン は文法の単一の「単位」であり、
識別子(例: foo)や句読点(例: =>)などです。EOF のような
特殊なトークンもあり、これはそれ自体でそれ以上トークンがないことを示します。
対応する括弧のような文字((…)、[…]、および {…})から
生じるトークンツリーがあります。これらは開きと閉じ、およびその間のすべてのトークンを含みます
(Rust では括弧のような文字が対応している必要があります)。
マクロ展開がソースファイルの生のバイト列ではなく
トークンストリームに対して動作することで、多くの複雑さが抽象化されます。
マクロ展開器(およびコンパイラの他の多くの部分)は、
コード内の構文構造の正確な行と列ではなく、
コード内でどの構造が使用されているかを考慮します。
トークンを使用することで、どこ かを気にせずに 何 かに注目できます。
トークンの詳細については、本書の Parsing の章を参照してください。
printer!(print foo); // `foo` は変数です
マクロ呼び出しを構文木
println!("{}", foo) へ展開し、その後その構文木を
Display::fmt の呼び出しへ展開するプロセスは、マクロ展開 の一般的な例の 1 つです。
MBE パーサー
マクロパーサーによって行われる MBE 展開には 2 つの部分があります:
- 定義の解析、および、
- 呼び出しの解析。
MBE パーサーは、Earley
parsing algorithm と精神的に似たアルゴリズムを使用するため、
非決定性有限オートマトン(NFA)に基づく正規表現パーサーのようなものと考えています。
マクロパーサーは compiler/rustc_expand/src/mbe/macro_parser.rs で定義されています。
マクロパーサーのインターフェイスは次のとおりです(これは少し簡略化されています):
fn parse_tt(
&mut self,
parser: &mut Cow<'_, Parser<'_>>,
matcher: &[MatcherLoc]
) -> ParseResult
マクロパーサーでは次の項目を使用します:
parser変数は、トークンストリームと解析セッションを含む通常の Rust パーサーの状態への参照です。 トークンストリームは、MBE パーサーに解析を依頼しようとしているものです。 生のトークンストリームを消費し、 メタ変数から対応するトークンツリーへの束縛を出力します。 解析セッションは、パーサーエラーを報告するために使用できます。matcher変数は、トークンストリームと照合したいMatcherLocのシーケンスです。 これらは、照合の前にマクロ定義内の元のトークンツリーから変換されます。
正規表現パーサーの類推では、トークンストリームが入力であり、それを matcher によって定義されたパターンと
照合しています。
ここでの例を使うと、
トークンストリームは例の呼び出し print foo の内部を含むトークンのストリームであり、
matcher はトークン(ツリー)のシーケンス print $mvar:ident である可能性があります。
パーサーの出力は ParseResult で、3 つのケースのうちどれが発生したかを示します:
- 成功: トークンストリームが指定された matcher と一致し、 メタ変数から対応するトークンツリーへの束縛が生成されています。
- 失敗: トークンストリームが matcher と一致せず、 “No rule expected token …” のようなエラーメッセージになります。
- エラー: パーサー内で 何らかの致命的なエラーが発生しています。 たとえば、複数のパターン一致がある場合にこれが発生します。これは、 マクロが曖昧であることを示すためです。
完全なインターフェイスは こちら で定義されています。
マクロパーサーは、通常の正規表現パーサーとほぼまったく同じことを行いますが、
1 つ例外があります。ident、block、expr などの
異なる種類のメタ変数を解析するために、マクロパーサーは通常の
Rust パーサーをコールバックする必要があります。
マクロ定義を解析するコードは compiler/rustc_expand/src/mbe/macro_rules.rs にあります。
マクロパーサーの実装の詳細については、
compiler/rustc_expand/src/mbe/macro_parser.rs のコメントを参照してください。
この例では、呼び出しから得られたトークンストリーム print foo を、
マクロ定義内のルールから先に抽出した matcher print $mvar:ident および print twice $mvar:ident と
照合しようとします。
マクロパーサーが現在の matcher 内で
非終端記号(例: $mvar:ident)に一致する必要がある場所に到達すると、通常の Rust パーサーをコールバックして
その非終端記号の内容を取得します。
この場合、Rust パーサーは ident
トークンを探し、それを見つけて(foo)マクロパーサーへ返します。
その後、マクロパーサーは解析を続行します。
さまざまなルールの matcher のうち、正確に 1 つだけが呼び出しに一致する必要があることに注意してください。 複数の一致がある場合、解析は曖昧です。一方、まったく一致がない場合は、構文エラーです。 ちょうど1つのルールが一致すると仮定すると、マクロ展開は次にそのルールの右辺を転写し、 左辺との照合時に捕捉した任意の一致の値を置換します。
手続き的マクロ
手続き的マクロも解析中に展開されます。 ただし、コンパイラ内にパーサーを持つのではなく、proc macro はカスタムの サードパーティ製クレートとして実装されます。 コンパイラは proc macro クレートと、 それらの中で特別に注釈された関数(つまり proc macro そのもの)をコンパイルし、 トークンのストリームを渡します。 proc macro はその後、トークンストリームを変換し、 新しいトークンストリームを出力できます。これは AST に合成されます。
proc macro で使用されるトークンストリーム型は_安定_しているため、rustc はそれを内部では使用しません。
コンパイラの(不安定な)トークンストリームは
rustc_ast::tokenstream::TokenStream で定義されています。
これは rustc_expand::proc_macro と rustc_expand::proc_macro_server で、
安定版の proc_macro::TokenStream へ、またその逆へ変換されます。
Rust ABI は現在不安定であるため、この変換には C ABI を使用します。
カスタム導出
カスタム導出は特殊な種類の proc macro です。
Macros By Example と Macros 2.0
MBE システムを改善するための、古く、ほとんど文書化されていない取り組みがあります。 これにより、より多くの hygiene 関連機能、よりよいスコープと可視性の ルールなどを与えます。内部的には、これは現在の MBE と同じ仕組みを使用しますが、 いくつかの追加の構文糖衣を備えており、名前空間内に存在することが許可されています。