Rustのモジュールを詳細に理解する(7) 名前解決とマクロ

概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。

  1. モジュール入門
  2. 名前解決の役割と用語
  3. モジュールグラフの構造
  4. インポート解決
  5. 可視性
  6. パス解決
  7. 名前解決とマクロ

本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。

注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。


衛生性とは

衛生性 (hygiene) とは、マクロ内外の変数をマクロ展開前の構造にもとづいてレキシカルスコープで解決できるようにする仕組みです。

macro_rules! foo {
    ($e:expr) => {
        let mut x = 42;
        $e;
        eprintln!("x in foo = {}", x); // 42
    }
}

fn main() {
    let mut x = 53;
    foo!(x = 64);
    eprintln!("x in main = {}", x); // 64
}

上のコードでは、 main が導入した束縛 xfoo が導入した束縛 x は区別されます。

マクロ展開を追跡するために、コンパイラはマクロ展開ひとつひとつに Mark という番号を割り振ります。たとえば上の例では foo! が1回、 eprintln! が2回展開されているので少なくとも3つの Mark が割り振られることになります。そして、衛生性を追跡するために、マクロ展開境界をまたいだ識別子には SyntaxContext という情報が割り当てられます。これは実質的には Mark の列 (より正確には Mark と透明度の対の列) で、今ある位置に至るまでにまたいだマクロ展開境界の一覧が表現されます。RustのASTの識別子やキーワードには、この SyntaxContext が付随しています。

上の例では、 foo!(x = 64) を展開するときに foo 内の識別子にだけマークが付与されて以下のようになります。

macro_rules! foo {
    ($e:expr) => {
        let mut x = 42;
        $e;
        eprintln!("x in foo = {}", x); // 42
    }
}

fn main() {
    let mut x = 53;
    // macro[1] { // of foo!()
    let mut x /* [1] */ = 42;
    x = 64;
    eprintln!("x in foo = {}", x /* [1] */); // 42
    // }
    eprintln!("x in main = {}", x); // 64
}

正確には let などのキーワードにもマークが付与されていますが、これはパースの際に無視されます。

レガシーマクロとモダンマクロ

macro_rules! によるマクロはグローバル定義に対する衛生性を持ちません。

mod m1 {
    const X: i32 = 42;
    #[macro_export]
    macro_rules! foo {
        () => { X }
    }
}

const X: i32 = 53;

fn main() {
    dbg!(foo!()); // 53
}

一方、RFC1584の定める宣言マクロ2.0ではグローバル定義に対する衛生性も導入されます。

#![feature(decl_macro)]

mod m1 {
    const X: i32 = 42;
    pub macro foo() {
        X
    }
}

const X: i32 = 53;

fn main() {
    dbg!(m1::foo!()); // 42
}

これらを区別するために、Rustのマクロ衛生性には透明度という概念があります。

  • Transparent (透明): このマークを衛生性のために使わない。
    • #[rustc_transparent_macro] のついた宣言マクロ2.0や、手続きマクロの衛生性オプトアウトで使われる。
  • SemiTransparent (半透明): ローカル束縛を探すときは考慮するが、グローバル定義を探すときは考慮しない。
    • macro_rules! で使われる。
  • Opaque (不透明): このマークを衛生性のために使う。
    • 宣言マクロ2.0や手続きマクロのデフォルト挙動。

少し前の段落で SyntaxContextMark の列だと書きましたが、 (Mark, Transparency) の列であるとみなすほうが正確です。そして SyntaxContext には以下の2つのメソッドが備わっています。

  • modern: Opaqueなマークだけを取り出した SyntaxContext を返す。
  • modern_and_legacy: OpaqueまたはSemiTransparentなマークだけを取り出した SyntaxContext を返す。

名前解決時は、識別子を比較する前に modern/modern_and_legacy のいずれかを呼んで正規化をしています。基本的には modern が使われます。modern_and_legacyが呼ばれる場面として重要なのが「値名前空間でレキシカルスコープ解決をするとき」です。これはまさに、旧来の macro_rules! で衛生性が有効だった場面に他なりません。

さて、modern形式のマクロ(手続きマクロ、宣言マクロ2.0)では、マクロ内の識別子がマクロ呼び出しの外側 (caller-site) のスコープで解決されることはありません。 (Markがずれるため)

かわりに、通常のスコープ内探索が終わったあと、マクロ定義の外側 (def-site) のスコープで探索を再開する仕組みになっています。(前の記事でも少し言及していました) これにより、「マクロ展開前の構造に基づいてレキシカルスコープで解決する」という衛生性の目標をより広く達成できるようになっています。

インポート解決とマクロ

Rustのマクロは元々、構文解析の後・名前解決の前に展開されていました。その名残りもあって、 #[macro_export] のない macro_rules! は今でも後方定義を許容しません。

foo!(); //~ERROR cannot find macro

macro_rules! foo {
    () => {
        fn f() {}
    }
}

現在は、マクロ解決は部分的に名前解決に統合されています。そのおかげで外部マクロを use したり、:: で参照したりできています。

use log::debug;

fn main() {
    debug!("foo");
    log::info!("bar");
}

これはつまり、インポート解決をしなければマクロが展開できない場合があるということです。一方で、マクロは任意のアイテムに展開できるので、マクロの展開により新たなインポート解決が必要になる可能性もあります。

インポート解決とマクロ: コンパイラアーキテクチャ

Rustコンパイラsyntaxrustc という2大クレートと、その周辺クレートから成り立っています。 rustcsyntax に依存し、 rustc_*rustc に依存し、 rustc_driverrustc_* に依存するという形でコードベースが分割されています。

ここで、マクロの展開は syntax の責務であるのに対し、インポート解決は rustc の責務です。この部分を繋ぐために、 syntax 側にDIのようなインターフェースが用意されています。

pub trait Resolver {
    fn next_node_id(&mut self) -> ast::NodeId;
    fn get_module_scope(&mut self, id: ast::NodeId) -> Mark;

    fn resolve_dollar_crates(&mut self, fragment: &AstFragment);
    fn visit_ast_fragment_with_placeholders(&mut self, mark: Mark, fragment: &AstFragment,
                                            derives: &[Mark]);
    fn add_builtin(&mut self, ident: ast::Ident, ext: Lrc<SyntaxExtension>);

    fn resolve_imports(&mut self);

    fn resolve_macro_invocation(&mut self, invoc: &Invocation, invoc_id: Mark, force: bool)
                                -> Result<Option<Lrc<SyntaxExtension>>, Determinacy>;
    fn resolve_macro_path(&mut self, path: &ast::Path, kind: MacroKind, invoc_id: Mark,
                          derives_in_scope: Vec<ast::Path>, force: bool)
                          -> Result<Lrc<SyntaxExtension>, Determinacy>;

    fn check_unused_macros(&self);
}

syntax はこのような「パスを渡すとマクロ定義への解決を試みてくれるオラクル」を受け取り、これを使いながらマクロを展開していきます。

依存性の実際の注入は rustc_driver で行われています。名前解決器をあらかじめ生成した上で、

  • 名前解決器にビルトインマクロを登録する。
  • 名前解決器を使って、マクロを展開する。
  • 名前解決器側だけでできる処理 (インポート解決のファイナライズ + パス解決) をする。

という3段階の処理をしています。

インポート解決とマクロ: シャドウイングの処理

未展開のマクロは名前解決にとってはいくつか困った問題があります。

  • 未展開マクロは任意の名前を追加で定義しうる。
  • 未展開マクロは extern crate を生成しうる。
  • 未展開マクロは #[macro_export] macro_rules!{} を生成しうる。

未展開マクロは任意の名前を追加で定義しうる

このため、未展開マクロの残っているモジュールでは、ある識別子が存在しないことを結論づけることはできないという追加規則が必要です。

ただし、シャドウイングに関してはRFC1560に追加の規則が定義されています。

Caveat: an explicit name which is defined by the expansion of a macro does not shadow implicit names.

これにより、globインポートのシャドウイングを検討するときにマクロを気にする必要はなくなっています。

未展開マクロは extern crate を生成しうる

extern crate は名前解決に対するグローバルな影響があります。クレートルートで extern crate serde as serde2; とすることにより serde2::Serialize というパスが解決可能になってしまうからです。

そのため、ExternPreludeの解決時はクレートルートに未解決のマクロがある場合、ExternPreludeの非存在を結論づけないという追加規則が必要です。

未展開マクロは #[macro_export] macro_rules!{} を生成しうる

#[macro_export] macro_rules!{} を使うと、任意の位置からクレートルートに名前を挿入できてしまいます。

このせいで名前解決が進まないのは本望ではないので、マクロ内定義マクロのパスによる参照を禁止するという方向で対策されています。これにより、マクロ解決時に未展開マクロを気にする必要はほぼなくなっています。

まとめ

Rustはマクロに衛生性を導入しており、そのせいで識別子に対する処理が複雑になっています。さらに歴史的な事情もあって衛生性には3段階のレベルがあるので、このことをきちんと理解しつつ名前解決の挙動を追うのは大変です。また、インポート解決とマクロ展開が相互作用するのも、この部分を複雑にしている一因となっています。

とはいえ、こういった細かい配慮によって全体としてはそれなりに一貫した体感のモジュールシステムが提供されており、一般的なRustプログラマーはここまで複雑な問題を気にする必要がないようになっていると思います。

ここまで7日間かけて名前解決についてまとめましたが、2年前に同じ部分を調べたときに比べて全容がより複雑になっていると感じます。その一方で、当時からあった細かい仕様や実装についてもより深く理解できるようになったので、この記事をまとめた甲斐があると思います。

この記事を通しで読む人がどれくらいいるかはわかりませんが、きちんと読めば(すぐには役立たなくても)Rustでのプログラミングを底から支える力になると思います。また、何か細かい仕様で困った人にとっての辞書としても使える記事に仕上がったと思います。

関連RFC

Rust2018以前の仕様に関連したRFC

Rust2018に関連した(または2018年頃に実装された)RFC

実装途上のRFC

Rustのモジュールを詳細に理解する(6) パス解決

概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。

  1. モジュール入門
  2. 名前解決の役割と用語
  3. モジュールグラフの構造
  4. インポート解決
  5. 可視性
  6. パス解決
  7. 名前解決とマクロ

本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。

注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。


仮想モジュール

