RustのRFC一覧 (~0200)

この記事は古いのでこちらをご覧ください

概要: RustのRFCの一覧を作ろうとしたが、あまりに多いのでとりあえず0200までで公開することにした。なお、単に全てのRFCを列挙したいならばここを見ればよい

このRFCはRustのコミュニティーが管理しているものであり、 “RFC” の元祖であるIETF RFCとは関係ない。いずれもrequest-for-commentの略である。

メタ

スタイル

  • RFC 0199 似たような動作で、所有権/借用の種別だけが異なるようなメソッドの命名規則を定める

構文

  • RFC 0016 属性を let 文、ブロック、式にも使えるようにする (#![feature(stmt_expr_attributes)])
  • RFC 0049 マッチ腕に属性を使えるようにする
  • RFC 0059 ~T 型と ~x 式を削除し、 Box/box で置き換える
  • RFC 0063 二重インクルードによる混乱を防ぐため、 mod foo; による外部ファイルの読み込みに制限をつける
  • RFC 0068 *T 型を *const T にリネーム
  • RFC 0069 バイトリテラル b'x' とバイト列リテラル b"Foo" の導入
  • RFC 0071 const/static 内でも、一定条件下でブロックを使えるようにする
  • RFC 0085 パターンの位置でマクロを使えるようにする
  • RFC 0087 トレイトオブジェクトの追加の境界を &(Foo : Send + Sync) ではなく &(Foo + Send + Sync) のように指定するようにする
  • RFC 0090/0021 字句解析器をより単純にする
  • RFC 0092 iffor などのブロックと紛らわしい場面では構造体の {} を禁止する
  • RFC 0132 UFCS (<T as Trait>::sth / <T>::sth) の導入
  • RFC 0135 where 節の導入 (一部未実装)
  • RFC 0160 if let 構文
  • RFC 0164 スライスパターンをfeature gateにする
  • RFC 0168 {mod, ..} によりモジュール自体とその中身を同時にインポートする (RFC 0532により廃止)
  • RFC 0169 use baz = foo::bar; 構文を廃止し use foo::bar as baz; で置き換える
  • RFC 0179 &p パターンを &p&mut p パターンに分割し、参照のミュータビリティーに応じて使い分けるようにする
  • RFC 0184 foo.0 のように、タプル/タプル構造体のフィールドに整数でアクセスできるようにする
  • RFC 0194 #[cfg(...)] の構文の整理
  • RFC 0198 foo[]/foo[n..m]/foo[n..]/foo[..n] のためのトレイト Slice/SliceMut を導入 (RFC 0439により廃止: foo[]foo[..] になり、 ..[] と独立な構文になった)

意味論・型システム

  • RFC 0001 構造体フィールドの可視性をデフォルトでprivateにする。
  • RFC 0003 未使用の属性のチェック方法を改善する
  • RFC 0008 extern "rust-intrinsic" の廃止 (破棄)
  • RFC 0019 既定実装 impl Send for .. {}, 否定実装 impl !Send for T {} により Send/Sync をライブラリレベルで実現する (#![feature(optin_builtin_traits)])
  • RFC 0026 enum のバリアントの可視性を常にpublicにし、 priv キーワードを廃止する。
  • RFC 0034/0011 struct/enum の型引数に境界 T: Trait を書けるようにし、それが充足されていることを利用側で検査する。
  • RFC 0048 トレイト周りの整理: self の一般化、coherence条件の整理、トレイト選択アルゴリズムの改善など
  • RFC 0066 一時的な値に対する参照を間接的に取った場合も、直接取った場合と同様にその生存期間を延長できるようにする
  • RFC 0079 #[repr(C)] などで明示しない限り、構造体レイアウトは入れ替えられる可能性がある
  • RFC 0107 パターンマッチのガード内でムーブ束縛された変数を使う (未実装)
  • RFC 0109 バージョン込みでcrateを指定できるcrate idを廃止して、ソースコードレベルではcrateの名前だけを指定するようにする
  • RFC 0111 IndexIndex/IndexMut に分割する
  • RFC 0112/0033 Box<T> から &mut T への型強制の廃止(DerefMut型強制とは別) (RFC 0139も参照)
  • RFC 0114 クロージャの整理: unboxed closureのための Fn/FnMut/FnOnce トレイトの導入、 proc の削除、クロージャ型を削除して |_| -> _ を構文糖衣に変更(構文糖衣は現在は廃止)、キャプチャー方式の指定、レシーバモードの明示(現在は廃止)
  • RFC 0115 整数リテラル型がデフォルトで i32 にフォールバックしないようにする (RFC 0212により廃止)
  • RFC 0116 use/let 等による同レベルシャドウイングの廃止 (現在はglob importの復活により、globのshadowingが可能)
  • RFC 0130 ボローチェッカーにおける Box<T> の特別扱いの廃止
  • RFC 0136 publicな定義の型にprivateな型を使うのを禁止する
  • RFC 0139 Box<T> から &T への型強制の廃止 (Deref型強制とは別) (RFC 0112も参照)
  • RFC 0141 生存期間の省略規則の整理
  • RFC 0151 クロージャref を指定しない限りムーブキャプチャーする (RFC 0231により廃止)
  • RFC 0155 固有実装は該当の型と同じモジュールでのみ可能 (RFC 0735により廃止)
  • RFC 0192 トレイトオブジェクト型の生存期間 (RFC 0599も参照)
  • RFC 0195 関連型・関連定数・関連生存期間 (関連生存期間は未実装)

ライブラリ

  • RFC 0040 libstd の実装を libcore, liballoc, liblibc などのライブラリに分割する。
  • RFC 0042/0007 regex crateの同梱 (現在は同梱されていない)
  • RFC 0050 デバッグモードでのみ有効化される debug_assert!() の導入
  • RFC 0060 StrBufString にリネーム
  • RFC 0093 println!format! から地域化の機能を削除し、構文を整理
  • RFC 0100 PartialOrd::partial_cmp を追加
  • RFC 0123 ShareSync にリネーム

コンパイラ

Rustのトレイト選択

概要: トレイト選択とは、トレイト制約の解決方法を検索するコンパイラの処理であり、型推論にもコード生成にも使われる重要な部品である。本記事ではトレイト選択の動作の詳細について解説する。

トレイト選択とは

トレイト選択 (selection) とは、トレイト制約 (obligation) の解決方法を検索する処理である。トレイト制約は、トレイト境界と同じく、

SomeType: SomeTrait<SomeArguments>

という形式をとる。例えば、

let z = x + y;

というコードを書いたとき、 z の型は <X as Add<Y>>::Output であるが、これをより具体的に決定するには、 X: Add<Y> の解決方法を決定する必要がある。

トレイト選択の処理はtraits/select.rsにまとまっているのでここを読むとだいたいわかる。

トレイト選択が呼ばれるタイミング

トレイト選択の本体は select関数 である。これは主に以下のような場所で呼ばれる。

  • 型推論において型を1段階以上解決する必要が生じた場合。 (今あるトレイト制約それぞれについて解決を試みるが、失敗してもよい)
  • 型推論の最後。 (全てのトレイト制約を解決する)
  • 定数式の評価中。 (関連定数・constメソッド等の解決)
  • 単相化後のコード生成。

特に、型推論中のトレイト選択では、型引数を含む上、型の判明していない型推論変数もある。そのためこの時点ではトレイト選択の結果が曖昧である可能性がある。

トレイト制約の解決方法

トレイト制約はいくつかの異なる方法で解決される。それらの分類を与えているのがSelectionCandidateである。

  • ImplCandidate … 最も標準的な解決方法。トレイト実装 (impl MyTrait for SomeType { .. }) により解決する。 (否定実装もこれに含まれる)
  • ParamCandidate … 最も標準的な解決方法。型引数等に対するトレイト境界 (fn f<T: Clone>T: Clone など) により解決する。
  • DefaultImplCandidate … 既定実装 impl Send for .. {} により解決する。
  • ProjectionCandidate … 射影型のトレイト制約を、関連型に対するトレイト境界 (type Owned: Borrow<Self>; など) により解決する。または、匿名型 (impl Trait) のトレイト制約を、それ自身により解決する。
  • ObjectCandidate … トレイトオブジェクトのトレイト制約を、それ自身により解決する。
  • 特定のトレイトに対するビルトインの解決方法
    • BuiltinCandidateCopy/Sized 制約を組込みの方法により解決する。
    • BuiltinUnsizeCandidateUnsize 制約を組込みの方法により解決する。
    • BuiltinObjectCandidate … トレイトオブジェクトに追加で付与される Send/Sync 境界により解決する。
    • ClosureCandidate/FnPointerCandidate … 関数ポインタ型 fn()|args| { .. } によるクロージャ匿名型 [closure@..] に対する Fn/FnMut/FnOnce を組込みの方法により解決する。

ImplCandidateParamCandidate は、この時点では全く別物であることに注意する。例えば X: Add<Y> を解決する場合でも、

fn f(x: i32, y: i32) {
    let z = x + y;
}

であれば ImplCandidate が選択されるが、

fn f<X: Add<Y>, Y>(x: X, y: Y) {
    let z = x + y;
}

であれば ParamCandidate が選択される。

トレイト選択の流れ

トレイト選択は以下の手順で行われる。

基本的には、収集・選別をして候補が2つ以上になったら曖昧、候補が0個だったり承認されなかったら失敗、それ以外の場合は成功である。また、型環境への副作用は承認の段階で発生する。

実装候補の収集と承認

実装候補の収集

実装候補の収集は以下のように行われる。

  • 該当トレイトに対する既知のトレイト実装を列挙する。
  • 各トレイト実装の型引数/生存期間変数をフレッシュな型変数/生存期間変数で置き換える。
  • トレイト実装のヘッダとトレイト制約の単一化を試みる。
  • 単一化の成否にかかわらず、型環境を直前の状態にロールバックする。
  • 単一化が成功した実装を、候補として残す。

例えば、 Vec<Box<i32>>: MyTrait<Vec<Box<i8>>> というトレイト制約に対し、

  • impl<T: Copy, U: Copy> MyTrait<Vec<T>> for Vec<U> { .. } は候補である。 (単一化に成功するため。実装に含まれるトレイト境界は満たされていないが、これは収集には関係ない。)
  • impl<T> MyTrait<Vec<T>> for Vec<T> { .. } は候補ではない。 (単一化に失敗するため)

実装候補の承認

実装候補の承認は以下のように行われる。

  • 単一化を行う。
  • where 等で指定されたトレイト境界をトレイト制約として追加する。

型引数候補の収集と承認

型引数候補の収集

型引数候補の収集は以下のように行われる。

  • where 節等のトレイト境界を列挙する。
  • 各トレイト境界を、トレイト制約と単一化し、成功したら候補に加える。
  • 単一化の成否にかかわらず、元の状態にロールバックする。

型引数候補の承認

型引数候補の承認は以下のように行われる。

  • 該当トレイト境界を、トレイト制約と単一化する。

既定実装候補の収集と承認

既定実装候補の収集

既定実装は impl Send for .. {} のように .. という記号を使って与えられる特殊な実装で、主に Send, Sync, UnwindSafe, RefUnwindSafe が用いている。

既定実装候補の収集 は条件つきで実行される。つまり、他に候補がない場合にのみ既定実装候補がチェックされる。この規則があるため、収集と選別を区別して考える必要がある。

さて、他に候補がない場合の既定実装候補の収集は以下のように行われる。

  • トレイトオブジェクト型には既定実装は使われない。 (必要なら &(Foo + Sync) のようにしたり、trait Foo : Sync のようにしたりする)
  • 型引数には既定実装は使われない。 (必要なら fn f<T: Send> のように明示する)
  • 射影型には既定実装は使われない。 (必要なら trait Foo { type X : Send; } のように明示する)
  • それ以外の型に対しては既定実装候補を残す。特に、 impl Trait 型には既定実装が適用される。
  • 型変数の場合は、上記のどちらの場合に落ちるかわからないため、曖昧となる。

既定実装候補の承認

既定実装候補の承認は以下のように行われる。なお、既定実装をもつことができるトレイトは、 SendSync のように引数をとらないマーカートレイトに限られる。

  • Self 型の各メンバーに、再帰的にトレイト制約を追加する。

ただし、各メンバーとは以下のことを指す:

  • 整数・浮動小数点数boolcharstr・関数ポインタ・関数定義・!: なし
  • ポインタ・参照: 参照先の型
  • 配列・スライス: 要素型
  • PhantomData<T>: T
  • タプル・構造体・共用体・列挙型: 全てのメンバーの型
  • クロージャ: キャプチャーされた変数の型。ただし参照キャプチャーされた場合はその参照型。
  • impl Trait: 匿名化される前の型。

射影候補の収集と承認

射影候補の収集

射影候補の収集は以下のようにして行われる。

  • Self 型が射影型なら、関連型の境界を列挙する。
  • Self 型が impl Trait 型なら、 impl Trait 内の Trait を列挙する。
  • これらのそれぞれと単一化し、どれか一つでも成功したら候補として採用する。
  • 単一化の成否にかかわらず、型環境は元の状態にロールバックする。

現在の実装では、複数の候補があった場合、曖昧にはならず、最初のものが採用される。

射影候補の承認

射影候補の承認は以下のようにして行われる。

  • Self 型が射影型なら、関連型の境界を列挙する。
  • Self 型が impl Trait 型なら、 impl Trait 内の Trait を列挙する。
  • これらのそれぞれと単一化し、失敗したらロールバックする。成功したらロールバックせず終了する。

トレイトオブジェクト候補の収集と承認

トレイトオブジェクト候補の収集

トレイトオブジェクト候補の収集は以下のようにして行われる。

  • トレイト制約のトレイトがobject-safeでないなら何もしない。
  • トレイト制約の Self 型がトレイトオブジェクト型でないなら何もしない。 Self 型が型推論変数の場合は曖昧となる。
  • トレイトオブジェクト型に追加の Send/Sync 境界があり、これがトレイト制約と一致する場合は、組込みトレイトオブジェクト候補として採用し、終了する。
  • それ以外の場合は、トレイトオブジェクト型の主境界のスーパートレイト境界を(型引数込みで)推移的に求め、トレイト制約と単一化できるものを列挙する。(trait Foo: Bar<i32> + Bar<i8> のように、同じトレイトの複数のスーパートレイト境界を持つ可能性もある)
  • 残ったスーパートレイト境界がちょうど1個ならそれを候補にする。それ以外の場合は曖昧と報告する。

トレイトオブジェクト候補の承認

トレイトオブジェクト候補の承認は収集とほぼ同様だが、単一化に成功した場合はロールバックせずに型環境に対する変更をコミットする。

なお、組込みトレイトオブジェクト候補(追加の Send/Sync 境界に由来する候補) の承認は何もしない。

クロージャ候補/関数ポインタ候補の収集と承認

クロージャ候補/関数ポインタ候補の収集

クロージャ候補の収集関数ポインタ候補の収集は以下のようにして行われる。

  • トレイト制約のトレイトが Fn/FnMut/FnOnce のいずれかを調べる。どれでもなかったら何もしない。
  • Selfクロージャ型/関数定義/関数ポインタのいずれかを調べる。どれでもなかったら何もしない。未解決の型推論変数の場合は曖昧となる。
  • クロージャ型については、キャプチャー変数の利用状況を見て、 Fn/FnMut/FnOnceが実装できるかどうかを調べる。実装できなかったら何もしない。この時点でクロージャ種別が判明していない場合は曖昧となる。
  • 関数定義/関数ポインタについては、 unsafe でないこと、 extern "Rust"である(これは何も書いていないのと同義)こと、可変長引数部分をもたないこと(extern "Rust"であることからも従う)を確認する。どれか1つでも満たされていなかったら何もしない。
  • それ以外の場合は、これを候補にする。

クロージャ候補/関数ポインタ候補の承認

クロージャ候補の承認関数ポインタ候補の収集は以下のようにして行われる。

  • クロージャ/関数ポインタ型から、それが実装するクロージャトレイト参照を生成する。 (例: [closure@..] から Fn(u8) -> u32 / fn(&String) -> &str から FnMut(&String) -> &str)
  • 生成したクロージャトレイト参照を、トレイト制約と単一化する。
  • クロージャの場合、クロージャの種別ごとに生じる制約を追加する。

Copy/Sized の収集と承認

Copyimpl・関連型の境界・型引数の境界のほかに、以下の組込みの規則により候補が生成されることがある。

  • 整数・浮動小数点数boolchar・関数ポインタ・関数定義・生ポインタ・共有参照・!: 無条件で Copy である。
  • 配列: 要素型が Copy であるときに Copy である。
  • タプル: 全ての要素型が Copy であるときに Copy である。
  • トレイトオブジェクト・str・スライス・クロージャ型・可変参照: Copy ではない。他の候補の有無にかかわらず、このトレイト制約は解決されない。
  • 構造体・列挙型・共用体・射影型・型引数・impl Trait型: Copy とは限らないが、impl Copy for などの候補により Copy になるかもしれない。
  • 未解決の型推論変数の場合: 曖昧となる。

Sized にも同様の組込みの規則がある。

  • 整数・浮動小数点数boolchar・関数ポインタ・関数定義・生ポインタ・参照・配列・クロージャ型・!: 無条件で Sized である。
  • タプル: 最後の要素型が Sized であるときに Sized である。
  • トレイトオブジェクト・str・スライス: Sized ではない。他の候補の有無にかかわらず、このトレイト制約は解決されない。
  • 構造体・列挙型・共用体: 各バリアントの末尾の型が Sized であるとき、 Sized である。
  • 射影型・型引数・impl Trait型: Sized とは限らないが、T: Sized などの候補により Sized になるかもしれない。
  • 未解決の型推論変数の場合: 曖昧となる。

これらの条件が満たされたときに、 Copy/Sizedの組込みトレイト候補が生成される。候補の生成時点では、再帰的な制約は加味しない。

Copy/Sized の組込みトレイト候補の承認のタイミングで、これらの再帰的な制約がチェックされる。

Unsize の収集と承認

T: Unsize<U> は関連型の境界・型引数の境界のほかに、以下の規則により候補が生成される

  1. TU がどちらも同じトレイトのトレイトオブジェクト型で、追加の Send/Sync 境界について T のほうが U より多くを実装しているときは、 T: Unsize<U> の候補が生成される。
  2. T がトレイトオブジェクト型ではなく、 U がトレイトオブジェクト型のときは、 T: Unsize<U> の候補が生成される。
  3. U がトレイトオブジェクト型ではなく、 TU が未解決の型推論変数のときは、曖昧となる。
  4. T が配列で U がスライスのときは、 T: Unsize<U> の候補が生成される。
  5. TU がどちらも同じ構造体を指しているときは、 T: Unsize<U> の候補が生成される。
  6. TU が同じ長さのタプルのときは、 T: Unsize<U> の候補が生成される。

これらの候補は以下の規則により承認される

  1. TU の主境界は等しい。 TU より長く生存する。
  2. U のトレイトはobject-safeである。 TU の全てのトレイトを実装している。 TU の生存期間以上に生存する。
  3. TU の要素型が等しい。
  4. 構造体はフィールドを1個以上持っている。最後以外のフィールド達と、最後のフィールドは、型引数を共有していない。最後以外のフィールドは TU で等しい。最後のフィールドを T0, U0 とおくと、 T0: Unsize<U0> である。 (これらの比較時には射影型は解決せずにそのまま扱う)
  5. タプルは1要素以上である。最後以外のフォールドは TU で等しい。最後のフィールドを T0, U0 とおくと、 T0: Unsize<U0> である。

候補の選別

選別はさらに以下の処理に分けられる。

  • 候補がちょうど1個なら、選別処理をスキップする。
  • 各候補の承認処理をして、失敗したら候補から外す。承認の成否にかかわらず、型環境は元の状態にロールバックする。
  • それでも候補が複数残っている場合は、各候補の特殊化関係を調べ、特殊化関係にあれば負けたほうを外す。

特殊化関係はcandidate_should_be_dropped_in_favor_ofにある。これは以下の規則になっている。

  • 型引数候補は最も強い。異なる型引数候補同士は比べられない。
  • 射影候補(impl Traitを含む)とトレイトオブジェクト候補(組込みトレイトオブジェクト候補を含まない)は、その次に強い。
  • それ以外の候補は上記よりも弱く、お互いには比べられない。ただし、以下の例外がある。
    • 2つの実装候補について、実装が特殊化関係にある場合は、より特殊なほうがより強い。
    • 2つの実装候補について、 #![feature(overlapping_marker_traits)] で、両方の実装がアイテムを持たず、極性も等しい場合には、同じ強さである。

同じ強さである場合も含めて、より弱いものから順番に脱落させていく。これにより最強の候補が1つだけ残ったら選別は成功となる。

トレイト選択では未解決の型推論変数が含まれていることがあるため、コヒーレンスに関係なく、複数の候補が残る場合があり、その場合はエラーになる。逆に、トレイト選択では基本的に現在見えている実装から探索するので、「これしか該当する実装がないから」という理由により型推論が進むことがある。以下はその例である。

use std::ops::Add;
fn f<T: Add<i32>>(t: T) {
    t + Default::default(); // OK
}
fn g<T: Add<i32> + Add<i8>>(t: T) {
    t + Default::default(); // Error
}
fn main() {
    
}

より奇妙な例としては、以下のようなものも考えられる。

fn f<T>() {
    let x : (_, T); // Error
}
fn g<T: ?Sized>() where (i32, T): Sized {
    let x : (_, T); // OK
}
fn main() {
    
}

収集時には曖昧でも、選別によって曖昧性がなくなる例としては、例えば以下のようなものが考えられる。

trait Foo<Y> {
    fn y() -> Y;
}
trait Bar {}
trait Baz {}

impl<T: Bar> Foo<i32> for T {
    fn y() -> i32 { 0 }
}
impl<T: Baz> Foo<i8> for T {
    fn y() -> i8 { 0 }
}

impl Bar for i32 {}
impl Baz for i64 {}

fn f() {
    let _ = <i32 as Foo<_>>::y();
}

fn main() {
}

否定実装の処理

既定実装と異なり、否定実装は収集段階では通常の実装と同様に扱われている。否定実装の特徴は、この実装が選別された場合にはコンパイルエラーとなるという点である。

自己再帰的な制約の処理

自己再帰的なデータ構造の Send の処理などで、自己再帰的な制約が出てくることがある。これは evaluate_stackの一部分で処理されている。以下のように実装されている。

  • トレイト制約の処理中は、再帰的に処理中のトレイト制約をスタックに積む。
  • 現在のトレイト制約が、再帰的に処理中のトレイト制約と等しいときは、収集処理をせずにすぐにOKを返す。

まとめ

トレイト選択の詳細について説明した。ただし、より一般の制約のfulfillmentや、coherence checkerなどについては言及していない。

Rustにおける左辺値選好と可変性調停

概要: Rustのミュータビリティー推論に使われる左辺値選好と可変性調停について説明する。

左辺値選好

Rustの型推論では、期待型のほかに、左辺値選好 (lvalue preference) という状態もトップダウンに渡される。

LvaluePreferenceは以下のどちらかの値をとる。

  • PreferMutLvalue … 可変な左辺値を優先する。
  • NoPreference … 選好はない。不変な左辺値や、右辺値などでよい。

左辺値選好はほぼ構文的に決定される。具体的には以下のように決定・伝搬される。

  • &mut の内側の式は PreferMutLvalue
  • 代入の左辺は PreferMutLvalue
  • メソッド呼び出しとフィールド参照のレシーバーは左辺値選好を継承する。
  • x[i] の左辺は左辺値選好を継承する。
  • 参照外し演算子は左辺値選好を継承する。

左辺値選好は次の用途に用いられる

  • x[i] は原則として Index に脱糖されるが、PreferMutLvalue ならば先に IndexMut を試す。
  • *x は原則として Deref に脱糖されるが、 PreferMutLvalue ならば先に DerefMut を試す。
    • メソッド以外の自動参照外しに対しても同じ処理が行われる。
    • メソッドのレシーバの自動参照外しは NoPreference が仮定される。 (後述)

左辺値選好が誤推論するケース

さて、この左辺値選好は多くの場合に正しく可変性を推論するが、誤った推論をするケースも考えられる。以下がこの左辺値選好の仮定である。

  • x[i] の左辺は可変性を継承する→正しい。 (Index/IndexMut の型がそうなっているので)
  • *x は可変性を継承する→正しい。 (Deref/DerefMut の型がそうなっている。また、 &T/&mut T/Box<T> の参照外しの動作も同様である。)
  • フィールド参照は可変性を継承する→正しい。
  • メソッド呼び出しは可変性を継承する→正しくない。 &mut self を受け取り &T を返したり、 &self を受け取り &mut T を返したりする可能性がある。

これにより、メソッド呼び出しのレシーバの位置に DerefIndex が来ると、可変性の推論で誤推論を起こす可能性がある。これを実験すると以下のようになる。

use std::fmt::Debug;
use std::ops::{Deref, DerefMut};


// deref/deref_mut 時にメッセージを出すラッパー
#[derive(Copy, Clone, Debug)]
pub struct Wrap<X: Debug + ?Sized>(X);

impl<X: Debug + ?Sized> Deref for Wrap<X> {
    type Target = X;
    fn deref(&self) -> &Self::Target {
        println!("deref({:?})", self);
        &self.0
    }
}

impl<X: Debug + ?Sized> DerefMut for Wrap<X> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        println!("deref_mut({:?})", self);
        &mut self.0
    }
}


