Maintaining serialization code with lossless roundtrips is really annoying and error-prone, unless you specifically use a library that uses a strict format to generate both directions from one specification. This is true even if the data is just a bunch of atoms in a JSON file, and much worse with a custom human-readable format which contains as much information as MIR.
Writing the tests in Rusts would not only avoid this work, but I would argue that it results in better tests as well. While it does mean the tests also cover the entire first half of the compilation pipeline, anything which takes in a whole module, runs a whole optimization process on it, and then inspects the resulting module, is already an integration test. Increasing the scope of such integration tests means more work, yes, but also more confidence about the application as a whole. More concretely, it provides some assurance that the optimization actually fires on real MIR generated from real Rust code — and ultimately that’s what we care about.
LLVM is in a very different situation: It’s its own independent project, it powers a lot of quite different frontends, and it already has stable and round-tripping serialization. For LLVM it makes sense to write tests that go from IR to IR. MIR is rustc-only, so different tradeoffs apply.
However, to make such tests more self-documenting, and to make it easier to tell why a test failed (a change in the earlier passes, or in the pass being tested), the test format should include a dump of the MIR before the pass under test ran, and perhaps the list of passes to run beforehand should be specified. Something like (MIR from play.rlo, hence the <anon>):
#![crate_type="lib"]
pub fn foo(x: i32) -> i32 {
x
}
--- pre: SimplifyCfg SomeOtherPassRunningBefore ---
fn foo(arg0: i32) -> i32 {
scope 1 {
let var0: i32; // "x" in scope 1 at <anon>:2:12: 2:13
}
let mut tmp0: i32;
bb0: {
var0 = arg0; // scope 0 at <anon>:2:12: 2:13
tmp0 = var0; // scope 1 at <anon>:3:5: 3:6
return = tmp0; // scope 1 at <anon>:3:5: 3:6
goto -> bb1; // scope 1 at <anon>:2:1: 4:2
}
bb1: {
return; // scope 1 at <anon>:2:1: 4:2
}
}
--- post: MoveForwarding ---
fn foo(arg0: i32) -> i32 {
scope 1 {
let var0: i32; // "x" in scope 1 at <anon>:2:12: 2:13
}
bb0: {
var0 = arg0; // scope 0 at <anon>:2:12: 2:13
return = var0; // scope 1 at <anon>:3:5: 3:6
goto -> bb1; // scope 1 at <anon>:2:1: 4:2
}
bb1: {
return; // scope 1 at <anon>:2:1: 4:2
}
}
This probably requires some normalization, like the UI tests.