globではない use は必ず「どのモジュールから」「何を」「何という名前で」インポートするかという形で表現されます。しかし use crate as foo;use foo as bar; など単一セグメントからなるパスのインポートの場合は「どのモジュールから」に相当する適切なモジュールがない場合があります。そこで名前解決時は3つの仮想モジュールが用意されています

  • CrateRootAndExternPrelude ... 「仮想Rust2015モード」における :: 開始パスで使われる。ルートモジュールとして振る舞うが、ルートにないものに関しては ExternPrelude にフォールバックする。
  • ExternPrelude ... Rust2018における :: 開始パスで使われる。外部クレート名を解決する。
  • CurrentScope ... :: 開始パス以外の始点。

ExternPrelude仮想モジュールの解決

::foo::bar はRust2015とRust2018で振る舞いが異なります。

  • Rust2015では crate::foo::bar と同じ。
  • Rust2018では foo クレート以下の bar と同じ。 (=ExternPreludeが使われる)

ではExternPrelude仮想モジュールが解決する「クレート名」とは何でしょうか。具体的には以下が使われます。

最後の規則のおかげで、以下のようなコードがRust2015/2018マイグレーションコードとして許されることになります。

extern crate serde_json as json;

mod m1 {
    pub fn foo() {
        let ((),) = ::json::from_str("[null]").unwrap();
    }
}

これに関連して、 extern crate self as foo; という構文が導入されています。これは use crate as foo; と似ていますが、上記のようにExternPreludeに別名を導入する追加効果があります。

たとえば、 serde_deriveserde:: で始まるパスを内部的に生成するとします。これを serde 自身から使うには extern crate self as serde; としておけばいいわけです。

CrateRootAndExternPrelude仮想モジュールの解決

:: ではじまるパスは以下の条件で特殊な動作をします。

  • グローバルRust2018モードである (=今コンパイルしようとしているクレートがRust2018)
  • 当該パスがRust2015クレートに由来している

この場合は仮想Rust2015モードと呼ばれ、絶対パス(Rust2015)とExternPreludeパス(Rust2018)の両方に解決されます。そのための仮想モジュールがCrateRootAndExternPreludeです。

CurrentScope仮想モジュール

CurrentScopeは通常の方法でパスを解決します。 use が単一セグメントからなる単独インポートだった場合に使われます。たとえば、 use foo as bar; であれば、 CurrentScopeから foobar としてインポートするという形で表現します。

複数セグメントからなる場合、たとえば use foo::bar as baz; であれば foo から barbaz としてインポートすると表記できるため使われません。

パス全体の解決

ここまで何度か述べたように、パスの中でも pub(in)use は特殊でした。 pub(in) は全く別のメソッドでパス解決されますし、 use は仮想モジュールがからむのでやや特殊です。それ以外のパスは、主に resolve_path で解決されます。

パス解決の全体の流れはシンプルです。 foo::bar::baz を値として解決したい場合、まず foo を型名前空間で解決します。次に foo の直下で型名前空間bar を解決します。最後に foo::bar の直下で値名前空間baz を解決します。ポイントは3つです。

  • 最後のセグメントは所望の名前空間で解決しますが、それ以外のセグメントは型名前空間で解決します。 (モジュール・型・トレイトであることが期待されるため)
  • 最初のセグメントの解決は複雑です。
  • 先頭に特殊な接頭辞がついている場合があります。

接頭辞つきのパスの解決

:: 接頭辞

:: についてはすでに説明した通り、Rust2015では絶対パス、Rust2018ではextern preludeパスとして解釈されます。

絶対パス接頭辞

crate:: または $crate:: で始まるパスは絶対パスです。このとき、 crate/$crate というキーワードが由来するクレート絶対パスとして解決されます。たとえば、futures-0.1の try_ready! マクロは以下のように書かれています。

#[macro_export]
macro_rules! try_ready {
    ($e:expr) => (match $e {
        Ok($crate::Async::Ready(t)) => t,
        Ok($crate::Async::NotReady) => return Ok($crate::Async::NotReady),
        Err(e) => return Err(From::from(e)),
    })
}

この $crate は必ずこのfuturesクレートのルートに展開されます。このため、他のクレートで try_ready! を呼び出しても正しく動作します。

$crateRFC0453で導入されたマクロ専用のキーワードですが、 crate:: (RFC2126) も動作はほぼ同じです。こちらはマクロ以外でも使えます。

相対パス接頭辞

self:: または super:: で始まるパスは相対パスです。 self/super をこの意味で解釈するのには以下の条件が必要です。

let x = self::foo;
let x = self::super::foo;
let x = super::foo;
let x = super::super::foo;

self/super を計算するときは、かならず正規モジュールが結果になるように調整されることに注意してください。正規モジュールとは、クレートルートまたは mod で定義されたモジュールのことです。正規ではないモジュールとは、 enum, trait またはブロックのことです。正規モジュールに調整するにあたっては正規祖先 (自分自身を含め、祖先の中で最も近い正規モジュール) が使われます。

const X: i32 = 42;

fn main() {
    const X: i32 = 53;
    dbg!(self::X); // 42
}

super::self::super:: の省略形だと考えて、

  • self:: = 現在のモジュールの正規祖先
  • ::super = その親モジュールの正規祖先

という風に定義できます。クレートルートの super をとろうとするとエラーになります。

接頭辞のないパスの解決

接頭辞のないパスはグローバルかローカルのいずれかに解決されます。ここまでの議論では完全に無視していましたが、とうとうローカル束縛のことも考えてあげる必要が出てきます。(なお、「匿名モジュール(ブロック)内で定義されたアイテム」はこの意味ではグローバルです)

接頭辞のないパスの1セグメント目は以下の優先度で解決されます。

  1. 通常のレキシカルスコープのローカル定義またはグローバル定義 (同じ階層ではローカル定義優先)
  2. 衛生的レキシカルスコープのグローバル定義
  3. extern prelude
  4. ツール属性のためのモジュール
  5. 通常のプレリュード

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

接頭辞のないパスはresolve_ident_in_lexical_scopeで解決されます。レキシカルスコープはモジュールよりも細かいリブ (rib) という階層で処理されます。

リブは名前空間別に別々に管理されており、値名前空間・型名前空間に対して登録されます。 (※マクロ用のリブはない) またそれとは別にラベルを管理するリブがあります。

たとえば、 fnクロージャは引数束縛があるので値名前空間にリブを作ります。 let はlet束縛があるので値名前空間にリブを作ります。ジェネリクスは型名前空間にリブを作ります。

また、モジュール自身もリブです。

//^クレートルートのrib (ValueNS, TypeNS)
fn id<T>(x: T) {
    //^ジェネリクス引数のrib (TypeNS)
    //^引数のrib (ValueNS)
    //^匿名モジュールのrib (ValueNS, TypeNS)
    let y = x;
    //^let束縛のrib (ValueNS)
    y
}

レキシカルスコープで名前を解決するときは、今いる位置で有効なリブを下から上に遡って探索していきます。モジュールリブなら、さらにグローバルな名前の解決も試みます。そして、正規モジュールのリブに到達したら、そこで探索を終了します。

そのため、基本的には mod 内・その場所で有効な定義のうち、最も近いものに解決されることになります。 (シャドウイングの一般的な規則)

fn f(x: i32) {  // (1)
    // (1) に解決される (1のribのみが有効)
    let x: i32 = 42; // (2)
    // (2) に解決される (1, 2のribが有効)
    let x: i32 = 53; // (3)
    // (3) に解決される (1, 2, 3のribが有効)
    {
        // (3) に解決される (1, 2, 3のribが有効)
        let x: i32 = 64; // (4)
        // (4) に解決される (1, 2, 3, 4のribが有効)
    }
    // (3) に解決される (1, 2, 3のribが有効)
}

ただし、ここでいう「有効な定義」は字句的な範囲 (let の場合はその let の終わりまで) で判定され、キャプチャー可能性など意味的な条件は使われません。そのため、

fn x() {}

fn f(x: i32) {
    fn g() {
        // 関数xではなく、関数fの引数xに解決される。
        // しかし、関数は環境をキャプチャーできないのでエラーになる。
        x; //~ERROR can't capture dynamic environment in a fn item
    }
}

type T = i32;

fn g<T>() {
    // 型エイリアスTではなく、関数gの型パラメーターTに解決される。
    // しかし、アイテムは外側のジェネリクスに関係ないのでエラーになる
    const X: Option<T> = None; //~ERROR can't use generic parameters from outer function
}

また、シャドウイングが別途禁止されている場合もあります。たとえば let 内で const と同名の束縛を導入すると定数パターンになり、 static と同名の束縛を導入するとエラーになります。

const X: i32 = 42;
static Y: i32 = 42;

fn main() {
    let X = 42; //~ERROR refutable pattern in local binding
    let Y = 42; //~ERROR let bindings cannot shadow statics
}

ローカル定義とグローバル定義の間にも、基本的には最近優先のルールが適用できます。

fn foo() {
    let x = 42;
    {
        fn x() -> i32 { 42 }
        // より内側で定義されている関数xに解決される
        dbg!(x() + 11);
    }
}

fn bar() {
    fn x() {}
    {
        let x = 42;
        // より内側で定義されている変数xに解決される
        dbg!(x + 11);
    }
}

ただし、グローバル定義のタイムトラベルに注意が必要です。

fn main() {
    // 匿名モジュールのリブはここから開始する(→ブロックの終わりまで)。
    // そのため、関数fはこの位置から有効

    // この時点では変数fは存在しないので関数fに解決される
    assert_eq!(f(), 42);

    let f = || 53;
    // letに対応するリブはここから開始する(→ブロックの終わりまで)。
    
    // ここでは関数fと変数fの両方が有効だが、
    // 変数fのほうが近い (より内側で定義されている)
    // のでそちらが採用される
    assert_eq!(f(), 53);

    fn f() -> i32 { 42 }
}

このため、レキシカルスコープのシャドウイングの優先度を考えるときは、アイテム(fnstaticなどグローバルに置けるもの)はモジュール(匿名モジュールの場合はブロック)の先頭に移動したと思って考えるとわかりやすいです。

衛生的レキシカルスコープ

RFC1584の定める宣言マクロ2.0では、現在の macro_rules! よりも広範囲のマクロ衛生性をサポートしています。

