MIRを触ってみる
MIRはRustプログラムがLLVM IRに変換される直前の段階で使用されている中間言語です。Rustの型情報が残っているため、高度あるいは高速な最適化への応用が期待される他、NLLの実装にも用いられています。当記事では実際にMIRを加工するパスを作成してみます。コンパイラの内部APIに触れるため要Nightlyです。また文中でコンパイラの内部APIのドキュメントである https://manishearth.github.io/ へのリンクがありますが、ここは結構古くなっているので注意が必要です。

さて、MIRはこんな感じのものです。Rustそっくりなので、見れば何が行われているか分かりますね。なお、このテキスト形式はデバッグ用の表現であり、これがMIRという訳ではありません。実態はコンパイラ内のデータ構造(Mir)です。

Rustコード
fn foo(x: i32, y: i32) -> i32 {
    x * x + y * y
}
MIR (rustc --emit=mir -O)
fn foo(_1: i32, _2: i32) -> i32 {
    let mut _0: i32;                     // return place
    let mut _3: i32;
    let mut _4: i32;
    let mut _5: i32;
    let mut _6: i32;
    let mut _7: i32;
    let mut _8: i32;

    bb0: {                              
        StorageLive(_3);                 // bb0[0]: scope 0 at test.rs:4:5: 4:10
        StorageLive(_4);                 // bb0[1]: scope 0 at test.rs:4:5: 4:6
        _4 = _1;                         // bb0[2]: scope 0 at test.rs:4:5: 4:6
        StorageLive(_5);                 // bb0[3]: scope 0 at test.rs:4:9: 4:10
        _5 = _1;                         // bb0[4]: scope 0 at test.rs:4:9: 4:10
        _3 = Mul(move _4, move _5);      // bb0[5]: scope 0 at test.rs:4:5: 4:10
        StorageDead(_5);                 // bb0[6]: scope 0 at test.rs:4:10: 4:10
        StorageDead(_4);                 // bb0[7]: scope 0 at test.rs:4:10: 4:10
        StorageLive(_6);                 // bb0[8]: scope 0 at test.rs:4:13: 4:18
        StorageLive(_7);                 // bb0[9]: scope 0 at test.rs:4:13: 4:14
        _7 = _2;                         // bb0[10]: scope 0 at test.rs:4:13: 4:14
        StorageLive(_8);                 // bb0[11]: scope 0 at test.rs:4:17: 4:18
        _8 = _2;                         // bb0[12]: scope 0 at test.rs:4:17: 4:18
        _6 = Mul(move _7, move _8);      // bb0[13]: scope 0 at test.rs:4:13: 4:18
        StorageDead(_8);                 // bb0[14]: scope 0 at test.rs:4:18: 4:18
        StorageDead(_7);                 // bb0[15]: scope 0 at test.rs:4:18: 4:18
        _0 = Add(move _3, move _6);      // bb0[16]: scope 0 at test.rs:4:5: 4:18
        StorageDead(_6);                 // bb0[17]: scope 0 at test.rs:4:18: 4:18
        StorageDead(_3);                 // bb0[18]: scope 0 at test.rs:4:18: 4:18
        return;                          // bb0[19]: scope 0 at test.rs:5:2: 5:2
    }
}

MIRの概要

意義や導入の経緯に関しては https://blog.rust-lang.org/2016/04/19/MIR.html をご覧ください。コンパイル時間および型チェック・ボローチェックの改善は実現しつつありますが、後述するようにMIR上の最適化の実装はあまり進んでいません。

MIRの最も大きな単位は関数です。クロージャも関数に数えられ、また右辺値の昇格によって生成される初期化関数も含みます。関数は複数の基本ブロック(bb)単位で構成され、それぞれが複数のステートメント(Statement)を持つという構造になっています。ただしテキスト形式のMIRで最後に表示されているものはステートメントではなく、基本ブロックからどのように抜けるかを指定するターミネータ(Terminator)で、分岐したり関数から抜けたりといった動作を指定します。ちなみに、MIR上ではmonomorphizationは行われていません。ジェネリックな型がそのまま残っています。

_nで示されているのはローカル変数(Local)で、連番が割り当てられていきます。_0が返り値を格納する場所で、その後に引数が続き、残りはユーザの作成した変数およびコンパイラによって生成され値の受け渡しに一時的に使われる変数となります。

ドライバの作成

独自のMIRパスを挿入するためには、以前はrustcのソースコード自体を弄る必要がありました。しかし最近の変更によりクエリ(後述)を差し替えた状態でコンパイラを立ち上げられるようになったため、気軽に試すことが可能です。Rustコンパイラを呼び出すプログラムであるドライバの内部でクエリの差し替えを行います。