Rustの名前解決(5/5) 可視性判定

概要: Rustの名前解決の詳細について解説する。本記事では、解決された名前の可視性判定について説明する。

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

可視性

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