Rustの名前解決(3/5) パス解決
概要: Rustの名前解決の詳細について解説する。本記事では、パス解決について説明する。
パス解決とは
Rustで foo::bar のようにパスを書いたとき、これが具体的にどこで定義されているどのアイテムの、どの特殊化を指すのかは、自明ではない。これを確定させるのがパス解決である。パス解決が行われると、パスは Def や Ty に紐付けられる。これにより、インポートによる曖昧性がなくなる。
PathとQPath
パスはhirの Path または QPath に解決される。
Path は例えば、 std::vec::Vec<T> のような通常のパス(型パラメーターを含んでもよい)である。一方 QPath はこれに加えて、 <BufReader<R> as Read>::read のように、Selfが関係するアイテムも含んでいる。
特に、型に関連づけられているがトレイト情報のないアイテムはlowering時点のパス解決では完全には解決できず、型検査の後に解決される。
パス解決のタイミング
次のような順番でパスが解決される。
useは、インポート解決中に同時に解決される。- loweringの前に、アイテム定義(関連アイテム以外)までのパスの解決が行われ、
def_mapが構築される。 - lowering時に、パスが定義部分と関連アイテム部分に分割され、
PathまたはQPathに解決される。 - 型検査後に、関連アイテムが解決される。
use の解決
インポート解決中に、resolve_import が resolve_path を呼び出している。これにより、 use のパスが解決される。
def_map の作成
lowering前に、rustc_resolveのビジター がAST中に出現するパスを走査し、定義までのパスの解決を行う。
パスの解決結果は、 Def と非負整数の組である。例えば、以下のようなコードを考える。
fn main() { use std::fs; let f = fs::File::open("hoge.txt").unwrap(); }
ここで fs::File::open は、 ::std::fs::File に対応するDefと、非負整数1で表される。この1は、関連アイテム部分の流さが1 (openで1個)であることをあらわしている。
この解決結果を、NodeId から引けるHashMapに保存する。この NodeId は、このパスのAST上の出現に対して割り当てられているものを使う。上の例では、
let f = fs::File::open("hoge.txt").unwrap();
^^^^^^^^^^^^^^ この部分
のノードに割り当てられているNodeIdが使われる。
lowering中のパス解決
lowering中のパス解決はlower_qpathで行われる。 def_map の情報をもとにパスの前半を切り出し、パスの後半(関連アイテム部分)をさらに解析する。例えば、 std::vec::Vec::<T>::IntoIter::Item::clone の場合、前半は std::vec::Vec<T> で、後半は IntoIter, Item, clone となっている。これらはすべて直前の型に関連づけられたアイテム(それぞれ、関連型、関連型、メソッド)であるから、 <<<std::vec::Vec<T>>::IntoIter>::Item>::clone と解釈される。
パス解決関数
パス解決に用いられる関数は場所により異なる。 def_map への記録は record_def 関数 により行われるが、その引数の多くは resolve_qpath で解決される。
QSelfとQPath処理
パス解決前のQPathは、QSelfという追加情報を持つパスとして表されている。 syntax::ast::QSelfは以下のように定義されている。
/// The explicit Self type in a "qualified path". The actual /// path, including the trait and the associated item, is stored /// separately. `position` represents the index of the associated /// item qualified with this Self type. /// /// ```rust,ignore /// <Vec<T> as a::b::Trait>::AssociatedItem /// ^~~~~ ~~~~~~~~~~~~~~^ /// ty position = 3 /// /// <Vec<T>>::AssociatedItem /// ^~~~~ ^ /// ty position = 0 /// ``` #[derive(Clone, PartialEq, Eq, RustcEncodable, RustcDecodable, Hash, Debug)] pub struct QSelf { pub ty: P<Ty>, pub position: usize }
ここの説明にあるように、 <A as T>::Fooのような式を書いたとき、ASTでは T::Foo に追加情報として (A, 1) という情報が与えられたものと考える。positionが 0 の場合は特殊で、トレイトが省略されたものとみなす。
QSelfを含むパスのパス解決を担当するのが resolve_qpath である。 resolve_qpath の基本動作は、 QSelf があるならばそこの情報をもとにし、なけれba resolve_path に移譲する、というものである。
u32 や f64 などのプリミティブ型は、 std::u32 のように同名のモジュールを持つ。この処理もここで行われる。
パス解決
パス解決の本体は、その次の resolve_path にある。これはざっくり言うと左側のパス要素から順に処理して進んでいくというものである。
具体的には以下のことが読み取れる。
- 最初が
selfやsuperで開始されるときは、識別子のある字句上のモジュールのnormal ancestorから開始する。 superがパスの冒頭部に連続して現れるときは、出現回数だけ(normal ancestorの)親モジュールに移動する。::から始まるときは、ルートから開始する。マクロ変数$crateから始まるときは、$crateが元々あった位置のcrateのルートモジュールが使われる。- 最後の要素の名前空間は原則として指定できる。途中の要素(モジュール例)は型名前空間に属する。
normal ancestor
各モジュールにはnormal ancestorと呼ばれる、他のモジュールへのリンクが存在する。これは、主に次のような特殊なモジュールで使われる。
fn f() -> u32 { 2 } fn main() { { println!("{}", self::f()); } }
ここで、 println! のすぐ外側の {} はパス解決時にはモジュールの一種と見なされている。しかし、ここで self::f の self が指しているのは、 f と main の両方を含むルートモジュールである。このギャップを埋めるのがnormal ancestorである。
明示的な mod 以外のmodでは、大抵、normal ancestorは親のnormal ancestorをそのまま使う。例えば、 build_reduced_graph_for_block を見ると、以下のようになっている。
let module = self.new_module(parent, ModuleKind::Block(block.id), parent.normal_ancestor_id);
まとめ
Rustのパス解決は簡単なようで、実際にはいくつかの理由で複雑になっている。1つは名前空間、1つは相対パスと絶対パス、1つはSelfの指定である。