#![feature(decl_macro)]

mod m1 {
    const X: i32 = 42;
    pub macro f() {
        dbg!(X);
    }
}

fn main() {
    m1::f!();
}

この場合、マクロを展開すると以下のようになります。

mod m1 {
    const X: i32 = 42;
    pub macro f() {
        dbg!(X);
    }
}

fn main() {
    // mark[1] { // of m1::f!()
    dbg /* [1] */ ! ( X /* [1] */ );
    // }
}

衛生性抜きでは、この X は解決できません。しかし、この識別子 X はマクロ展開 [1] に由来しているので、さらなる探索が行われます。マクロ展開 [1] で使われたマクロは m1::f なので、 f が定義された時のスコープ m1 でも X の解決を試みます。

extern prelude フォールバック

祖先モジュールで #![no_implicit_prelude] が定義されていなければextern preludeへのフォールバックが調べられます。

Rust2018で単に std::iter::once のように書いたときに標準ライブラリが参照されるのはこの仕組みによるものです。 (ようやくここまで来た……)

ツール属性のためのモジュール

祖先モジュールで #![no_implicit_prelude] が定義されていなければ、既知のツール属性モジュールへのフォールバックが調べられます。

ツール属性とはRFC2103で規定されている、「コンパイラは知らないが周辺ツールが知っている属性名」を安全に導入する仕組みです。

たとえばrustfmtで特定のアイテムのフォーマットを抑止するには以下のようにしていました。

#[cfg_attr(rustfmt, rustfmt_skip)]
fn foo() {}

単に #[rustfmt_skip] と書けばよさそうに見えますが、RFC0572で規定されている通りコンパイラ前方互換性のために未知の属性をエラーにすることになっています。そのため rustfmt というrustfmt処理時しか有効にならないcfgで条件をつけてやる必要がありました。

RFC2103はこれを解決するために属性のモジュール化をするもので、これにより次のように書けるようになりました。

#[rustfmt::skip]
fn foo() {}

これは通常の名前解決の仕組みにある程度乗っかっています。この場合はマクロ名前空間rustfmt::skip が解決されていることになります。

そして、同じくRFC0572に言及されているように、当面の実装ではツール名は固定です。現時点では rustfmtclippy が既知のツール名として扱われています

このため以下のような奇妙なコードのコンパイルが通ります。

use {clippy, rustfmt};

通常のプレリュード

祖先モジュールで #![no_implicit_prelude] が定義されていなければ、通常のプレリュードへのフォールバックが調べられます。

よく知られているようにRustの標準ライブラリにはpreludeモジュールがあり、この中にあるアイテムは明示的なインポートなしで使用できるようになっています。

これを実現するためにまず、構文解析のタイミングで以下のような特殊な use が埋め込まれます

#[prelude_import]
use ::std::prelude::v1::*;

埋め込まれるモジュールは実際には条件によって異なります。

  • #![no_core] が指定されたときは、何も埋め込まれません。
  • #![no_core] ではないが #![no_std] が指定されたときは、以下の2通りのどちらかになります。
    • #![compiler_builtins] が指定されたときは、 extern crate core; #[prelude_import] use ::core::prelude::v1::*; が埋め込まれます。
    • #![compiler_builtins] が指定されなかったときは、 extern crate core; extern crate compiler_builtins; #[prelude_import] use ::core::prelude::v1::*; が埋め込まれます。
  • #![no_core]#![no_std] も指定されなかったときは、 extern crate std; #[prelude_import] use ::std::prelude::v1::*; が埋め込まれます。

相対パスを考慮したパス解決

ここまでで説明したのは型相対ではない場合のパスの解決でした。 resolve_qpath は、これに加えて型相対パスを考慮した解決を行います。

  • <T>::A::B の場合は何もせず、パス全体 (A::B) を未解決として返します。
  • <T as A::B>::C::D の場合は <>:: の直後まで (この場合は <T as A::B>::C) の解決を試み、解決できたところまでを返します。
  • A::B::C の場合は単に解決できたところまでを返します。

ただし、 A::B::C 形式のパスの解決に失敗した場合、プリミティブ型へのフォールバックが検討されます。

use std::i32;

fn main() {
    let x: i32 = 42; // i32 (as a type)
    dbg!(i32::MAX); // std::i32::MAX
    dbg!(i32::max_value()); // <i32>::max_value
}

プリミティブ型へのフォールバックは、以下の条件で発生します。

  • 先頭セグメントがプリミティブ型名と一致する
  • 先頭セグメントを型名前空間で解決しようとしていた (=全体を型名前空間で解決しようとしていた or 長さ2以上のパスを解決しようとしていた)
  • パスの全体または途中までが正規モジュールに解決された

この場合、通常の方法で解釈しても望みがないことは明らか(モジュールの :: は名前解決の時点で解決できるはず)なので、先頭セグメントをプリミティブ型名として読み替え、型相対パスとして解釈します。

スコープ内トレイトの列挙

インポート解決で得られた結果は、パス解決のほかに、スコープ内トレイトの列挙のためにも使われます。これはRustの以下の規則のために必要です。

  • <T>::A は、型 T 自身の固有実装のほか、スコープ内にあるトレイトの実装から探索される。
  • x.method() は、 x の(自動参照・自動参照外しの適用後を含む)型の固有実装またはスコープ内にあるトレイトの実装から探索される。

関連アイテムやメソッドの解決をスコープ内トレイトに限定することで、「依存ライブラリを増やしただけで勝手にメソッド名の解決順が変わる」というような不安定な挙動を極力抑える意図があるものと思われます。

「スコープ内」と言っているだけあって、これは通常のパスの最初のセグメントの解決とほぼ同じ挙動をします。具体的には以下の優先順位で探索が行われます。

  1. 現在実装中のトレイト
  2. レキシカルスコープ内のトレイト
  3. 衛生的レキシカルスコープ内のトレイト
  4. 通常のプレリュード内のトレイト

2から4はほぼ同じなので省きます。「現在実装中のトレイト」が探索されることで以下のようなコードが許容されています。

use std::fmt;
use std::num::NonZeroU32;

// +1した状態で保存される整数。
// Optionと組み合わせたときの空間効率が良い。
#[allow(non_camel_case_types)]
pub struct u32m1(NonZeroU32);

impl fmt::Debug for u32m1 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        (self.0.get() - 1).fmt(f)
    }
}

上のコードではu32m1Debug::fmt の実装を u32Debug::fmt に移譲しています。ここで .fmt() と書けるのは、実装中のトレイトが使われているおかげです。

ちなみにトレイト自身のデフォルト実装では、そもそもトレイトがスコープ内にあることが普通なのでこのような特別対応は必要ありません。ただし、それによって以下のようなコーナーケースは発生しているようです。

trait Foo {
    fn foo(&self) {
        mod m1 {
            fn f() {
                // ().foo(); // error
            }
        }
    }
}

impl Foo for () {
    fn foo(&self) {
        mod m1 {
            fn f() {
                ().foo(); // OK
            }
        }
    }
}

まとめ

(5)でモジュールグラフが作られ、(6)でパスの解決ができたので、これで名前解決の担当範囲はおおよそ理解できたことになります。

ただし、名前解決とマクロの相互作用についてはきちんと扱っていなかったので、次回は最後にその話をして終わりにしたいと思います。

Rustのモジュールを詳細に理解する(5) 可視性

概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。

  1. モジュール入門
  2. 名前解決の役割と用語
  3. モジュールグラフの構造
  4. インポート解決
  5. 可視性
  6. パス解決
  7. 名前解決とマクロ

本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。

注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。


可視性とは

可視性 (visibility) は、そのアイテムを名指ししていいかどうかのアクセス制御をする仕組みで、Rustにおける実装のカプセル化のために使われます。かつては pub と未指定 (module private) の2種類でしたが、 RFC1422にてモジュール固有の可視性が実装されました。また、 RFC1560が実質的にRFC1422の事前準備としての役割を果たしているため、こちらも要チェックです。

可視性は名指しできるアイテムには基本的に付与されます。付与されないものとして以下があります。

  • 実装
    • 実装は型で索引されるため、可視性で管理するものではないという考えだと思われる。
    • トレイトアイテムに可視性がないのも同様?
  • macro_rules! によるマクロ
    • 伝統的に #[macro_export] の有無で制御されてきたため?
    • RFC1561では宣言マクロ2.0の一部として可視性の統一を目指す案も記載されている

逆に、アイテム以外に付与されるものとして以下があります。

  • 固有実装の中身 (メソッド・関連関数・関連定数)
  • 構造体・列挙型・共用体のフィールド

可視性の種類と構文

可視性の指定は意味的には2種類(+α)に分けられます

  • 公開。どこからでも参照できる。
  • モジュール限定。特定モジュール以下から参照できる。
  • 完全に不可視。アイテム自身からも見えないというのはおかしいので、ユーザーに見えるところでは出てこない。内部的に使われる。 (たとえば、可視性のjoin operationの単位元になっている)

単に pub と書くと公開になります。そして、モジュール限定を指定するための記法がたくさんあります。

  • pub(in foo::bar::baz) と書いた場合。この場合 foo::bar::baz 以下が可視範囲となる。
  • 未指定の場合 (fn f() {}) → pub(in self) と同じ。
  • pub(self) の場合 → pub(in self) と同じ。
  • pub(super) の場合 → pub(in super) と同じ。
  • pub(crate) の場合 → pub(in crate) と同じ。
  • crate の場合 → pub(in crate) と同じ。RFC2126の一部だが、Rust2018までに安定化する必要はないと判断され今に至る。

Q&A

  • pub(in)in はなんで必要なの? → 元々の提案・実装ではなかったのですが、タプル構造体のメンバーに対して使ったときの構文的な曖昧性を取り除くために追加されました。
  • pub(self) って要るの?→マクロなどで機械的に処理するときに self を放り込めて便利だからだと言われています。

なお、 crate による可視性指定もタプル構造体のメンバーに使ったときの構文的な曖昧性が指摘されています

可視性チェック

あるアクセスが可視性を満たしているかどうかは「どのDef(或いはresolution)を」「どのモジュールから見るか」によって決定されます。

