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の指定である。