// 左辺値選好による推論が失敗する例
#[derive(Copy, Clone, Debug)]
pub struct A;

impl A {
    pub fn f<'a, 'b>(&'a self, p: &'b mut i32) -> &'b mut i32 {
        p
    }
    pub fn g<'a, 'b>(&'a mut self, p: &'b i32) -> &'b i32 {
        p
    }
}

fn main() {
    let mut x = Wrap(A);
    let mut y : i32 = 0;
    
    // レシーバが&Tでかまわないが、構造上&mut Tが推論される例
    &mut *(*x).f(&mut y); // deref_mut(Wrap(A));
    &mut *x.f(&mut y); // deref(Wrap(A));
    // レシーバが&mut Tを必要とするが、構造上&Tが推論される例
    &*x.g(&y); // deref_mut(Wrap(A));
}

これを見ると、一番最初の例だけ、 Deref::deref でよいはずの位置で DerefMut::deref_mut が呼ばれているが、他の例では問題なく動作している。

これは次のような理由による。

  • この例には書いていないが、&T で十分だが &mut T が推論され、しかし実際には DerefMut が実装されていない場合には、 Deref にフォールバックされるため問題ない。
  • メソッドのレシーバの自動参照外しでははじめ DerefMut ではなく Deref が仮定される。
  • メソッド解決後に発生する可変性調停のため、推論と異なり &mut T が必要な場合は、そのように修正される。

