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
) の正規祖先はそれ自身である。 - それ以外 (
enum
とtrait
とブロック) の正規祖先は、その親モジュールの正規祖先である。- 現行のソースを見る限り、内部的には、非ローカルcrateの
enum
の正規祖先はそれ自身であるように見えるが、これはよくわからない……
- 現行のソースを見る限り、内部的には、非ローカルcrateの
正規祖先へのリンクは DefId
で保持しているが、利用するときは Module
を取り出す。
正規祖先は super
/self
の解決に用いられる。
解決
各モジュールは解決の一覧を持つ。解決は以下のような辞書エントリである。
- キー: 識別子と名前空間(型、値、マクロのいずれか)の組。識別子は非衛生化された状態で保存される。
- 値:
NameBindingKind
と衛生性マークと可視性の組。
パスの種類
パスは以下の3形式のいずれかからなる。
- 相対パス:
self
またはsuper
と、追加の0個以上のsuper
から始まるもの。- ただし、
self
のみからなり、型またはモジュール以外の文脈の場合は、レキシカルスコープのパスとして扱われる。
- ただし、
- 絶対パス:
::
または$crate
から始まるもの。 - レキシカルスコープのパス: 通常の識別子のみ(1個以上)からなるもの。
相対パスの場合、 self
と super
は以下のように解決される。
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
,trait
のconst
: ConstantItemRibKind (値と型) trait
とimpl
: 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や型検査の途中で処理することになる。
まとめ
とりわけ注意が必要なのは以下の点