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 が特定の型のときは、型推論の時点で「組込み演算子である」と判定され、トップダウン型推論が可能になる。

Rust パターンマッチの変数束縛とコンストラクタ/定数の区別

パターンマッチを持つ言語では、変数束縛とコンストラクタ/定数が構文上曖昧である場合がある。Rustでは以下の規則に従っている。

また、上記の条件で変数束縛とみなされたが、アイテム名と一致するときはエラーとなる。

例えば以下のようになる。

struct A;
fn main() {
    // let A = A; // Error
    let self::A = A; // OK
    match A { A => {}}; // OK
    // match A { ref A => {}}; // Error
    // match A { A @ A => {}}; // Error
}

Rustで use std; が必要なときとエラーになるときがあるのは何故か

Rustでは use std;use rand; のようなインポートが必要な場合と、逆に書くとエラーになる場合がある。

簡単に言うと

  • トップレベルモジュールでは、書くとエラーになる(既にある名前と衝突する)。それ以外の場所では、必要な場合がある。
  • これは名前をローカルで使うための仕組みと、名前の別名を公開するための仕組みが同じ use により実現されていることと関係している。

そもそも use std; は何故必要なのか

例えば、あるモジュール m1.rs 内で以下のようなコードを書くとエラーになる。

fn foo() {
    let stderr = std::io::stderr();
}
rustc 1.18.0 (03fc9d622 2017-06-06)
error[E0433]: failed to resolve. Use of undeclared type or module `std`
 --> m1.rs:2:18
  |
2 |     let stderr = std::io::stderr();
  |                  ^^^^^^^^^^^^^^^ Use of undeclared type or module `std`

error: aborting due to previous error

呼ぼうとしているのは確かに std::io::stderr なのにこれが発見されないのは何故か。これは、この手のパスが文脈によって異なる解釈をされることに由来する。そもそもパス(::で区切られているやつ)は次の3種類に分けられる。

  • :: または $crate:: で始まるもの (例: ::std::default::Default)
  • self または super で始まるもの (例: self::MyTrait) (※単独の self は例外)
  • それ以外
    • use path;pub(in path) では、絶対パスとして解釈される。
    • それ以外の文脈では、レキシカルスコープのパス (相対パスの亜種) として解釈される。

つまり、 std::io::stderr はここでは絶対パスではなく、相対パスのようなものとして扱われてしまっているのである。

絶対パスとして修正する場合

先ほどのコードで意図しているのは絶対パスだと考えられる。したがって次のようにすればエラーにはならない。

fn foo() {
    let stderr = ::std::io::stderr();
}

レキシカルスコープのパスとして修正する場合

もう一つの解釈として、レキシカルスコープのパスとしての使用を意図していた可能性も考えられる。例えば、以下のコードは正しい。

use std::io;
fn foo() {
    let stderr = io::stderr();
}

同じ要領で、 std をインポートすればよいという考えがありえる。これも正しい。

use std;
fn foo() {
    let stderr = std::io::stderr();
}

トップレベルモジュールでは不要なのはなぜか

トップレベルモジュールでは通常、 extern crate が多く行われている。例えば、 extern crate rand; と書いておくと、 rand のトップレベルモジュールが ::rand にリンクされる。

ここでもし m1 モジュール内で use rand; をすると、これがさらに ::m1::rand にリンクされることになる。これにより m1 の直下で rand を簡単に参照できるようになる。

同じ理屈で、トップレベルモジュールで use rand; をすると、 ::rand::rand にリンクしようとしていることになる。これはおかしいのでエラーになってしまうという寸法である。

こういった理屈のため、 use nalgebra as na; のような別名インポートはトップレベルモジュールでも必要である。例えば、

extern crate nalgebra;
use nalgebra as na;

..

mod m1 {
    use nalgebra as na;
}

のようになる。もちろん、以下のようにそもそも extern crate の時点で別名をつけてリンクすることもできる。(好ましい習慣かは別として)

extern crate nalgebra as na;
// use 不要

mod m1 {
    use na;
}

Rustの use の設計について

Rustの use は2つの目的を兼ねている。これは例えばこのNiko Matsakis氏のコメントでも確認できる。

  • ある定義(関数や型など)に別名をつける(別名をつけて公開する)こと。 (再エクスポート)
  • ある定義をローカルから使うためにスコープに入れること。 (インポート)

前者はモジュールグラフの切り貼り自体を目的にしているのに対して、後者は let などと同様にレキシカルスコープへの名前の導入ができればよく、グラフの切り貼りは意図していない。しかしRustでは両者を同じように、グラフの切り貼りで実現する仕組みになっている。このことは、 use の動作を理解するにあたって押さえておくとよいところかもしれない。

