読者です 読者をやめる 読者になる 読者になる

Rustの名前解決(4/5) メソッド記法とメンバ変数と関連アイテムの解決

概要: Rustの名前解決の詳細について解説する。本記事では、型情報を必要とする名前の解決を説明する。

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

曖昧性が生じる例

この記事で扱うのは、型情報がないと曖昧性があるような名前の解決である。まずは例を挙げる。

メソッド記法は以下のような曖昧性がある。

trait Foo {
    fn f(&self) { println!("Foo"); }
}
trait Bar {
    fn f(&self) { println!("Bar"); }
}
struct A;
impl Foo for A {}

fn main() {
    let x = A;
    x.f();
}

この例では、 A::f()Foo::fBar::f のいずれを指しているかを決定するために、 x の型を決定した上でトレイト実装を検索する必要がある。

メンバ変数は以下のような曖昧性がある。

struct A {
    m: u32,
}
struct B {
    m: u16,
}

fn main() {
    let x = A { m: 0 };
    println!("{}", std::mem::size_of_val(&x.m));
}

この例では、 x.mAB のどちらのメンバ変数 m であるかを決定するために、 x の型を決定する必要がある。

関連型(関連アイテムの一種)は以下のような曖昧性がある。

trait Foo {
    type X;
}
trait Bar {
    type X;
}

fn f<T:Foo>() { println!("{}", std::mem::size_of::<T::X>()); }
fn g<T:Bar>() { println!("{}", std::mem::size_of::<T::X>()); }

struct A;
impl Foo for A {
    type X = u16;
}
impl Bar for A {
    type X = u32;
}

fn main() {
    f::<A>();
}

この例では、 T::X<T as Foo>::X なのか <T as Bar>::X なのかを決定するために、 T のtrait boundを参照する必要がある。

メソッド(関連アイテムの一種)は以下のような曖昧性がある。

trait Foo {
    fn f() { println!("Foo"); }
}
trait Bar {
    fn f() { println!("Bar"); }
}
struct A;
impl Foo for A {}

fn main() {
    A::f();
}

この例では、 A::f()Foo::fBar::f のいずれを指しているかを決定するために、トレイト実装を検索する必要がある。

トレイトの列挙

以下に挙げる名前解決のうちの一部では、「スコープ内にある利用可能なトレイト」の一覧を出す必要のあるものがある。この処理は、assemble_extension_candidates_for_traits_in_scopeで行われている。この一覧は、 def_map と同様の trait_map という変数に計算済みのものがあり、これを取り出している。

trait_map の計算は rustc_resolve::Resolver::get_traits_containing_item で行われている。これによると、検索範囲は

  • 自身のいるモジュール
  • 自身のいるモジュールの祖先
  • prelude (明示的に除外しない限り)

であることがわかる。

この中で、 所望の識別子を含んでいるトレイトのみが、 trait_map に追加されている。

所望の識別子を含んでいても、もとの型がそのトレイトを実装していなければ採択されない。この処理は consider_candidates 内の consider_probe で行われている。

pick_methodによると、上記の条件を満たしたトレイトが複数あると基本的にエラーになる。ただし、inherent implとtrait implで衝突した場合は、inherent implが優先される。

メソッド記法の解決

メソッド記法の解決はやや複雑であり、「名前解決」から離れる面もあるためこの記事では深入りしない。メソッド記法を処理する部分のREADME にそれなりに説明がある。

要点は、自動デリファレンスと自動リファレンスやunsizingなどの自動変換によりいくつかの候補となる型が生成され、それらに優先順位がつけられる。それぞれの型について、上に挙げたようなトレイトの検索が行われる。

メンバ変数の解決

rustc_typeck::checkcheck_field で解決される。これが呼ばれた時点で、もとの構造体の型が判明していると仮定している。

関連型の解決

関連型の解決はrustc_typeck::astconv::AstConv::ast_ty_to_tyで行われる。ここではまず、 <QSelf>::AssocTyp における <QSelf> が、解決済みのパスであることをチェックしている。解決済みのパスならば、紐づけられた Def を取得する。

その後、 rustc_typeck::astconv::AstConv::associated_path_def_to_ty に処理がうつる。ここでは、得られた <QSelf> のDefによりさらに条件分岐している。コメントにある通り、ここで出てくる <QSelf> は実は Self か型パラメーターでなければならない

Self や型パラメーターの場合は、このパラメーターが導入された箇所で、 where 等によるtrait boundが与えられているはずなので、そこからtraitを検索する。所望の関連型を所有しているトレイトがちょうど1つあれば、それを答えとする。

メソッドの解決

型以外の関連ジャイテムの解決は rustc_typeck::check::resolve_ty_and_def_ufcs で行われる。この処理はさらに rustc_typeck::check::method::probeprobe_for_name に移譲される。あとは上で説明したように、該当する実装が1つあったときだけ採用される。

まとめ

Rustの識別子の中には、型に依存して解決されるものがある。これらは比較的アドホックな方法で解決されており、型の明示が必要な原因のひとつになっていると思われる。