どのDefを……については、たとえば foo::bar::baz を参照しようとしているならば、 foo, foo::bar, foo::bar::baz にそれぞれアクセス権があるかどうかチェックされます。このときたとえば foo::baruse に由来しているときは、その use 自身の可視性に基づいてチェックされます。そのため、 baz 自体が pub 指定でも、それにどういうパスでアクセスするかや、 bar 内で baz がどう use されているかによってはアクセスできない場合もあります。

このこと自体は割と当たり前の挙動です。 use は単に外部の名前をスコープに置くための便宜的な用途でも使いますが、それが外から見えてしまっては困るからです。

また、可視性を判定するときは use は無視して、元々あったモジュールの親子関係(=モジュール木)に基づいて判定します。そのため、この判定はモジュールの親リンクを辿るだけで可能です。

可視性と use

可視性は use の挙動にも影響を与えます。そのため、インポートを解決してから全ての可視性をチェックするわけではなく、インポート解決とuse の可視性判定は同時進行します。

use は複数の名前空間にまたがって処理されるので、個々の名前空間ではインポートの失敗が許容されています。

たとえば、 pub use foo::bar; の場合、各名前空間ごとの処理では以下のような扱いになります。 (すでに foo までは解決できている前提)

  • foo::bar にアクセスできない場合は、その名前空間ではインポートしない。
    • foo::bar が存在しない場合と同じ扱い
  • foo::bar にアクセスできるが、その可視性が pub よりも弱い (e.g. crate) 場合は、弱いほうの可視性でインポートする。
  • foo::bar にアクセス可能で、その可視性が十分な場合は、 pub でインポート (再エクスポート) する。

その上で、全体としては指定した可視性 (この場合は pub) でインポートできたものが1つ以上ないとエラーになります。ただし、globインポートの場合はいくつか例外があり、たとえば何もインポートしなかった場合はエラーにはなりません。

mod m0 {
    pub fn f() {}
    mod f {}
}
mod m1 {
    fn f() {}
    pub mod f {}
}
use m0::f; // 値名前空間でしかインポートしない
use m1::f; // 型名前空間でしかインポートしない

mod m2 {
    pub(crate) mod m3 {
        pub(super) fn f() {}
        pub mod f {
            pub const X: i32 = 42;
        }
    }
    pub use m3::f; // 関数fをpub(self)で、モジュールfをpubでインポートする
    fn g() {
        f();
    }
}
fn g() {
    m2::f::X;
}

mod m4 {
    // ここに例えば fn foo() {} と書くと下のインポートがエラーになる
}
pub use m4::*; // 何もインポートしないのでエラーにならない

可視性自身のパス解決

pub(in) 構文があるため、可視性の中でもパス解決をする必要があります。この可視性のパス解決はresolve_visibilityという専用のメソッドで行われています。

pub(in) のパスは基本的には use と同じ挙動をします。つまり、Rust2015モードではデフォルト絶対パスです。その他に以下のような特徴があります。

  • 絶対パス相対パス以外は未対応。つまり、 pub(in self::foo::bar) は可能だが、 pub(in foo::bar) をRust2018モードで行うとエラーになる
  • use と異なり、当該パスのインポートが解決されるまで待つ仕組みは実装されていないらしい (issueにした)

use 以外の可視性検査

use は上記のように名前解決と密接に関わっていますが、それ以外の可視性は以下のタイミングで検査されます。

private-in-public

private-in-publicはRFC0136の通称です。 (use の可視性に関する規定もここに含まれていますが、これは今となっては扱いがかなり違うので本節の対象外とします)

RFC0136は、公開APIの型も公開されている必要があるという規定です。たとえば、

  • 関数が pub なら、その引数や戻り値型も pub である必要がある
  • 構造体フィールドが pub なら、その型も pub である必要がある
  • トレイト実装の全ての入力型が pub なら、その関連型も pub である必要がある

などの規定があります。

struct A;
pub fn f(_: A) {} // error
pub trait Foo {
    type X;
}

struct A;
type Unit = ();

impl Foo for Unit {
    type X = A; // error
}

逆にOKなものとしては、

  • 構造体が pub でも、そのフィールドが pub である必要はない
  • 関数が pub でも、その実装で使われているアイテムが pub である必要はない

などがあります。

このような可視性は名前解決よりも後のフェーズで検査されます

private-in-publicのlintへの緩和

RFC2145によりprivate-in-public規則が整理されました。

  • 既存の規則がより明確に説明されました。
  • 到達可能性に基づく新しい規則が提案されました。
  • それにあわせて、既存のprivate-in-publicチェックをlintに緩和することが提案されました。

到達可能性とヴォルデモート型

現在のprivate-in-publicルールはヴォルデモート型 (Voldemort type) と呼ばれる種類のパターンを許容しています。

mod m1 {
    pub struct Voldemort;
}

// 外部クレートはVoldemortにアクセスする手段がない。
// しかし、Voldemort自体はpubなので、この関数は許されている
pub fn you_know_who() -> m1::Voldemort {
    m1::Voldemort
}

RFC2145で提案されている新しい規則では、ヴォルデモート型に対する対応も含まれます。

public/private dependencies

private-in-publicに関連して、RFC1977ではこれを依存関係に対して拡張することを提案しています。たとえば、 Cargo.toml に以下のように書いたとします。

[package]
name = "foo"
# ...

[dependencies]
# fooは内部的な高速化のためにrayonを使うが、
# パラレルイテレーターのインターフェースを提供するわけではないのでprivate
rayon = { version = "1.0.3", public = false }
# fooの公開している型がSerialize/Deserializeを実装しているのでpublic
serde = { version = "1.0.90", public = true }

このとき、 foo の公開APIrayon の型が含まれていないか が追加で検査されます。

この場合、 foorayon のメジャーバージョンを上げても自身のメジャーバージョンを上げる必要はない一方、 serde のメジャーバージョンを上げたときは(通常は)自身のメジャーバージョンも上げる必要があると解釈されます。 (ただし、semver compatibilityを機械的に調べる方法は今のところないので、これはインフォーマルにそう定められているだけといえる)

またこの情報を、三角依存関係を同じバージョンに解決するかどうかの判定にも使えないかと考えられています。たとえば上の状況で、さらに barrayon, serde, foo の全てに依存していたとします。このとき、 serde は単一バージョンに解決する必要がある一方、 rayon は単一バージョンに解決する必要はないことになります。

まとめ

可視性 (pub) とインポート解決の挙動について説明しました。ここまでを理解すれば、モジュール構造の設計で(挙動の予測ができずに)困ることはほぼなくなるのではないでしょうか。また、それに関連して、名前解決以降のフェーズでの可視性検査も軽く説明しました。

インポートの解決までがようやく説明できたので、これを前提に次回はパス解決を説明を説明します。

Rustのモジュールを詳細に理解する(4) インポート解決

概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。

  1. モジュール入門
  2. 名前解決の役割と用語
  3. モジュールグラフの構造
  4. インポート解決
  5. 可視性
  6. パス解決
  7. 名前解決とマクロ

本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。

注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。


インポート解決とパス解決

名前解決の目標は foo::bar::baz を解決することですが、その途中でまずインポート (use) を解決する必要があります。これをインポート解決といいます。

インポート解決の中でもパスを解決する必要がありますが、この時点では use および pub(in) 内のパスだけ解決すればよいことに注意しましょう。それ以外のさまざまなパスはインポートのあとに解決されます。

use tree

use の構文は何段階かに分けて進化してきましたが、本質的には昔から変化しておらず、以下の2つしかありません。

  • 単独別名インポート use foo::bar as baz;
    • ただし、やや特殊な仕様として as _ がある (前述)
    • また ::{self} がある場合は名前空間を制限する規則がある
  • globインポート use foo::bar::*;

たとえば、以下のuseは

use std::{
    self, fmt,
    io::{self, Read, Write as IoWrite},
    ops::*,
};

……以下のように分解することができます。 (build_reduced_graph_for_use_tree)

use std as std; // ただし、型名前空間しかインポートしない
use std::fmt as fmt;
use std::io as io; // ただし、型名前空間しかインポートしない
use std::io::Read as Read;
use std::io::Write as IoWrite;
use std::ops::*;

ただし、この展開では以下のようなケースに注意が必要です。

// 素直に展開すると無が生成され、コンパイルが通ってしまう
// unstable moduleを参照しているのでコンパイルを弾いてほしい
use std::intrinsics::{};

このようなケースではダミーインポートが生成され、それによって当該モジュールにアクセス権があることを担保しています。

禁止されるインポート形式

以下のようなインポートは形式的に禁止されています。

// 0セグメントインポートの禁止
use {self}; // error
use ::{self}; // error

// selfまたはsuperで終わるパスのインポートの禁止
use self; // error
use self as s; // error
use super; // error
use super as s; // error

// $crate自体のインポートは可能だが廃止予定
macro_rules! foo {
    () => {
        use $crate; // warning
        use $crate as s; // warning
    }
}
foo!();

// 特殊な名前のままのインポートの禁止
use crate; // error
// use crate as s; // これはOK

// 0セグメントglobインポートの禁止 (prelude glob importになるため)
use *; // error
use ::*; // error

また、trait itemのインポート自分自身からのglobインポートは意味的に弾かれます。

trait Foo {
    fn foo();
}
use Foo::foo; // error

trait Bar {}
use Bar::*; // error

use self::*; // error

Rust2015モード

Rust2015の use ではデフォルトで絶対パスが用いられていました。この互換性の確保のために、以下の条件で :: が先頭に付与されたかのように扱われます

  • グローバル2015モードである。
  • 当該 use パスの先頭セグメントがRust2015モードのクレート由来である。
  • 当該 use パスの先頭セグメントがキーワードではない。 (たとえば、 self::crate:: ではない。すでに :: で始まっている場合も除外する)

グループインポートの場合は、 {} を個別のuseに分解した状態で上の規則が適用されます。

extern crate の処理

extern crate の処理は簡単で、指定したクレートのルートを指定した場所に継ぎ木するだけです。

// failureクレートのルートを crate::f として継ぎ木する。
extern crate failure as f;

use の処理は最小不動点

extern crate に比べて、 use の処理は簡単ではありません。 use は順序よく解決されるとは限らないからです。

そのため、最初の処理ではどこにどういう use 指令があったかだけ覚えておいて、最小不動点アルゴリズムにより可能な部分から解決していきます。

// (2)→(1) の順で解決する必要がある

