Rustの名前解決(5/5) 可視性判定
概要: Rustの名前解決の詳細について解説する。本記事では、解決された名前の可視性判定について説明する。
可視性
Rustの可視性は ast::Visibility, hir::Visibility, ty::Visibility で管理される。 ty::Visibility が最終なのでこれを見ると、次のようになっている。
#[derive(Clone, Debug, PartialEq, Eq, Copy, RustcEncodable, RustcDecodable)] pub enum Visibility { /// Visible everywhere (including in other crates). Public, /// Visible only in the given crate-local module. Restricted(DefId), /// Not visible anywhere in the local crate. This is the visibility of private external items. Invisible, }
これらはASTからHIRに変換されたあと、HIRからTyに変換されるが、インポート解決の段階ではHIRをバイパスして直接Tyに変換する。
特に、ソースコード中に書いた可視性指定は以下のように変換される。
pubと書いた場合、Publicになる。- 何も指定しなかった場合、そのアイテムの親モジュールの子孫に制限される。 (
Restricted) pub(in path::to::module)の場合、指定したモジュールの子孫に制限される。 (Restricted)pub(self)とpub(super)はpub(in self),pub(in super)と同じ。 (Restricted)pub(crate)は、そのアイテムが所属するcrateのトップレベルモジュールの子孫に制限される。 (Restricted)
Invisible は内部的に利用される。
可視性の2つのプリミティブ
ty::Visibility には is_accessible_from と is_at_least という2つのメソッドが定義されている。
vis.is_accessible_from(module, tree)は、moduleにあるアイテムから可視性visのアイテムが見えるかどうかを返す。vis1.is_at_least(vis2, tree)は、vis1の可視範囲がvis2を含んでいるかどうかを返す。
tree には、モジュールのなす木構造データを渡す。これは、コンパイルの段階に応じて使い分けるために、 DefIdTree というトレイトで抽象化されている。
可視性は木構造をもとに判定される。つまり、useの有無自体は可視性の範囲には関係なく、もともとのモジュールの親子関係により、可視性が判定される。
この2つのプリミティブを組み合わせて、プログラムが可視性を守っているかを以下のように調べる。
可視性に関する5つの判定
可視性判定は大きく5つに分類できる。
- アイテム参照の可視性判定
- 自分を含んでいるかどうか
- glob importの判定
- 再エクスポートの可視性判定
- 「公開インターフェース中の非公開型」の判定
アイテム参照の可視性判定
最も基本となる判定である。使おうとしたアイテムが、自分のいるモジュールから見えないアイテムだった場合には、エラーになる。これは型や関数などだけではなく、パスの途中に出現するモジュール名に対しても個別に判定される。
この処理は主に resolve_ident_in_module で行われている。参照しようとした名前が is_accessible でなければ、エラーになる。
inherent implのメソッド構文、inherent implのUFCS構文、トレイトメソッド呼び出し、構造体のメンバ参照、タプル構造体のメンバ参照、構造体またはタプル構造体の初期化とパタンーマッチにおけるメンバの可視性はそれぞれ別の場所で処理されている。
自分を含んでいるかどうか
pub(..) を使うと、自分自身から不可視なアイテムが構文上定義できるが、これは resolve_visibility 内で禁止されている。
glob importの判定
use foo::*; のようなglob importでは、インポート側モジュールから不可視なアイテムはインポートされない(したがって、他に同じ名前がインポートされていた場合、それがglob importであっても、衝突しない)。
resolve_glob_import と update_resolution 内で、この判定が行われている。
再エクスポートの可視性判定
可視性が指定された use は「再エクスポート」と呼ばれる。再エクスポートで、元のアイテムに指定されていた可視性を広げることはできない。これはfinalize_import内でチェックされている。
enumのバリアントの再エクスポートについては別途 finalize_resolutions_in でチェックされている。
また、追加の制約として、glob reexportでは、reexport自体の可視性が、実際にreexportされたアイテムの可視性の最大値と一致しないといけない。ただしこの制約は、glob reexportが実際には1つもexportできなかった場合には適用されない。
「公開インターフェース中の非公開型」の判定
“PRIVATE IN PUBLIC” とも呼ばれる。各種アイテムのインターフェース部分に、そのアイテム自身の可視性より狭い型などが出現してはいけない。例えば、公開されている関数の戻り値型が非公開な型を使っていたら、エラーになる。これを調べているのがPrivateItemsInPublicInterfacesVisitor である。
アイテムのどの部分が「インターフェース」とみなされるかは、個別的に指定されている。
const,static,fn,typeは、そのジェネリックス束縛、where束縛、その型や戻り値型が「インターフェース」とみなされる。- ただし、
impl Traitが出現する場合、そのimpl Trait自身の実体の可視性は検査されないが、Traitの可視性は検査される。
- ただし、
traitはジェネリックス束縛とwhere束縛が検査される。enumはそのジェネリックス束縛とwhere束縛と各バリアントの各フィールドの型が検査される。struct,unionは、そのジェネリックス束縛と各フィールドの型が検査される。- ただし、フィールドごとの可視性が考慮される。
impl T { .. }は、そのジェネリックス束縛とwhere束縛とimpl内の各アイテムが検査される。- ただし、
impl自身の可視性は、中に含まれているアイテムの可視性のうち最小のものとして定義される。
- ただし、
impl Trait for T { .. }は、そのジェネリックス束縛とwhere束縛とimpl内の各アイテムが検査される。- ただし、
impl自身の可視性は、中に含まれているアイテムの可視性と実装対象のトレイトの可視性のうち最小のものとして定義される。
- ただし、
まとめ
Rustの可視性は一見すると不可解な挙動をすると感じられるかもしれないが、この5回で見てきたように名前解決の仕組みから順番に紐解いていけば、それなりにきちんと把握できる範囲におさまる。とはいうものの規則自体が単純ではないため、知らない規則に振り回されないようにするには、このように可能な限り網羅的に調べあげるほかない場合もある。