MIR(Mid-level IR)
MIR は Rust の Mid-level Intermediate Representation です。これは HIR から構築されます。MIR は RFC 1211 で導入されました。 これは Rust を根本的に単純化した形式であり、特定のフロー依存の安全性チェック (特に借用チェッカー!)に使われるほか、最適化やコード生成にも使われます。
MIR の非常に高レベルな導入、および MIR が依存しているコンパイラの概念 (制御フローグラフやデシュガリングなど)について知りたい場合は、 MIR を紹介した rust-lang のブログ記事を読むとよいでしょう。
MIR の概要
MIR は [compiler/rustc_middle/src/mir/][mir] モジュールで定義されていますが、
それを操作するコードの多くは [compiler/rustc_mir_build][mirmanip_build]、
[compiler/rustc_mir_transform][mirmanip_transform]、および
[compiler/rustc_mir_dataflow][mirmanip_dataflow] にあります。
MIR の主な特徴には、次のようなものがあります。
- 制御フローグラフに基づいている。
- ネストした式を持たない。
- MIR 内のすべての型は完全に明示的である。
MIR の主要な語彙
このセクションでは、MIR の主要な概念を紹介します。要約すると次のとおりです。
- 基本ブロック: 制御フローグラフの単位で、次のもので構成されます。
- 文: 後続が 1 つのアクション
- ターミネータ: 複数の後続を持つ可能性があるアクション。常に ブロックの末尾にある
- (基本ブロック という用語に馴染みがない場合は、背景の章を参照してください)
- ローカル: スタック上に(少なくとも概念的には)割り当てられるメモリ位置。
関数引数、ローカル変数、一時値などがあります。これらはインデックスで識別され、
_1のように先頭にアンダースコアを付けて表記されます。また、戻り値を格納するために 割り当てられる特別な「ローカル」(_0)もあります。 - プレース:
_1や_1.fのように、メモリ内の位置を識別する式。 - Rvalue: 値を生成する式。「R」は、これらが代入の「右辺」
であることを表しています。
- オペランド: rvalue への引数で、
22のような定数、または_1のようなプレースのいずれかです。
- オペランド: rvalue への引数で、
単純なプログラムを MIR に変換し、整形表示された出力を読むことで、MIR がどのように 構築されるかを感覚的に理解できます。実際、playground では MIR ボタンが用意されており、 プログラムの MIR を表示できるため、これを簡単に試せます。このプログラムを play に入力する (またはこのリンクをクリックする)し、上部の「MIR」ボタンをクリックしてみてください。
fn main() {
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
}
次のようなものが表示されるはずです。
// 警告: この出力形式は人間の利用者のみを対象としています
// また、予告なく変更される可能性があります。自由に試してみてください。
fn main() -> () {
...
}
これは main 関数の MIR 形式です。
上記リンクで表示される MIR は最適化されています。
最適化では、StorageLive のような一部の文が削除されます。
これは、その値がコード内で一度もアクセスされないことをコンパイラが検出するために起こります。
最適化されていない MIR を表示するには、rustc [filename].rs -Z mir-opt-level=0 --emit mir を使用できます。
これには nightly ツールチェーンが必要です。
変数宣言。 少し詳しく見ていくと、多数の変数宣言から始まっていることがわかります。 それらは次のような形をしています。
let mut _0: (); // 戻り値のプレース
let mut _1: std::vec::Vec<i32>; // src/main.rs:2:9: 2:16 のスコープ 0 内
let mut _2: ();
let mut _3: &mut std::vec::Vec<i32>;
let mut _4: ();
let mut _5: &mut std::vec::Vec<i32>;
MIR の変数には名前がなく、_0 や _1 のようなインデックスを持っていることがわかります。
また、ユーザーの変数(例: _1)と一時値(例: _2 や _3)が混在しています。
ユーザー定義変数は、それらに debuginfo が関連付けられているため区別できます(下記参照)。
ユーザー変数の debuginfo。 変数宣言の下には、_1 がユーザー変数を表していることを示す
唯一の手がかりがあります。
scope 1 {
debug vec => _1; // src/main.rs:2:9: 2:16 のスコープ 1 内
}
各 debug <Name> => <Place>; アノテーションは、名前付きのユーザー変数と、
デバッガーがその変数のデータを見つけられる場所(つまりプレース)を記述します。
ここでの対応関係は単純ですが、最適化によってプレースが複雑になったり、
複数のユーザー変数が同じプレースを共有するようになったりすることがあります。
さらに、クロージャのキャプチャも同じ仕組みで記述されるため、最適化がなくても
複雑になります。例: debug x => (*((*_1).0: &T));。
「scope」ブロック(例: scope 1 { .. })は、ソースプログラムの字句構造
(どの名前がいつスコープ内にあったか)を記述します。そのため、たとえばデバッガーで
コードをステップ実行している場合、// in scope 0 と注釈付けされたプログラムのどの部分にも
vec は存在しないことになります。
基本ブロック。 さらに読み進めると、最初の 基本ブロック が見えます (当然、実際に表示すると少し異なる場合があります。また、ここでは一部のコメントを無視しています)。
bb0: {
StorageLive(_1);
_1 = const <std::vec::Vec<T>>::new() -> bb2;
}
基本ブロックは、一連の 文 と最後の ターミネータ によって定義されます。 この場合、文は 1 つです。
StorageLive(_1);
この文は、変数 _1 が「live」であることを示します。つまり、後で使われる可能性があるということです。
これは、変数 _1 の使用が終わったことを示す StorageDead(_1) 文に遭遇するまで継続します。
これらの「ストレージ文」は、LLVM によってスタック領域を割り当てるために使われます。
ブロック bb0 の ターミネータ は、Vec::new への呼び出しです。
_1 = const <std::vec::Vec<T>>::new() -> bb2;
ターミネータは、複数の後続を持つことができるため、文とは異なります。つまり、制御が異なる場所へ
流れる可能性があるということです。Vec::new への呼び出しのような関数呼び出しは、
巻き戻しの可能性があるため常にターミネータです。ただし、Vec::new の場合は実際には巻き戻しが
不可能であることがわかるため、後続ブロックとして bb2 だけを列挙しています。
先に進んで bb2 を見ると、次のようになっています。
bb2: {
StorageLive(_3);
_3 = &mut _1;
_2 = const <std::vec::Vec<T>>::push(move _3, const 1i32) -> [return: bb3, unwind: bb4];
}
ここには 2 つの文があります。_3 一時値を導入する別の StorageLive と、
次の代入です。
_3 = &mut _1;
一般に、代入は次の形式をとります。
<Place> = <Rvalue>
プレースとは、_3、_3.f、*_3 のような式で、メモリ内の位置を表します。
Rvalue は値を作成する式です。この場合、rvalue は可変借用式であり、
&mut <Place> のような形をしています。そのため、rvalue の文法はおおよそ次のように定義できます。
<Rvalue> = & (mut)? <Place>
| <Operand> + <Operand>
| <Operand> - <Operand>
| ...
<Operand> = Constant
| copy Place
| move Place
この文法からわかるように、右辺値をネストすることはできません。右辺値は場所と定数のみを参照できます。さらに、場所を使う場合には、その場所をコピーしているのか(そのためには場所の型が T であり、T: Copy である必要があります)、それともムーブしているのか(これは任意の型の場所に対して機能します)を示します。したがって、たとえば Rust に x = a + b + c という式があった場合、それは 2 つの文と一時変数にコンパイルされます。
TMP1 = a + b
x = TMP1 + c
(試して確認してみてください。ただし、オーバーフローチェックを省くために release モードにした方がよいかもしれません。)
MIR のデータ型
MIR のデータ型は、[compiler/rustc_middle/src/mir/][mir]
モジュールで定義されています。前のセクションで述べた主要な概念はそれぞれ、かなり直接的な形で Rust の型に対応します。
主要な MIR データ型は [Body] です。これは単一の関数のデータを含みます(「昇格された定数」のための Mir のサブインスタンスも含みますが、それらについては以下で読むことができます)。
- 基本ブロック: 基本ブロックはフィールド
[
Body::basic_blocks][basicblocks] に格納されます。これは [BasicBlockData] 構造体のベクターです。基本ブロックを直接参照する人はいません。代わりに、[BasicBlock] 値を受け渡しします。これは、このベクターへのインデックスを [newtype 化した][newtype’d]ものです。 - 文は型 [
Statement] で表されます。 - 終端子は [
Terminator] で表されます。 - ローカルは [newtype 化された][newtype’d]インデックス型 [
Local] で表されます。 ローカル変数のデータは [Body::local_decls][localdecls] ベクターにあります。また、戻り値を表す特殊な「ローカル」を識別するための特別な定数 [RETURN_PLACE] もあります。 - 場所は構造体 [
Place] によって識別されます。いくつかのフィールドがあります。_1のようなローカル変数- 射影。これは基底となる場所から「取り出される」フィールドやその他のものです。これらは [newtype 化された][newtype’d]型
[
ProjectionElem] で表されます。したがって、たとえば場所_1.fは射影であり、fが「射影要素」、_1が基底パスです。*_1も射影であり、*は [ProjectionElem::Deref] 要素によって表されます。
- 右辺値は enum [
Rvalue] で表されます。 - オペランドは enum [
Operand] で表されます。
定数の表現
コードが MIR 段階に到達すると、定数は一般に 2 つの形式を取り得ます。
MIR 定数([mir::Constant])と 型システム定数([ty::Const])です。
MIR 定数はオペランドとして使われます。x + CONST では、CONST は MIR 定数です。
同様に、x + 2 では、2 は MIR 定数です。型システム定数は型システムで使われ、特に配列の長さに使われますが、const ジェネリクスにも使われます。
一般に、どちらの種類の定数も「未評価」または「すでに評価済み」であり得ます。
未評価の定数は、この結果を計算するために評価する必要があるものの DefId を単に格納します。
評価済みの定数(「値」)はすでに計算されています。その表現は型システム定数と MIR 定数で異なります。MIR 定数は mir::ConstValue に評価され、型システム定数は ty::ValTree に評価されます。
型システム定数には、const ジェネリクスをサポートするためにさらにいくつかのバリアントがあります。ローカルな const ジェネリックパラメーターを参照でき、推論の対象にもなります。
さらに、mir::Constant::Ty バリアントにより、任意の型システム定数を MIR 定数として使うことができます。これは、const ジェネリックパラメーターがオペランドとして使われるたびに発生します。
MIR 定数値
一般に、MIR 定数値(mir::ConstValue)は、ユーザーが書いた何らかの定数を評価することで計算されたものです。この const 評価 は、結果を個々のバイトという非常に低レベルな表現で生成します。この値はメモリ内に格納されるため、これを「間接」定数(mir::ConstValue::Indirect)と呼びます。
しかし、すべてをメモリ内に格納するのは非常に非効率です。そのため、mir::ConstValue には、特定の単純で一般的な値をより効率的に表現できる他のバリアントがあります。特に、Rust でリテラルとして直接書けるもの(整数、浮動小数点数、char、bool に加えて、"string literals" や b"byte string literals" も含む)には、メモリ内表現の完全なオーバーヘッドを避ける最適化されたバリアントがあります。
ValTree
評価済みの型システム定数は「valtree」です。ty::ValTree データ構造により、次のものを表現できます。
- 配列、
- 多くの構造体、
- タプル、
- enum、および
- ほとんどのプリミティブ。
この表現における最も重要な規則は、すべての値が一意に表現されなければならないということです。
言い換えると、特定の値は特定の 1 つの方法でのみ表現可能でなければなりません。たとえば、2 つの整数からなる配列を ValTree として表現する方法は 1 つしかありません。
Branch([Leaf(first_int), Leaf(second_int)]) です。
理論上は [u32; 2] を u64 にエンコードできるため、単なる
Leaf(bits_of_two_u32) にできるとしても、それは ValTree の正当な構築ではありません
(また、それを行うのは非常に複雑なので、誰かがそうしようとする可能性は低いでしょう)。
これらの規則は、一部の値が表現できないことも意味します。型レベル定数には union は存在できません。アクティブなバリアントが不明であるため、どのように表現すべきか明確ではないからです。同様に、生ポインターを表現する方法もありません。アドレスはコンパイル時には不明であり、そのためそれらについていかなる仮定もできないからです。一方で参照は表現できます。参照の等価性はその値に対する等価性として定義されているため、アドレスは無視し、背後にある値だけを見ます。参照のポインター値がコンパイル時に観測可能でないようにしなければなりません。そのため、&42 は 42 とまったく同じようにエンコードします。
valtree から MIR 定数値への任意の変換では、実際の間接参照を再導入しなければなりません。コード生成時には、複数の使用箇所の間でアドレスが重複排除される場合もされない場合もあり、それは完全に任意の最適化上の選択に依存します。
結果として、ValTree のすべてのデコードは、まず型に対してマッチし、それに応じて判断することで行わなければなりません。値それ自体は、それに属する型がなければ有用な情報を何も与えません。
昇格された定数
const-eval WG の昇格に関するドキュメントを参照してください。
[mir]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/index.html
[mirmanip_build]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir_build/index.html
[mirmanip_transform]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir_transform/index.html
[mirmanip_dataflow]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_mir_dataflow/index.html
[Body]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Body.html
[newtype’d]: ../appendix/glossary.html#newtype
[basicblocks]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Body.html#structfield.basic_blocks
[BasicBlock]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.BasicBlock.html
[BasicBlockData]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.BasicBlockData.html
[Statement]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Statement.html
[Terminator]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/terminator/struct.Terminator.html
[Local]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Local.html
[localdecls]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Body.html#structfield.local_decls
[RETURN_PLACE]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/constant.RETURN_PLACE.html
[Place]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/struct.Place.html
[ProjectionElem]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.ProjectionElem.html
[ProjectionElem::Deref]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.ProjectionElem.html#variant.Deref
[Rvalue]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.Rvalue.html
[Operand]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.Operand.html
[mir::Constant]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/mir/enum.Const.html
[ty::Const]: https://doc.rust-lang.org/nightly/nightly-rustc/rustc_middle/ty/struct.Const.html