use m1::A; // (1)
mod m1 {
    pub use crate::m2::A; // (2)
}
mod m2 {
    pub struct A;
}

以下では、このことを念頭に置きつつ話を進めていきます。

単独別名インポートの解決

単独別名インポートの規則は比較的簡単ですが、名前空間についてだけ気をつけることがあります。 use A as B は、

このような仕様のため、 use を単に使うだけでは特定の名前空間をインポートするのは不可能です (トリッキーな方法で実現可能)

// 型としてのSerializeとderiveマクロとしてのSerializeを両方インポートする
use serde::Serialize;

// モジュールとしてのenvとマクロとしてのenvを両方インポートする
use std::env;

また、 use foo::bar as baz; という指定があったとしても、各名前空間self::baz が利用可能になるかどうかはこの時点では確定していないことになります。「あとでなかったらエラーにするから」という理由で、先行して self::baz が存在することを仮定して処理を進めることはできないというわけです。

globインポートの解決

globインポートはより複雑な仕様を持っています。(RFC1560にその経緯などが書いてあります。) use A::*; は以下のように処理されます。

  • A 直下にある利用可能な全ての名前空間・名前をインポートする。
  • ただし、globインポート以外の定義がすでに存在したら、そちらが優先される。
  • globインポート同士で衝突した場合は、個別に以下のルールで仲裁される。
  • 該当がなかった場合 (e.g. 空モジュールのインポート) でもエラーにはならない
fn main() {
    {
        // glob importは優先度が低い
        use std::sync::*;
        use std::rc::{Rc, Weak};
        let x: Weak<i32>; // OK
    }
    {
        // glob import同士は競合する
        use std::sync::*;
        use std::rc::*;
        let x: Weak<i32>; // ERROR
    }
    {
        // 複数のglob importが同じDefに解決される場合はOK
        use core::cell::*;
        use std::cell::*;
        let x: Cell<i32>; // OK
    }
}

最小不動点アルゴリズム

これらの条件を踏まえつつ、実際には解決途中の状態も考慮に入れながら処理しています。たとえば、以下のようなコードを考えます。

use crate::m1::f::X;

fn main() {
    dbg!(X);
}
pub fn f() {}

mod m1 {
    pub use crate::m2::f;
    pub use crate::m3::*;
}
mod m2 {
    pub use crate::f;
}
mod m3 {
    pub mod f {
        pub const X: i32 = 42;
    }
}

最初の行の crate::m1::f::X を解決するためには、型名前空間crate::m1::f を解決する必要があります。これの候補は2つあります。

  • m1 内の pub use crate::m2::f;
  • m1 内の pub use crate::m3::*;

この2つのうちどちらに解決されるかは、前者がどの名前空間でインポートされるかに依存します。 m2 の中身を見ても pub use crate::f; としか書いていないので、そのためにはさらに遡って crate 直下の f がどの名前空間に存在するかを知る必要があります。

このように、インポートの解決順序はあまり自明ではありません。そこで、名前解決のさいは「現状どこまで判明しているのか」を正確に表現しつつ、次に確実にわかることを順番に探していきます (最小不動点)。

モジュール木の構築とインポート文の管理

前述のように、名前解決の最初の処理はモジュール木の構築です。これのメインの処理は build_reduced_graph_for_item です。

たとえば、定数 const X: i32 = 42; の処理は比較的簡単です。これは define メソッド を呼ぶことでモジュールにDefを紐付けることができます。

これによって Module が作られます。Moduleの主要なデータは以下の通りです。

  • 親リンク (parent), 子リンク (resolutions), 正規祖先リンク (normal_ancestor_id)
    • 匿名モジュールは親リンクを持つが、親からの子リンクを持たない
  • このモジュールを入力とするglobインポートの一覧 (glob_importers)
  • このモジュールを出力とするglobインポートの一覧 (globs)

build_reduced_graph_for_item によって、

  • 通常の定義はそのまま resolutions に紐付けられます。
  • use foo::bar;extern crateresolutions に紐付けられます。
  • use foo::*;globs に紐付けられます。

use foo::bar; をこの時点で resolutions に紐付けていいのは NameResolution にそれを表現するための仕組みがあるからです。 resolutions 自体は識別子と名前空間からNameResolutionを引くマップですが、そのNameResolutionは以下のような定義になっています

#[derive(Clone, Default, Debug)]
/// Records information about the resolution of a name in a namespace of a module.
pub struct NameResolution<'a> {
    /// Single imports that may define the name in the namespace.
    /// Import directives are arena-allocated, so it's ok to use pointers as keys.
    single_imports: FxHashSet<PtrKey<'a, ImportDirective<'a>>>,
    /// The least shadowable known binding for this name, or None if there are no known bindings.
    pub binding: Option<&'a NameBinding<'a>>,
    shadowed_glob: Option<&'a NameBinding<'a>>,
}

つまり、 NameResolution は単に最終的な解決結果ではなく、「暫定2位までの解決結果」と「この名前を定義しうる単独別名インポートの一覧」を保持しています。なので、 binding が存在していても、これがglobインポート由来かつ他に単独インポートが存在するかもしれない場合はこの結果は確定ではないということになります。

最小不動点ループ

最小不動点処理のエントリーポイントは ImportResolver::resolve_imports にあります。ここでは以下のような処理をしているわけです。

では、インポートが未解決というのはどういう状態でしょうか。

まず第一に、インポート元のモジュールがそもそも確定していない場合があります。たとえば、 use foo::bar::baz as hoo; を処理するためにはまず foo::bar が何であるかが確定している必要がありますが、この foobar 自体が use 由来かもしれず、その場合は手のつけようがありません。これが確定すると imported_modules というフィールド に保存されます。

globインポートの場合、この時点でglobインポート自体は必ず「解決」扱いになります。というのも、globインポート自体の「解決」は基本的には以下の2つの処理を(この順番で)するだけだからです。

  1. インポート元モジュールの glob_importers に当該インポート指令を追加する。 (→今後同期されることを保証)
  2. インポート元モジュールでこの時点で解決済みになっているDefを列挙し、インポート先に継ぎ木する。 (→過去分の同期を完了させる)

その後の処理はインポート元モジュールが変化したタイミングでの glob_importers によるフックに移譲されます。

一方、単独別名インポートの場合は、3つの名前空間それぞれでインポートの可否を調べ、3つの結果がすべて確定した時点で解決済みになります。「この名前空間ではインポートできるかもしれないし、できないかもしれない」というものが残っているうちは未解決です。

また、マクロ展開に由来する未解決状態も考えられますが、これはまた話がややこしくなるので別途説明します。

通常の use 以外のインポート指令

上のアルゴリズムで、インポート解決の単位となっていたインポート指令は ImportDirective および ImportDirectiveSubclass として定義されています。これを見ると通常の use も含めてインポート解決によって解決されるものは以下のものがあることがわかります。

  • 通常の単独別名インポート (use foo::bar as baz;)
  • ::{self} 形式の単独別名インポート (use foo::bar::{self} as baz;)
    • この場合は、型名前空間のみがインポートされる。
  • 通常のglobインポート (use foo::* as baz;)
  • prelude用のglobインポート
  • extern crate によるインポート
    • インポート解決では使われない。
    • rustfix用?
  • #[macro_use] extern crate によるインポートのマクロ部分
    • インポート解決では使われない。
    • rustfix用?

モジュール解決のファイナライズ

最小不動点アルゴリズムは、解決可能なものを処理した時点で停止します。この時点では必要なものがすべて解決されたかはわからないので、finalize_imports メソッド でそれを確認しています。

まとめ

use 自体の仕様が、グロブのシャドウイング名前空間の違いに由来する複雑さを抱えています。その上、インポート解決はuse の解決を順序不定で行う必要があり、そのためにRustコンパイラは、「どこまでが既に判明しているのか」という状態を慎重に更新しながら適切に状態を更新し続けるという難しい処理を実装しています。

次回は可視性について扱います。可視性はインポート解決の動作にかかわっているものの、いったん可視性抜きで説明できることが多かったので分割しました。

Rustのモジュールを詳細に理解する(3) モジュールグラフの構造

概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。

  1. モジュール入門
  2. 名前解決の役割と用語
  3. モジュールグラフの構造
  4. インポート解決
  5. 可視性
  6. パス解決
  7. 名前解決とマクロ

本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。

注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。


モジュール木からモジュールグラフへ

Rustのモジュールはファイルシステムのように木構造がベースになっています。(前述のように、mod m1; のようになっているものは構文解析の段階で展開されているので、ここでは統一して扱います)

pub mod m1 {
    pub mod m2 {
        mod m3 {
            
        }
        mod m4 {
            
        }
    }
}

しかし、 use が絡むと話が難しくなります。これはシンボリックリンクのようなもので、相互参照や再帰的なモジュールを作ることができてしまいます。

pub mod m1 {
    pub mod m2 {
        mod m3 {
            // m3とm4で相互にインポートしている
            use super::m4;
        }
        mod m4 {
            use super::m3;
        }
        // m1::m2::m1::m2::...
        pub use crate::m1;
    }
}

このモジュールグラフを構築する部分が名前解決の要ですが、glob importなどがあるため単純にはいきません。また、木構造といっても単純な木構造とは言えない事情がいくつかあります。これらの詳細な事情をひとつずつ解説していきます。

名前空間

Rustではひとつのモジュールに同じ名前のDefを最大3個まで紐付けられます。これを名前空間 (namespace)といい、以下の3つの名前空間があります。