可変性調停

可変性調停 (reconciliation) はメソッドの解決後に行われる処理で、レシーバが &mut self だった場合に Deref/Index を再解決する処理である。

これは次のような手順を踏む。

  • レシーバを指定している式を調べ、影響を受ける Deref/Index 呼び出しを列挙する。 (例えば x[i][j].some_method()some_method のレシーバが &mut self の場合、 x[i]x[i][j] を補正する必要がある。
  • それぞれの Deref/IndexPreferMutLvalue で再推論する。これによりこの部分が DerefMut/IndexMut に昇格する。

これにより、&mut T が要求される部分で &T を誤推論したことにより発生しえたエラーを回避している。

まとめ

  • *xx[i] などではイミュータブルな Deref/Index とミュータブルな DerefMut/IndexMut のいずれかが選択される。
  • これらは色々な仕組みによりだいたいいい感じに推論される。それには以下のような動作が関わってくる。
    • 構文的に決まる「左辺値選好」をトップダウンに伝搬する。
    • DerefMut が駄目なら Deref にフォールバックする。
    • メソッド解決でははじめイミュータブルを仮定し、 &mut self が要求されていたらミュータブルとして再推論する。

Rustのバイト列リテラルの型

Rustのリテラルの型検査は check_lit にある。これによると、文字列リテラルの型は &'static str である一方、バイト列リテラルの型は &'static [u8] ではなく、 &'static [u8; N] である。

そのため、以下のようなコードがコンパイルできる。

fn main() {
    let x : &[u8; 5] = b"Hello";
    let y : [u8; 5] = *b"Hello";
}

なお、 b"Hello" は、 &[72, 101, 108, 108, 111] と等価ではない。数値リテラルu8 とは限らないという点のほかに、生存期間が異なる。

fn main() {
    let x : &'static [u8] = b"Hello";
    let y : &'static [u8] = &[72, 101, 108, 108, 111];
}
rustc 1.20.0-nightly (3bfc18a96 2017-06-29)
error[E0597]: borrowed value does not live long enough
 --> <anon>:3:30
  |
3 |     let y : &'static [u8] = &[72, 101, 108, 108, 111];
  |                              ^^^^^^^^^^^^^^^^^^^^^^^^ does not live long enough
4 | }
  | - temporary value only lives until here
  |
  = note: borrowed value must be valid for the static lifetime...

