Rustの名前解決(3/5) パス解決

概要: Rustの名前解決の詳細について解説する。本記事では、パス解決について説明する。

  1. 名前解決にかかわる構文
  2. インポート解決
  3. パス解決
  4. メソッド記法とメンバ変数と関連アイテムの解決
  5. 可視性判定

パス解決とは

Rustで foo::bar のようにパスを書いたとき、これが具体的にどこで定義されているどのアイテムの、どの特殊化を指すのかは、自明ではない。これを確定させるのがパス解決である。パス解決が行われると、パスは DefTy に紐付けられる。これにより、インポートによる曖昧性がなくなる。

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_importresolve_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 に移譲する、というものである。

u32f64 などのプリミティブ型は、 std::u32 のように同名のモジュールを持つ。この処理もここで行われる。

パス解決

パス解決の本体は、その次の resolve_path にある。これはざっくり言うと左側のパス要素から順に処理して進んでいくというものである。

具体的には以下のことが読み取れる。

  • 最初が selfsuper で開始されるときは、識別子のある字句上のモジュールのnormal ancestorから開始する。
  • super がパスの冒頭部に連続して現れるときは、出現回数だけ(normal ancestorの)親モジュールに移動する。
  • :: から始まるときは、ルートから開始する。マクロ変数 $crate から始まるときは、 $crateが元々あった位置のcrateのルートモジュールが使われる。
  • 最後の要素の名前空間は原則として指定できる。途中の要素(モジュール例)は型名前空間に属する。

normal ancestor

各モジュールにはnormal ancestorと呼ばれる、他のモジュールへのリンクが存在する。これは、主に次のような特殊なモジュールで使われる。

fn f() -> u32 { 2 }

fn main() {
    {
        println!("{}", self::f());
    }
}

ここで、 println! のすぐ外側の {} はパス解決時にはモジュールの一種と見なされている。しかし、ここで self::fself が指しているのは、 fmain の両方を含むルートモジュールである。このギャップを埋めるのが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の指定である。