(C++C#では、Rustのモジュールに対応するもののことを名前空間と呼んでいます。Rustの名前空間C言語の名前空間などに近い概念だといえます。)

たとえば、以下のようにタプル構造体を定義した場合は実際には2つのDefが紐付いています。 (ユニット構造体の場合も同様)

struct Color(u8, u8, u8);
fn main() {
    let color: Color = Color(255, 255, 255);
    //                 ^^^^^ 関数としてのColor (値名前空間)
    //         ^^^^^ 型としてのColor (型名前空間)
}

レコード構造体の場合は型名前空間しか消費しないので、関数とのオーバーロードが可能です。(推奨するわけではありません)

struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

#[allow(non_snake_case)]
fn Color(red: u8, green: u8, blue: u8) -> Color {
    Color { red, green, blue }
}

RFC0234の規定により、列挙型のレコードバリアントは値名前空間も予約することになっています。

また、 serde::Serialize はトレイトとしてもderiveマクロとしても振る舞います。

use serde::Serialize;

// トレイトとしてのserde::Serializeを参照する場合
impl Serialize for MyStruct1 { .. }

// deriveマクロとしてのserde::Serializeを参照する場合

#[derive(Serialize)]
struct MyStruct2 { .. }

use as _

Rust1.33.0から、以下のような別名インポートが可能になりました。(RFC2166)

// Read/Writeをスコープ内トレイトの解決に利用する。
// しかし、このモジュールのRead/Writeという名前を汚染しない。
use std::io::{Read as _, Write as _};

これは後述するスコープ内トレイトの解決のために使われますが、特徴として as _ で作られた名前は重複しないという特殊仕様があります。これは名前解決前に _ を実際には別々のsymbol idに割り当てることで実装されています。Rustコンパイラ識別子を整数に変換して保持しているので、 _ に重複して別々の整数を割り当てるというハックが可能というわけです。

なお、意図してかは不明ですが、as _ でインポートされたものもglob importに含まれるようです。 (playground)

mod m1 {
    pub use std::io::BufRead as _;
}
use m1::*;

fn main() {
    std::io::stdin().lock().read_line(&mut String::new()).unwrap();
}

無名のconst

コンパイルアサーションのための const _ (RFC2526; 不安定機能) でも上記と同様の仕組みをとっています。 (playground)

#![feature(underscore_const_names)]

const _: i32 = 42;
const _: i32 = 53;

これらはトレイトではないので、外から見て意味のある挙動はないはずです。 (名指しできないのにスコープ内に置いて意味があるのはトレイトだけ)

クレートルート

先ほど書いたようにモジュール構造は(extern crateuse を除いて考えると)木構造になっています。

mod m1 {}
mod m2 {}

したがって、上の例ではまずクレートのルート(根)が暗黙の最上位モジュールとして存在し、その直下に m1m2 があるという構造になります。

そして、依存している他のクレートはそれぞれが別のモジュール木になっています。つまりモジュール木というのは不正確でモジュール森と考えたほうが正確です。他のクレートを参照できる仕組みについては後述します。

// serdeクレートのルートを crate::serde にマウントする (Rust2015時代のイディオム)
extern crate serde;

// failureクレートのルートを crate::failure にマウントせず組み込みプレリュードとして直接呼び出す (Rust2018時代のイディオム)
use failure::Fail;

非正規モジュール

クレートルートと mod 以外に、モジュールとして振る舞うものが3つありますenumtraitブロックです。

enum MyEnum {
    // MyEnumモジュールの直下にVariant1, Variant2があるものとして扱われる
    Variant1,
    Variant2,
}

trait MyTrait {
    // MyTraitモジュールの直下にmy_methodがあるものとして扱われる
    fn my_method(&self);
}

fn my_function() {
    // my_functionモジュールの直下に匿名モジュール[0]があるものとして扱われる
    // 匿名モジュール[0]の直下にはAと匿名モジュール[1]がある
    struct A;
    {
        // 匿名モジュール[1]の直下にはBがある
        struct B;
    }
}

enumがモジュールとして扱われているのはもちろん、以下を可能にするためです。

enum MyEnum {
    // デフォルトでは MyEnum::Variant1, MyEnum::Variant2 として参照可能
    Variant1,
    Variant2,
}

// こうすることで単に Variant1, Variant2 として参照できるようになる
use self::MyEnum::*;

traitがモジュールなのは名前解決時にメソッド名まで解決してしまいたいからです。 (型相対なパスは型推論をしないと解決できないが、トレイト相対なパスは型推論によらず解決できる)

let x: i32 = Default::default();
//           ^^^^^^^^^^^^^^^^ ここまで解決する
let x = <i32 as Default>::default();
//              ^^^^^^^^^^^^^^^^^ ここまで解決する
let x = i32::default();
//      ^^^ ここまでしか解決しない (Defaultトレイトのdefaultかどうかははっきりしないので)

(ただし、トレイトアイテムのuseは別途禁止されています)

ブロックひとつひとつがモジュールであるおかげで、ローカルでしか使わない構造体を定義したり、一時的なインポートをすることができます。

fn do_something_with_io() {
    // この関数のみで有効なuse
    use std::fmt;
    {
        // このブロック内でしか使わない型
        struct StructForInternalUse;
        impl fmt::Debug for StructForInternalUse { ... }
        ...
    }
}

非正規モジュールは相対パスのときに無視されます。 (self:: は最も近い正規モジュールを指す)

const X: i32 = 42;

fn main() {
    const X: i32 = 53;
    // スコープ内には2つのXがあるので、より内側にある53が採用される
    assert_eq!(X, 53);
    // 最も近い正規モジュールはクレートルートなので、その直下のXである42が採用される
    assert_eq!(self::X, 42);
}

mod, enum, trait として定義されているものはDefとして親モジュールから名前で参照できますが、ブロックに対応する匿名モジュールは親モジュールからのリンクはなく、子から親へのリンクしかありません。

エイリアス列挙型のバリアント

以下のコードは現在のRustでは動きません。 (playground)

// #![feature(type_alias_enum_variants)]

enum MyEnum {
    Variant1,
    Variant2,
}

// 同じDef
use MyEnum as MyEnum2;

// 異なるDef
type MyEnum3 = MyEnum;

fn main() {
    // OK
    let x1 = MyEnum::Variant1;

    // OK
    let x2 = MyEnum2::Variant1;
    
    // エラー。 #![feature(type_alias_enum_variants)] が必要
    let x3 = MyEnum3::Variant1;
}

前述のように、列挙型のバリアントに出てくる ::use MyEnum::*; などを可能にするために名前解決によって解決されます。ところが、名前解決の観点からは型エイリアスは同じDefではないので、列挙型に(モジュール的に)ぶら下がっているバリアントは引き継ぎません。

RFC2338 でこの不便が解消される予定です。RFC2338の現状の実装では名前解決に手を入れていません。かわりに、型解決の段階で関連アイテムの亜種として処理しています。本記事の手前のほうで、「名前解決はパスを途中まで解決する」と書きましたが、それで言うと以下のようになります。

#![feature(type_alias_enum_variants)]

enum MyEnum {
    Variant1,
    Variant2,
}
use MyEnum as MyEnum2;
type MyEnum3 = MyEnum;

fn main() {
    let x1 = MyEnum::Variant1;
    //       ^^^^^^^^^^^^^^^^ 名前解決でここまで解決される

    let x2 = MyEnum2::Variant1;
    //       ^^^^^^^^^^^^^^^^^ 名前解決でここまで解決される
    
    let x3 = MyEnum3::Variant1;
    //       ^^^^^^^ 名前解決でここまで解決される
    //                ^^^^^^^^ 型推論時にここを解決する
}

この実装ではRFC本文に書かれている例(type alias enum variant を use しようとしている)がコンパイルできません。これが意図したものであるかどうかはやや不明で、eddyb氏の発言を見るにはじめからあまり考慮していないようにも見えます。目的論的にも、 use が必要な場面ではもとの列挙型から引っ張ってくれば十分とも言えるので、RFCの本文がoutdatedであると考える余地があります。

もし type alias enum variant を use でも使えるようにするとなった場合は、モジュール木の構造自体に何らかの影響を及ぼすと思われます。

トレイト別名

RFC1733のトレイト別名はトレイト別名から関連アイテム(メソッド・関連関数・関連定数・関連型)を引くことについて言及していないので、今のところ名前解決への影響はなさそうです。

まとめ

モジュール木はナイーブにはモジュールをノードとし、識別子をラベルとするラベルつき木といえますが、匿名モジュールや名前空間などがあるため正確にはより複雑な構造を持っていることを説明しました。

次回use の挙動と、それを解決するためのアルゴリズムについて説明します。

Rustのモジュールを詳細に理解する(2) 名前解決の役割と用語

概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。

  1. モジュール入門
  2. 名前解決の役割と用語
  3. モジュールグラフの構造
  4. インポート解決
  5. 可視性
  6. パス解決
  7. 名前解決とマクロ

本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。

注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。


Rust2015までのモジュールまわり

ここまではRust2018イディオムを前提に説明をしてきましたが、以降は詳細な説明に入るためにRust2015(特に1.29以前)での状況も軽く説明します。

  • crate:: は存在しませんでした。
    • かわりに、 :: で始まる旧式の絶対パスを使っていました。
    • また、マクロ衛生性のために $crate:: という特殊な絶対パスがありました。
  • extern prelude (外部クレート名がスコープ内に入る機能) は存在しませんでした。
    • かわりに、 extern crate で外部クレートのルートをローカルクレートのルート直下にマウントしていました。
  • マクロは名前解決に統合されていませんでした。
    • マクロをuseするかわりに、 #[macro_use] extern crate を使っていました。
    • マクロをpubでエクスポートするかわりに、 #[macro_export] macro_rules! を使っていました。
    • マクロ定義はマクロ使用よりも手前に存在する必要がありました。
  • 一様パスではありませんでした。
    • use または pub(in) の指定時のみ、暗黙の絶対パスが適用されていました。

また、名前解決自体とは直接は関係ないですが、サブモジュールを別ファイルに分離するときは親ファイルが lib.rs/main.rs/mod.rs である必要がありました。 (たとば、 foo.rsmod bar; とやって foo/bar.rs を参照するのは禁止されていた。この場合は foo.rsfoo/mod.rs にリネームする必要があった)

なお、1.30以降では、Rust2018へのスムーズな移行のために上記の機能のうち一部をRust2015にも開放しています。

Rust2015-2018移行イディオム

Rustは2015のプロジェクトを2018に移行するための仕組みを提供しています。これは以下の手順を踏みます。

  1. ビルドする。
  2. cargo fix --edition を実行する。
  3. ビルドする。
  4. Cargo.tomledition = "2018" を記載する。
  5. ビルドする。
  6. cargo fix --edition-idioms を実行する。
  7. ビルドする。

この手順からわかるように、Rust2018は「Rust2015でも2018でもコンパイルが通る中間状態」が(基本的には)存在するように設計されています。モジュールに関しては、以下のような状態がそれにあたります。

  • extern crate は維持します。
  • :: で始まるパスは以下の2種類に分けます。
    • ::[外部クレート名] となっているものは維持します。
    • ::[クレート内の名前] となっているものは crate:: に置き換えます。
  • use で使われているパスについても、上記と同様の規則を適用して置き換えます。

以下で述べる仕様の一部は、このような「Rust2015時点での互換性」や「移行状態が存在すること」を達成するために存在しています。

名前解決とは

ここからはあらためて、用語の定義をおさらいします。まずは名前解決について。

たとえば、以下のコード (playground) は42を出力します。

fn main() {
    dbg!(Vec::new());
}

#[allow(non_snake_case)]
mod Vec {
    pub fn new() -> i32 {
        42
    }
}

このように、同じ Vec::new でもそれが何を指すかは場合によって異なります。これを(途中まで)確定させるのが名前解決の役割です。

グローバルな名前だけではなくローカルな名前の解決もここで行われます。たとえば、

fn main() {
    let x = 42;
    let x = 53;
    dbg!(x);
}

で、 dbg!(x) 内の x がどちらの let x に対応しているかを決定するのはこのフェーズです。

パス

パスは :: で区切られた名前の列です。ただし、 ::<> という形のもの (turbofish) はジェネリクスパラメーターの一部なので、パスセグメントには数えません。

  • Vec ... 1セグメントのパス
  • std::vec::Vec ... 3セグメントのパス
  • Vec<i32>::new ... 2セグメントのパス
  • Vec::<i32>::new ... 2セグメントのパス

特殊なものとしては ::, $crate::, crate::, self::, super:: で始まるものがあります。大雑把には以下のような役割です。

  • :: ... Rust2015では絶対パス、Rust2018ではextern preludeパスとして振る舞います。内部的には {{root}}:: として扱われます
  • $crate::, crate:: ... 絶対パスです。元々は $crate というマクロ専用のパスがあり、同じ用途の crate があとから追加されました。 $crate::crate:: の動作はほぼ同じです。
  • self::, super:: ... 相対パスです。 super は先頭または self:: の直後から任意回数続けることができます。

. で始まるもの (s.parse::<i32>()parse など) はパスではありません。

パスとジェネリクスパラメーター

型文脈と値文脈ではパス要素にジェネリクスパラメーターを付与できます。意味的な扱いは後述しますが、以下のような規則があります。

  • モジュール文脈 (use, pub()) ではジェネリクスパラメーターは利用不可能
  • 型文脈 (構文的に型やトレイトが期待されている場所) では <>ジェネリクスパラメーターを指定可能
    • ()-> という特殊なジェネリクスパラメーター指定も利用可能。これは Fn(i32) -> i32 のように使います。
    • ::<> も一応使える
  • 式文脈 (構文的に式やパターンが期待されている場所) では ::<>ジェネリクスパラメーターを指定可能

型文脈 or 式文脈の区別は難しくないですが、少しわかりづらいケースを書いておきます。

  • レコード構造体の初期化構文 MyStruct { .. } は式文脈だが、名前空間(後述)としては型名前空間が用いられる
  • as の直後は型文脈
    • 1 as usize < 2 のパースに失敗するのはこのため
  • 式文脈のパスでも、そのジェネリクスパラメーターは型文脈になる
    • Vec::<Vec<i32>>::new() のようになる

ジェネリクスパラメーターを実際に処理するのは型推論・型検査時なので、以降はジェネリクスパラメーターには触れません。

修飾子つきパス

<>:: で始まる特殊なパスもあります(これはturbofish ::<> とは別物)。これはさらに以下の2種類に分けられます。

  • <T>:: で始まるもの ... T は型。
  • <T as Trait>:: で始まるもの ... T は型で、 Trait はトレイト。

この2つは名前解決の観点からは大きく異なります。 <T>:: は型相対パスなので、基本的に名前解決ではほとんど何もできずに型推論にその処理を委ねることになります。

一方、 <T as Trait>:: のほうは実際は単なるジェネリクスパラメーターのついたパスです。トレイトにとっての Self は第0ジェネリクスパラメーターであるという原則を念頭に置いて、以下のコードを眺めるとそれがわかると思います。

// `A: Into<B>` は、AをBに変換できるということである。

// AもBも指定しないとき
let x: i64 = Into::into(42);
// Bだけ指定するとき
let x = Into::<i64>::into(42);
// Aだけ指定するとき
let x: i64 = <i32 as Into<_>>::into(42);
// AもBも指定するとき
let x = <i32 as Into<i64>>::into(42);

こういった背景から、たとえば <i32 as std::default::Default>::default は4セグメントのパス (= std::default::Default::default) として扱われます。

Def

Rustにおける名前解決はコンパイラのフェーズのひとつで、パス(の途中まで)をDefに解決する処理です。Defは src/librustc/hir/def.rsに定義があります。

Defの内容は多様で、現状は以下の内容が含まれています (読み飛ばしてOKです)

  • 名前空間に属するDef
    • モジュール (mod my_module;)
    • 構造体 (struct MyStruct;)
    • 共用体 (union MyUnion {})
    • 列挙型 (enum MyEnum {})
    • 列挙型のバリアント (enum MyEnum { MyVariant })
    • トレイト (trait MyTrait {})
    • 存在型 (existential type MyType;) RFC2071
    • エイリアス (type MyType = i32;)
    • 外部型 (extern { type MyType; }) RFC1861
    • トレイトエイリアス (trait MyTrait = Into<i32>;) RFC1733
    • 関連型 (trait MyTrait { type MyAssocType; } / impl MyTrait for MyStruct { type MyAssocType = i32; })
    • 関連存在型 (impl MyTrait for MyStruct { existential type MyAssocType; }) RFC2071
    • プリミティブ型 (i32)
    • 型パラメーター (fn foo<MyTyParam>() {})
    • Self型 (Self)
    • ツールモジュール (rustfmt など)
  • 名前空間に属するDef
    • 関数 (fn my_function() {})
    • 定数 (const MY_CONST: i32 = 42;)
    • 定数ジェネリクスパラメーター (fn foo<const MY_CONST: i32>() {}) RFC2000
    • static変数 (static MY_STATIC: i32 = 42;)
    • タプル構造体・ユニット構造体のコンストラクタ (struct MyStruct; / struct MyStruct();)
    • 列挙型のタプルバリアント・ユニットバリアントのコンストラクタ (enum MyEnum { MyVariant1, MyVariant2() })
    • タプル構造体のSelfコンストラクタ (Self)
    • 関連関数 (trait MyTrait { fn my_assoc_function(); } / impl MyTrait for MyStruct { fn my_assoc_function() {} })
    • 関連定数 (trait MyTrait { const MY_ASSOC_CONST: i32; } / impl MyTrait for MyStruct { const MY_ASSOC_CONST: i32 = 42; })
  • 名前空間に属するローカルDef
    • ローカル束縛 (let my_var = 42;)
    • Upvar: キャプチャされた変数 (let my_upvar = 42; let closure = || { .. my_upvar .. };)
    • ラベル ('outer: loop { .. })
  • マクロ名前空間に属するDef
    • マクロ (macro_rules! my_macro {})
    • マクロ以外の属性値 (#[inline] など)

このDefの定義にはextern crateuse が含まれていないことに注意してください。これらは既存のDefと同じものを作ります。したがって、以下のコードでは……

type MyType1 = i32;
use self::MyType1 as MyType2;
type MyType3 = i32;

MyType1MyType2 は同じDefですが、 MyType1MyType3 は別のDefということになります。

名前解決と型解決の役割分担

名前解決は「パス(の途中まで)をDefに解決する処理」と書いた通り、名前解決の時点ではパスが完全には解決されない場合があります。

たとえば、以下のコード (playground) はエラーになります。これは、 Option が2種類の cloned を持っており、 Option::cloned と書いた時点では確定できないためです。

fn main() {
    Option::cloned(Some(&42));
    //~^ERROR multiple applicable items in scope
}

これは、型を明示することで解決できます。 (playground)

fn main() {
    Option::<&i32>::cloned(Some(&42));
}

この「型を使って解決する」というのは、もっと複雑な形で現れることもあります。たとえば以下のように書いても解決されます。 (playground)

fn main() {
    Option::<<&Vec<i32> as IntoIterator>::Item>::cloned(Some(&42));
}

こういうものまで名前解決の時点で解決しようとすると複雑になりすぎるため、型に依存して決まる部分名前解決より後のフェーズ(型推論・型検査)で行われます。そのため、名前解決では以下のようにパスのどこまでが何というDefに解決されたかを最終的な解決結果として用います。

#[derive(Copy, Clone, Debug)]
pub struct PathResolution {
    // 途中までの解決結果
    base_def: Def,
    // 解決できなかったパスセグメントの個数
    unresolved_segments: usize,
}

たとえば、名前解決は以下のところまで解決します。

let v = Vec::<i32>::new();
//      ^^^ ここまで
let v = Clone::clone(&v);
//      ^^^^^^^^^^^^ ここまで
let v = Vec::clone(&v);
//      ^^^ ここまで
let v = <Vec<i32> as IntoIterator>::Item::min_value();
//                   ^^^^^^^^^^^^^^^^^^^ ここまで
let it = std::iter::once(42);
//       ^^^^^^^^^^^^^^^ ここまで

同じ理由で、メソッド名も名前解決では処理されません

let x = Some(42);
let x = x.unwrap();
//        ^^^^^^ これがOption::unwrapなのかResult::unwrapなのかは後で決まる

このように、ここで解説している「名前解決」の範疇では型推論に頼らない範囲での解決しか行いません。この2段階解決の仕組みを頭に入れておくと構造の整理がつけやすいと思います。

名前解決の手順

名前解決の責任範囲は明確になりました。その手順を少し細かく割ると以下のようになります。

  1. useextern crate 以外の構造 (モジュール木) を確定させる。
  2. useextern crate を解決する。
    • これによりモジュールグラフが構成される。
  3. 各パスの指している内容を(途中まで)確定させる。
    • ただし、マクロの名前解決の結果1.に戻る場合がある (RFC1561)

こうして名前解決が終わると、その結果を織り込みながらAST(抽象構文木)をHIR(高レベル中間表現)に変換します。HIRはASTとよく似ていますが名前解決済みなので、後のフェーズは各パスが指すDefを簡単に特定できます。

まとめ

foo::bar::baz などのパスが指す実体(Def)がどこなのかを対応づけるのが名前解決です。ただし、 Vec::new()x.unwrap() など型に相対して決まるものはこのフェーズでは解決できないので、途中まで解決した状態で進みます。

次回は、モジュールグラフの構造を説明します。

Rustのモジュールを詳細に理解する(1) モジュール入門 (この回は簡単です!)

概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。

  1. モジュール入門
  2. 名前解決の役割と用語
  3. モジュールグラフの構造
  4. インポート解決
  5. 可視性
  6. パス解決
  7. 名前解決とマクロ

本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。


モジュールとは

モジュールはRustのプログラムを分割する単位のひとつです。ワークスペース・クレート・モジュールという単位になっていると考えてください。

  • ワークスペースは関連するライブラリ(クレート)をひとつにまとめたもので、通常1つのgitリポジトリに対応します。たとえばAWSクライアントライブラリであるrusoto/rusotoには1つのワークスペースがあり、その中にはAWSクライアントのコアライブラリと各サービスに対応するライブラリが存在しています。開発時に依存関係のビルドを集約するためのものなので、 crates.io にアップロードした時点でワークスペースというまとまりに意味はなくなります。
  • クレート (crate)は依存解決の単位です。クレート同士は一方通行の依存関係しか持てません(dev-dependenciesなどの例外あり)
    • 依存の方向がはっきりしている場合(BはAに依存するが、AはBに依存せずに単独でも使える)はクレートを分割するのがよいでしょう。
    • 並列コンパイラは実装途上なので、大きなプロジェクトで並列性を高める場合は依存関係を考慮してクレートを分割するのがセオリーです。たとえばRustコンパイラでは共通インターフェースを rustc 内で定義し、各フェーズのアルゴリズムrustc_resolve, rustc_typeck, rustc_mir などで実装してから rustc_driver で集約するという構造になっています。
  • モジュールはクレート内で機能を小さい単位にわけて整理したり、実装を隠蔽したりするために使います。
    • 1ファイルが1モジュールになるように書くのが一般的です。
    • 基本的にクレート全体がコンパイルされるので、スリム化には使えません。

実際にプログラムを書き始めるときは、1モジュールから始めて、必要に応じてモジュール分割→クレート分割→リポジトリ(ワークスペース)分割という形が一般的かと思います。

Rustのモジュールは(他の多くの言語と同様)ファイルシステムのように木構造になっており、モジュールを指定するときの記法 (foo::bar::baz) はそのものずばりパスと呼びます。

Rust2018で一様パスが導入された

Rust2018にあわせてRFC2126で定められたモジュールシステムの改革が実装され、2015時点でのモジュールシステムの問題点が改善されました。互換性のために古い書き方も残されているので全体の仕組みは複雑になりましたが、イディオマティックなRustコードでのモジュールの動作はかなりわかりやすくなったのではないかと思います。

Rust2018時代のイディオマティックなRustコードでは、

  • extern crate を使いません。
    • 代わりに、extern prelude (1.30.0で安定化) を使います。
  • #[macro_use], #[macro_export] も使いません。
    • 代わりに、マクロの名前解決 (RFC1561; 1.30.0で安定化) を使います。
  • :: で始まるパスは基本的に使いません。
    • :: で始まるパスの意味自体がRust2018で大きく変化しました。
    • 代わりに、 crate:: (1.30.0で安定化) を使います。
  • $crate:: で始まるパスは使いません。
    • 代わりに、 crate:: (1.30.0で安定化) を使います。

特に、これらを守ることで一様パス (1.32.0で安定化) の恩恵を受けることができます。すなわち、 usepub(in) でのパス指定と、それ以外の箇所でのパス指定が一貫するということです。 (playground)

pub enum RealWeekday {
    Sun,
    Fri,
    Sat,
}

pub mod m1 {
    pub enum Weekday {
        Sun,
        Sat,
    }

    // デフォルトパス (use内)
    pub use Weekday::Sun;
    // デフォルトパス (use以外)
    pub const X: Weekday = Weekday::Sun;

    // extern preludeパス (use内)
    pub use chrono::naive::MIN_DATE;
    // extern preludeパス (use以外)
    pub const Y: chrono::NaiveDate = chrono::naive::MIN_DATE;

    // 相対パス (use内)
    pub use self::Weekday::Sat;
    // 相対パス (use以外)
    pub const Z: self::Weekday = self::Weekday::Sat;

    // 絶対パス (use内)
    pub use crate::RealWeekday::Fri;
    // 絶対パス (use以外)
    pub const W: crate::RealWeekday = crate::RealWeekday::Fri;
}

これは1.29以前のRust2015では以下のように書く必要がありました。 (playground)

// 1.29まではextern crateが必須だった
extern crate chrono;

pub enum RealWeekday {
    Sun,
    Fri,
    Sat,
}

pub mod m1 {
    pub enum Weekday {
        Sun,
        Sat,
    }

    // デフォルトパス (use内) → 相対パス
    pub use self::Weekday::Sun;
    // デフォルトパス (use以外)
    pub const X: Weekday = Weekday::Sun;

    // extern preludeパス (use内) → extern crate + デフォルトパス
    pub use chrono::naive::MIN_DATE;
    // extern preludeパス (use以外) → extern crate + 絶対パス(旧)
    pub const Y: ::chrono::NaiveDate = ::chrono::naive::MIN_DATE;

    // 相対パス (use内)
    pub use self::Weekday::Sat;
    // 相対パス (use以外)
    pub const Z: self::Weekday = self::Weekday::Sat;

    // 絶対パス (use内) → 絶対パス(旧)
    pub use ::RealWeekday::Fri;
    // 絶対パス (use以外) → 絶対パス(旧)
    pub const W: ::RealWeekday = ::RealWeekday::Fri;
}

また、extern preludeへの移行により、 use chrono; のような単独useに関する許しがたい挙動 (ルートでは書いてはいけないが、ルート以外では書く必要がある) が解消されたのも非常に好ましい変化だと言えます。

Rust2018時代のパスの解釈

上記のようにパスの解釈が整理されたので、(Rust2018イディオムに限っていえば)シンプルに以下のように説明できます。

  • 通常のパス foo::bar::baz はスコープ内の foo を探索し、なければプレリュードにフォールバックする。
    • stdCargo.toml に指定されている依存関係は、プレリュードの亜種という扱い。
  • self:: または super:: で始まるパスは相対パスで、今いる mod からの相対位置で探索する。
  • crate:: で始まるパスは絶対パスで、クレートのルートから探索する。

これによって、モジュールについて説明しなくても直感的に動くケースが増えたと思います。たとえば

let it = std::iter::once(42);

use std::iter;
let it = iter::once(42);

はどちらも場所に関係なく自然に動作するので、完全修飾名を使うかどうかは単にDRYや可読性や好みの観点で決定すればよくなりました。 (Rust2015では std::iter::once を使うにはルートである or use std; がある or ::std::iter::once と表記する必要があるという複雑な条件を検討しなければならなかった)

モジュールを使いこなすための基本知識3つ

前節で説明した通り、Rust2018ではパスの挙動が比較的シンプルになったので、考えることは少なくなりました。いっぽう、 mod/use の使い方については、知っておいたほうがよいことがもう少しあります。

  • 1クレート = 1ファイル
  • mod はスコープを断ち切る
  • pub useuse の仲間

1クレート = 1ファイル

rustc コマンドは一回の呼び出しで1つの lib.rs を1つの *.rlib に変換します。たとえば以下のような lib.rs が考えられます。

pub mod fmt { ... }
pub mod io { ... }
pub mod sync { ... }
...

しかし、大きなクレートではこのように1ファイルに詰めると巨大化しすぎて破綻します。そのため以下のように分割できるような仕組みになっています。

pub mod fmt; // 中身は `fmt.rs` か `fmt/mod.rs` にある
pub mod io; // 中身は `io.rs` か `io/mod.rs` にある
pub mod sync; // 中身は `sync.rs` か `sync/mod.rs` にある
...

これは構文解析の段階で結合されてしまうので、Rustの仕様上は1ファイルにまとめた場合とほぼ同じ挙動になります。ファイルを置いただけではモジュールにならないのはこういう仕組みだからと考えることもできます。

コンパイラの仕組みとしては以上のようになっています(1クレート = 1ファイル)が、イディオマティックなRustコードではモジュールを最大限ファイルに分割する(1モジュール = 1ファイル)のが一般的です。

mod はスコープを断ち切る

mod は名前解決のスコープを断ち切ります。たとえば、

const X: i32 = 21 + 21;

#[cfg(test)]
mod tests {
    use super::*; // ←
    #[test]
    fn test_everythings_answer() {
        assert_eq!(X, 42);
    }
}

というテストコードは(2015/2018で共通の)頻出パターンです。ここで fn test_everythings_answer から X は(mod 境界をまたいでいるので)そのままでは参照できず、 super::X と書く必要があります。ただ、このようなファイル内テストではファイル内の定義や use をそのまま再利用するほうが自然なので、 use super::* と書いて全てをインポートしています。これによってあたかもスコープが外側まで延長されたかのような感覚でテストコードを書くことができます。

前述のように、テスト以外のイディオマティックなRustコードでは「1モジュール=1ファイル」として書かれるので、この仕組みによってファイル外の use の影響を回避できるようになっていると考えることもできます。

pub useuse の仲間

RFC1560による2016年のモジュールシステム改革以降、 pub use は特別扱いではなく use と一貫した挙動をするようになっています。

#![allow(dead_code, path_statements)]

mod m1 {
    use std::marker::PhantomData;
    pub use std::marker::PhantomPinned;
    
    struct Foo;
    pub struct Bar;
    
    fn foo() {
        PhantomData::<i32>;
        PhantomPinned;
        Foo;
        Bar;
    }
}

fn bar() {
    m1::PhantomPinned;
    m1::Bar;
}

use の働きは単に名前をスコープに入れるというわけではありません。今いるモジュールに定義を「継ぎ木」して、結果として名前がスコープ内に入るという挙動をします。 pub use の場合は、そうして「継ぎ木」した定義が外からも参照できるようになる、という寸法です。

まとめ

Rust2018でモジュールまわりの非直感的な挙動が整理され、比較的直感的に動作するようになりました。

とはいえ、その正確な動作は奥が深いです。次回は名前解決の役割がどこからどこまでなのかを明確にします。