error: aborting due to previous error(s)

生存期間を考慮すると、 b"Hello" はおよそ以下のように脱糖されると考えられる。

fn main() {
    let x : &'static [u8] = b"Hello";
    let y : &'static [u8] =
        { static S : [u8; 5] = [72, 101, 108, 108, 111]; &S };
}

Rust unsize期待型とキャスト期待型

Rustの期待型は以下のようなデータ構造になっている。

#[derive(Copy, Clone, Debug)]
pub enum Expectation<'tcx> {
    NoExpectation,
    ExpectHasType(Ty<'tcx>),
    ExpectCastableToType(Ty<'tcx>),
    ExpectRvalueLikeUnsized(Ty<'tcx>),
}

このように4つのコンストラクタを持つ Expectation だが、最も大きな働きをしているのは NoExpectationExpectHasType であり、ほぼ Option<Ty> と思って問題ない。

この記事ではまず、残りの2つの動作について把握する。これは ExpectHasType よりも弱い期待をするもので、ごく限定された場面でのみ生成される。

unsize期待型

ExpectRvalueLikeUnsized(U) は、その式の型が T: Unsize<U> であるような T であることを期待するものである。

この期待型は rvalue_hint によってのみ生成される。この関数は、

  • [T], str, Trait 型に対しては ExpectRvalueLikeUnsized
  • それ以外の型に対しては ExpectHasType

を期待する。この処理は以下の部分で行われる。

  • 関数呼び出し引数の推論された期待型を推論するとき。
  • 関数呼び出し引数を型強制するとき。 ([T], str, Trait に対しては型強制は実行されない)
  • box x 式の期待型が Box<T> だったとき。
  • &x/&mut x 式の x が左辺値で、期待型が &'a T/&'a mut T だったとき

これらに共通するのは、これらが全て Sized な式を期待しているという点である。 (&xx が左辺値なら x!Sized かもしれない)

ExpectRvalueLikeUnsizedto_option によって取り出される。これを調べると、 ExpectRvalueLikeUnsized は以下の用途にしか使われていないことがわかる。

  • 配列リテラルの要素に対して型強制を行う。 ([a, b, c]: ExpectRvalueLikeUnsized([T]) なら aT に型強制される)
    • もちろん、 ExpectRvalueLikeUnsized 以外の期待型についてもこれは行われる。

例えば以下の例では、配列の要素に対して型強制が行われている。

fn main() {
    let x : Box<[*const i32]> = Box::new([&1]);
}

キャスト期待型

ExpectCastableToType(T) は、その式の型が x as T で変換可能であることを期待するものである。

これは as 式の型検査に対してのみ生成される。逆に、これが利用されるのは以下の場面のみである。

前者については、以下の例を見るとわかる。

fn main() {
    println!("{}", 2000000000000 as i64); // 2000000000000
    println!("{}", (1000000000000 + 1000000000000) as i64); // -1454759936
}

これは以前の記事で紹介した仕組みだけでは説明できない。以前の記事で紹介した仕組みにより、 1000000000000 + 1000000000000 はこの左辺・右辺と同じ型をもつことが仮定される。そのため上の 2000000000000i64 になるなら原則として 1000000000000i64 になるはずだが、そうなっていない。

これは、ここで 2000000000000i64 になる仕組みが、型推論ではなく、期待型により実現されているからである。ここで説明したように x as i64 は内側の x に対して ExpectCastableToType(i64) を生成する。これを受けた整数リテラルは、サフィックスを持たないリテラルの型を i64 とおく。そして ExpectCastableToType演算子の内側には伝搬しないため、 1000000000000i64 とはならず、したがってデフォルトの i32 になってしまう。

後者には以下のような奇妙な例がある。

fn main() {
    let x = [&1] as [*const i32; 1];
}

これはコードの形に反して、 [&i32; 1] から [*const i32; 1] へのキャストではなく、 &i32 から *const i32 への型強制が行われている。

また、 ExpectCastableToTypeif 式の内側に伝搬しない という特徴がある。理由はその部分のコメントにあるように、then節側で厳しすぎる推論をしてしまうことを防止するためである。

Rust 関数呼び出しの期待型の伝搬

以前の記事で説明したように、Rustでは型強制や整数リテラルなどの特殊な型推論をシームレスに行うために期待型という概念を導入している。

この期待型はトップダウンに伝搬されるが、先ほどの記事で挙げた例

fn main() {
    let x : Box<[_]> = Box::new([1, 2, 3]);
    //                 ^^^^^^^^^^^^^^^^^^^ coercion site
    let y : Box<Box<[_]>> = Box::new(Box::new([1, 2, 3]));
    //                               ^^^^^^^^^^^^^^^^^^^ coercion site
}

では Box::new 関数の外側から内側に向けて期待型の伝搬が行われている。

これは Box::new に限ったことではなく、一般にどのような関数でもこのような伝搬が行われる。この計算をしているのが expected_inputs_for_expected_outputs メソッドである。

これは大まかに言うと次のようなことをしている。

  1. 関数の引数型と戻り値型を生成する。ジェネリクスにより未確定の部分には型推論変数を入れる。例えば、 Box::new の場合、引数型は ?T で戻り値型は Box<?T> である。
  2. 型推論環境のスナップショットを生成する。
  3. 関数の戻り値型と、関数呼び出しの期待型で、単一化を行う。例えば期待型が Box<Box<[?X]>> なら、単一化により ?T := Box<[?X]> が生成される。
  4. 引数型の型推論変数を展開する。今回は ?T := Box<[?X]> が判明しているため、引数型は ?T から Box<[?X]> にリファインされる。
  5. 展開結果を、引数の期待型として覚えておく。
  6. 型推論環境のスナップショットをロールバックする。

期待型はあくまでヒントであるため、期待型とは異なる型が推論されることはある。例えば、上の例ではまず Box::new([1, 2, 3])Box<[?X]> が期待されているという情報から [1, 2, 3][?X] が期待されているという情報が生成される。このような型強制はできないため失敗し、 Box::new([1, 2, 3]) の型は Box<[{integer}; 3]> になる。そこで翻ってこの期待型 Box<[?X]> と比較すると、この時点で型強制が可能であるため、強制後の型は Box<[{integer}]> となる。

このとき、期待型の伝搬のためにまず ?T := [?X] がユニフィケーションで生成されるが、これはロールバックにより消滅する。中の型が確定してからあらためてユニフィケーションが行われ、このときは ?T := [{integer}; 3] となる。

このように期待型の伝搬の過程では誤った単一化子が生成されることがあるが、期待型の処理ごとにロールバックすることで競合を回避している。

Rustにおける演算子の型推論の特殊ルール

概要: 原則として x + y::std::ops::Add(x, y) の構文糖衣であるが、型推論で特別扱いされる。

演算子の脱糖

演算子の脱糖は型推論の後、HIRからMIRへの変換のタイミングで行われる。原則として x + y::std::ops::Add(x, y) の構文糖衣である。これは *x/x[i] 以外の他の演算子についても同様である。しかし型推論では以下のような特殊扱いがある。

fn main() {
    use std::ops::Add;
    let x : i8 = 1 + 1; // OK
    let x : i8 = Add::add(1, 1); // Error
}

下側のコードがエラーになる理由

Rustの演算子オーバーロード可能であり、型の制約が少ない。具体的には「左辺と右辺の型から、計算結果の型が一意に決まる」ということだけが要請される。そのため型 A と型 B の値を足して型 C の値ができることがありえる。例えば、以下のような足し算を定義することができる。

fn main() {
    use std::ops::Add;
    struct A;
    struct B;
    impl Add<B> for A {
        type Output = i8;
        fn add(self, other: B) -> Self::Output { 42 }
    }
    let x : i8 = 1i8 + 1i8; // OK
    let x : i8 = A + B; // OK
}

つまり、外側の型 i8 から、内側の型を決めることは基本的にできない。演算子に限らず、戻り値型が <A as Add<B>>::Output のような射影型になっているときは、型はボトムアップにしか伝搬しない。

つまり、 Add::add(1, 1) : i8 から 1 : i8 は推論されない。この型情報の不足から、 1: {integer} だけが判明する。これは後でデフォルトである i32 と推論されるが、すると Add::add(1i32, 1i32) : i32 であり型が一致しない。

上側のコードがエラーにならない理由

lhs + rhs で、 lhs, rhs がある特定の型の場合に、Rustは「lhs, rhs, lhs + rhs の型は全て同じ」という制約を追加する。これにより自動的に内側の 1i8 であることが判明する。

演算子ごとの動作の違い

二項演算子は以下の4種類に分類される。それぞれ、特定の条件下型に対して追加の仮定をおく

  • 短絡回路演算子: ||, &&
    • 常に、左辺/右辺/戻り値は bool である。
  • シフト演算子: <<, >>
    • 両辺がともに整数型であるとき、左辺の型と戻り値の型は等しい。
  • 数学演算子: +, -, *, /, %
    • 両辺がともに整数型であるか、両辺がともに浮動小数点数型であるとき、左辺の型と右辺の型と戻り値の型は等しい。
  • ビット演算子: |, &, ^
    • 両辺がともに整数型であるか、両辺がともに浮動小数点数型であるか、両辺がともに bool であるとき、左辺の型と右辺の型と戻り値の型は等しい。
  • 比較演算子: ==, !=, <, <=, >, >=
    • 両辺がともにスカラー型であるとき、左辺の型と右辺の型は等しく、戻り値の型は bool に等しい。

ただし、スカラー型とは、 bool/char/整数型/浮動小数点数型/関数定義/関数ポインタ/生ポインタ のいずれかである。

「整数型である」「浮動小数点数型である」というのは、 i32usize のような特定の型のほかに、 {integer} のような推論型も含む。

単項演算子の場合

3つある単項演算子 */!/- のうち、 !-似たような動作をする。ただし以下の違いがある:

  • これらの演算子に遭遇したタイミングで structurally_resolved_type が呼ばれる。そのため、被演算子の型がこの時点で全く判明していない場合は、エラーになる。

! は、被演算子が整数または bool のとき、 - は、被演算子が整数または浮動小数点数のときに、組込み演算子とみなされる。このとき戻り値の型は被演算子の型と等しいと仮定される。

まとめ

  • x + y はほぼ ::std::ops::Add(x, y) の構文糖衣であるが、型推論の動作が微妙に異なる。
  • xy が特定の型のときは、型推論の時点で「組込み演算子である」と判定され、トップダウン型推論が可能になる。