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

Rustのモジュールの復習

以前名前解決についてまとめたが、やはり調べ損ねている部分があるので、もう一度まとめてみた。

DefとModuleとNameBindingKind

DefId はRust中に出現する定義(enum, enum のバリアント、 fn, let, macro_rules! foo など)を指している。これはcrateのID + crate内の識別番号で表される。 Def は大雑把に言うと DefId に追加の情報を加えたものである。

Rustの(広義の)モジュールはModuleDataで表されている。広義のモジュールは以下からなる。

  • 各crateのルートモジュール
  • mod
  • enum
  • trait
  • ブロック

ブロック以外は Def でありしかも名前をもつため ModuleKind::Def(Def, Name) で定義される。一方ブロックは ModuleKind::Block(NodeId) で定義される。

モジュールには様々な名前を束縛することができる。束縛される値は NameBindingKind で列挙されている。

  • NameBindingKind::Def: Def
  • NameBindingKind::Module: 狭義のモジュール (ルートモジュールと mod)
  • NameBindingKind::Import: use

親モジュール

広義のモジュールは高々1つの親モジュールを持つ。これによりモジュールは森構造をなす。根となるのは各crateのルートモジュールのみである。

モジュールの親子関係はASTの祖先/子孫関係と対応していると考えてよい。

親子関係は、次に述べる正規祖先と組み合わせて super の解決に用いられるほか、可視性の基準に用いられる。

正規祖先

親リンクとは別に、各モジュールは正規祖先へのリンクを持つ。正規祖先は以下のように定義される。

  • 狭義のモジュール (ルートモジュールと mod) の正規祖先はそれ自身である。
  • それ以外 (enumtrait とブロック) の正規祖先は、その親モジュールの正規祖先である。
    • 現行のソースを見る限り、内部的には、非ローカルcrateの enum の正規祖先はそれ自身であるように見えるが、これはよくわからない……

正規祖先へのリンクは DefId で保持しているが、利用するときは Module を取り出す。

正規祖先は super/self の解決に用いられる。

解決

各モジュールは解決の一覧を持つ。解決は以下のような辞書エントリである。

  • キー: 識別子と名前空間(型、値、マクロのいずれか)の組。識別子は非衛生化された状態で保存される。
  • 値: NameBindingKind と衛生性マークと可視性の組。

パスの種類

パスは以下の3形式のいずれかからなる。

  • 相対パス: self または super と、追加の0個以上の super から始まるもの。
    • ただし、 self のみからなり、型またはモジュール以外の文脈の場合は、レキシカルスコープのパスとして扱われる。
  • 絶対パス: :: または $crate から始まるもの。
  • レキシカルスコープのパス: 通常の識別子のみ(1個以上)からなるもの。

相対パスの場合、 selfsuper は以下のように解決される。

  • self は、現在のモジュールの正規祖先である。
  • super 1個につき、「親モジュールの正規祖先」を辿る操作が1回行われる。

絶対パスの場合、解決の開始位置は以下のように決定される。

  • :: の場合、ローカルcrate (現在コンパイル中のcrate) のルートモジュールから解決が開始される。
  • $crate は、該当マクロ定義のあったcrateのルートモジュールから解決が開始される。詳しくは過去の記事を参照。

レキシカルスコープのパスの場合、最初の識別子はレキシカルスコープで解決される(後述)。

レキシカルスコープからの解決

レキシカルスコープはコンパイラ内ではRibという単位で管理されている。Ribは以下の地点で発生する。

  • ルートモジュールと mod: ModuleRibKind (値と型)
  • 関数: ItemRibKind (値とラベル)
  • enum, type, struct, union, fn: ItemRibKind (型)
  • メソッド(trait, impl 内の fn): MethodRibKind (値とラベルと型)
  • クロージャ: ClosureRibKind (値とラベル)
  • trait, impl: ItemRibKind (型)
  • ラベルつきブロック: NormalRibKind (ラベル)
  • 配列型の長さ、バリアントの判別子、 const, traitconst: ConstantItemRibKind (値と型)
  • traitimpl: NormalRibKind (型)
  • matchの各節: NormalRibKind (値)
  • ブロック (匿名モジュールの場合): ModuleRibKind (値と型)
  • ブロック (匿名モジュールでない場合): NormalRibKind (値)
  • block / macros_at_scope: MacroDefinition (値とラベル)
  • with_module_lexical_scope: ModuleRibKind (値と型)
  • if let, while let, for in: NormalRibKind (値)

Rib は識別子と解決先の一覧を保持している。ただしこれらの更新のタイミングはRibの種類によって異なる。例えば、

  • ModuleRibKind は、モジュールに入った時点で全ての一覧が完成した状態になる。構文上の位置は関係ない。
  • NormalRibKind は、モジュールに入った時点では一覧は存在せず、パス解決と同時に更新されていく。例えば let の前後で名前解決の挙動が違うのはこの仕様により実現されている。

resolve_ident_in_lexical_scope は、このRibを内側から外側に順番に調べ、ローカル定義またはアイテムがあれば終了する。ただし、探索途中で、ブロック(匿名モジュール)以外の ModuleRibKind に遭遇した場合は、この探索を打ち切る。この規則により、上位モジュールでの use が下位モジュールに影響を与えるのを防いでいる。

なお、レキシカルスコープからの解決では、値名前空間は構文文脈を含めた状態で解決されるが、型名前空間は識別子を非衛生化した状態で解決される。

use のレキシカルスコープ解決

use に出現するパスは、他のパス解決よりも前に行われる。このときはRibはルートモジュールのみ存在するため、レキシカルスコープのパスは絶対パスとほぼ同じ意味になる。

パスの途中の要素の解決

パスの要素について、名前空間は以下のように決定される。

  • 最後以外の全ての要素は、型名前空間として扱われる。
  • 最後の要素は、型またはモジュールの文脈であれば型名前空間、値の文脈であれば値名前空間として扱われる。

パスの途中の要素の解決は、だいたい想像される通りのことが起こっている。ただし識別子は非衛生化される。

また、パス解決が途中で失敗した場合(直前がモジュールでなかった or モジュールだったが、名前を検索しても見つからなかった場合)も、この時点ではエラーにはならない。残りの部分は関連型やメソッドなどの名前かもしれないからである。この時点では、パスのどの要素まで解決されたかを含めて返し、残りはloweringや型検査の途中で処理することになる。

まとめ

とりわけ注意が必要なのは以下の点

  • 狭義のモジュールと広義のモジュールがある。 enum, trait, そしてブロックはモジュールの一種とみなされる。(正規祖先の定義も要確認)
  • パスは相対パス絶対パス・レキシカルスコープのパスの3種類がある。
  • use とそれ以外では解決のタイミングが異なる。 use でレキシカルスコープ形式のパスが使われた場合、実際には絶対パスとほぼ同義になる。
  • use 以外でレキシカルスコープ形式のパスが使われた場合、レキシカルスコープの探索は狭義のモジュールの境界で打ち切られる。
  • 同じ識別子でも、名前空間や構文文脈により区別されることがある。