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回で見てきたように名前解決の仕組みから順番に紐解いていけば、それなりにきちんと把握できる範囲におさまる。とはいうものの規則自体が単純ではないため、知らない規則に振り回されないようにするには、このように可能な限り網羅的に調べあげるほかない場合もある。