まとめ

  • トップレベルモジュールでは、 use std; を書くとエラーになる(既にある名前と衝突する)。それ以外の場所では、 use std; が必要な場合がある。
  • これは名前をローカルで使うための仕組みと、名前の別名を公開するための仕組みが同じ use により実現されていることと関係している。

Rustでunsafeが必要な操作

Rustでは unsafe の表明を行わない限り未定義動作が発生しない。コンパイラや言語仕様のミスにより未定義動作が発生しうる場合には優先的に修正される。(なお、整数オーバーフローのように特定条件下でエラーになるものであっても、正しくunwind/abortできるものは未定義動作とは呼ばない。)

unsafe の表明

unsafe の表明は、構文的には以下のいずれかである。

  • unsafe ブロック
  • unsafe impl

unsafe を表明した場合、その部分では「安全かどうかをコンパイラが保証できない操作」が行えるため、この部分が安全であるかどうかを検査するのはプログラマの責任であるということになる。それぞれの操作には、それが安全に実行できるための事前条件(safety precondition)が決められているから、これが成り立っていることをプログラマが自分で保証する、という算段である。

unsafe の要求

unsafe を用いたライブラリ関数の場合、「関数が何らかの性質を満たしているときは安全」という状況が考えられる。例えば、比較関数が正当であることを前提としたソートアルゴリズムの実装というのが考えられる。

Rustでは原則としてこれは許されない。ライブラリの呼び出し側が (unsafe だったり、可視性を破壊したりしていない範囲内で) どんな異常な引数で関数を呼んでも、その関数は安全に動作しないといけない。

どうしても呼び出し側に安全性の責任を転嫁したい場合は、関数シグネチャunsafe をつける。これにより、そのライブラリ関数は、安全に使うための追加の事前条件(safety precondition)を要求していることになる。その事前条件はドキュメントに説明されるべきということになる。

このように unsafe を要求する構文には以下のものがある。

なお、関数定義の unsafe は要求と表明を兼ねている。

unsafe ブロック/関数内でないとできない操作の一覧

unsafeが適切に呼ばれているかどうかは rustc::middle::effectモジュール で検査されている。

以下では事前条件も書いてみるが、すべて筆者による推測である。

unsafe 関数/unsafe メソッドの呼び出し

unsafe のついている関数やメソッドを呼び出したときに発生する。関数を取り出して呼び出していない場合は発生しない。

事前条件: その関数/メソッドによって指定されている事前条件を守る。

生ポインタの参照外し

*const T*mut T 型の値 p に対し、 *p を行う。 &*p のように左辺値の場合 (生ポインタを参照に昇格するのに使う) にも unsafe が必要である。

事前条件・不変条件: おそらく以下のような条件が必要とされている。

  • アラインメントが揃っている。
  • 有効な場所を指している。
  • 有効な値が格納されている。または、昇格した参照が使用されるまでに有効な値が格納される。
  • エイリアスを持たないか、これ自身を含む全てのエイリアスが読み取り専用として扱われている。

インラインアセンブリ

asm!()global_asm!() によるインラインアセンブリの埋め込みは常に unsafe である。

事前条件・不変条件: Rustのもつ全ての不変条件を守ること。例えば、書き込み借用できるメモリ以外に書き込まない。不正な値を書き込まない。 Copy でない値をコピーした場合に元の位置のデストラクタを呼んではいけない。など。

static mut 変数へのアクセス

static mut で宣言された静的変数の読み取り/書き込みアクセスは unsafe である。

事前条件・不変条件: 書き込み参照するときは、他に誰かが参照していないこと。読み取り参照するときは、他に誰かが書き込み参照していないこと。(他スレッドからのアクセスも含む)

extern { static } 変数へのアクセス

extern { static X : u32; } のように、FFIで外部の静的変数を参照する変数への読み取り/書き込みアクセスは unsafe である。

事前条件: 値を取り出す場合は、不正な値が入っていないよう注意する。

なお、この条件は互換性のために現在は警告扱いになっている(warning cycle)。将来はエラーとなる予定である。

union 要素へのアクセス

union 要素へのアクセス (読み取り、代入、パターンマッチによる読み取り) は unsafe である。

事前条件・不変条件:

  • 読み取りでは、そのフィールドに有効な値が入っていること。
  • 書き込みでは、この union の何らかの不変条件を保ち、結果的に Drop が正常に動作すること。 (Drop を実装していなければ問題ない)

なお、最近の変更により、 Copyunion の要素への代入は unsafe ではなくなった。