名前解決
前の章では、すべてのマクロが展開された状態で 抽象構文木 (AST)
がどのように構築されるかを見ました。
また、それを行うには、インポートとマクロ名を解決するために
ある程度の名前解決が必要になることも見ました。
この章では、実際にそれがどのように行われるのか、そしてそれ以上の内容を示します。
実際には、マクロ展開中に完全な名前解決を行うわけではありません。その時点では
インポートとマクロだけを解決します。
これは、そもそも何を展開するのかを知るために必要です。
その後、AST 全体が得られた後で、クレート内のすべての名前を解決するために
完全な名前解決を行います。
これは rustc_resolve::late で行われます。
マクロ展開中とは異なり、この後期展開では、新しい名前を追加できないため、
名前の解決は一度だけ試みれば十分です。
名前の解決に失敗した場合、それはコンパイラーエラーになります。
名前解決は複雑です。さまざまな名前空間(例: マクロ、値、型、ライフタイム)があり、名前は異なる(ネストした) スコープで有効になる場合があります。 また、名前の種類が異なれば解決の失敗の仕方も異なり、 スコープが異なれば失敗の起こり方も異なります。 たとえば、モジュールスコープでは、 失敗とは、そのモジュール内に未展開のマクロも未解決の glob インポートも 存在しないことを意味します。 一方、関数本体のスコープでは、失敗するには、対象の名前が 現在のブロック、すべての外側のスコープ、およびグローバルスコープに 存在しないことが必要です。
基本
プログラム内では、変数、型、関数などを名前で参照します。 これらの名前は常に一意であるとは限りません。 たとえば、次の有効な Rust プログラムを見てください。
#![allow(unused)]
fn main() {
type x = u32;
let x: x = 1;
let y: x = 2;
}
3 行目の x が型(u32)なのか値(1)なのかを、どうすれば判断できるのでしょうか?
これらの衝突は名前解決中に解決されます。
この具体的なケースでは、
名前解決により、型名と変数名は別々の名前空間に存在するため、
共存できるものとして定義されます。
Rust における名前解決は 2 段階のプロセスです。
第 1 段階は
macro 展開中に実行され、
モジュールのツリーを構築し、インポートを解決します。
マクロ展開と名前解決は、ResolverAstLoweringExt トレイトを介して
相互に通信します。
第 2 段階への入力は、入力ファイルをパースし、
macros を展開することで生成された構文木です。
この段階では、ソース内のすべての名前から、
その名前が導入された関連箇所へのリンクが生成されます。
また、タイプミスの候補、インポートすべきトレイト、
未使用項目に関する lint など、有用なエラーメッセージも生成します。
第 2 段階(Resolver::resolve_crate)が正常に実行されると、
コンパイルの残りの部分が、現在存在する名前について問い合わせるために使用できる
一種のインデックスが作成されます
(hir::lowering::Resolver インターフェイスを通じて)。
名前解決は rustc_resolve クレート内にあり、その大部分は
lib.rs に、いくつかのヘルパーやシンボル型固有のロジックは他のモジュールにあります。
名前空間
異なる種類のシンボルは異なる名前空間に存在します。たとえば、型は 変数と衝突しません。 通常これは起こりません。なぜなら、変数は 小文字で始まり、型は大文字で始まるからです。しかし、これは単なる 慣習です。 次は、コンパイル可能な(警告付きの)合法な Rust コードです。
#![allow(unused)]
fn main() {
type x = u32;
let x: x = 1;
let y: x = 2; // ほら、ここでも x はまだ型です。
}
これに対処し、またこれらの名前空間ごとに少し異なるスコープ規則に対応するため、 リゾルバーはそれらを分離したままにし、それぞれに対して別々の構造を構築します。
言い換えると、コードが名前空間について述べるとき、それはモジュール階層を 意味しているのではなく、型、値、マクロの対比を意味しています。
スコープと rib
名前はソースコード内の特定の領域でのみ可視です。 これにより階層構造が形成されますが、 必ずしも単純なものとは限りません。あるスコープが 別のスコープの一部であるからといって、外側のスコープで可視な名前が 内側のスコープでも可視であるとは限らず、同じものを参照するとも限りません。
これに対処するために、コンパイラーは Rib という概念を導入します。
これはスコープの抽象化です。
可視な名前の集合が変わる可能性があるたびに、
新しい Rib がスタックにプッシュされます。
これが起こり得る場所には、たとえば次のものが含まれます。
- 明らかな場所、つまりブロックを囲む波括弧、関数の境界、 モジュール。
let束縛の導入。これは同じ名前を持つ別の束縛をシャドーイングできます。- マクロ展開の境界。これはマクロ衛生に対処するためです。
名前を検索するとき、ribs のスタックは最も内側から
外側へと走査されます。
これにより、その名前の最も近い意味(他の何かによって
シャドーイングされていないもの)を見つけやすくなります。
外側の Rib への遷移は、
どの名前が使用可能かにも影響する場合があります。ネストした関数(クロージャではありません)がある場合、
内側の関数は外側の関数のパラメーターやローカル束縛にアクセスできません。
通常のスコープ規則では可視であるはずだとしてもです。
例を示します。
#![allow(unused)]
fn main() {
fn do_something<T: Default>(val: T) { // <- 型と値の両方で新しい rib (1)
// `val` はアクセス可能で、ヘルパー関数も同様
// `T` はアクセス可能
let helper = || { // ブロック上の新しい rib (2)
// ここでは `val` はアクセス可能
}; // (2) の終了、`helper` 上の新しい rib (3)
// `val` はアクセス可能、`helper` 変数が `helper` 関数をシャドーイングする
fn helper() { // <- 型と値の両方で新しい rib (4)
// ここでは `val` はアクセスできない、(4) はローカルに対して透過的ではない
// ここでは `T` はアクセスできない
} // (4) の終了
let val = T::default(); // 新しい rib (5)
// ここでの `val` は変数であり、パラメーターではない
} // (5)、(3)、(1) の終了
}
異なる名前空間の規則は少し異なるため、各名前空間は
他の名前空間と並行して構築される、独立した専用の Rib スタックを持ちます。
さらに、ローカルラベル(例: ループやブロックの名前)用の Rib スタックもありますが、
これはそれ自体では完全な名前空間ではありません。
全体的な戦略
クレート全体の名前解決を実行するために、構文木を
トップダウンに走査し、遭遇したすべての名前を解決します。
これはほとんどの種類の名前に対して機能します。
なぜなら、名前が使用される時点では、それはすでに Rib
階層に導入されているからです。
これにはいくつか例外があります。
項目は少し扱いが難しいです。なぜなら、遭遇する前であっても
使用できるため、各ブロックはまず項目をスキャンして
その Rib を埋める必要があるからです。
さらに問題となるものとして、再帰的な不動点解決を必要とするインポートや、 コードの残りを処理する前に解決して展開する必要があるマクロがあります。
したがって、解決は複数の段階で実行されます。
投機的なクレート読み込み
有用なエラーを提供するため、rustc はパスが見つからない場合に、それらをスコープにインポートすることを提案します。 これはどのように行われるのでしょうか? すべてのクレートのすべてのモジュールを調べ、候補となる一致を探します。 これには、まだ読み込まれていないクレートさえ含まれます!
まだ読み込まれていないインポート候補を含めるためにクレートを積極的に読み込むことは、投機的なクレート読み込み と呼ばれます。これは、その過程で発生したエラーを報告すべきではないためです。読み込みを決定したのはユーザーではなく rustc_resolve です。
これを行う関数は lookup_import_candidates で、rustc_resolve::diagnostics にあります。
投機的な読み込みとユーザーによって開始された読み込みを区別するために、rustc_resolve は record_used パラメーターを受け渡します。この値は、読み込みが投機的な場合は false です。
TODO: #16
これは、コードを学ぶ最初の段階の結果です。 間違いなく不完全で、詳細も十分ではありません。 また、場所によっては不正確な可能性もあります。 それでも、おそらくそこで何が起こっているかについての有用な最初の道しるべにはなります。
- それは具体的に何にリンクし、それはコンパイルの後続ステージでどのように公開され、消費されるのか?
- 誰がそれを呼び出し、実際にどのように使用されるのか。
- これはパスであり、その結果だけが使用されるのか、それともインクリメンタルに計算できるのか?
- 全体的な戦略の説明がやや曖昧。
Ribという名前はどこから来たのか?- これは独自のテストを持っているのか、それとも何らかの e2e テストの一部としてのみテストされているのか?