crates/mimir-room/tests/room/mir.rs
//! MIR facts in the room, through the consumer's own delta and the kept
//! fold's advance:
//!
//! ```text
//! repo `mx`, crate mx@0.1.0
//! seq 1 an OLD-format dump: fns f, legacy — no MIR rows at all
//! seq 2 the new driver recompiled f: f's mir_body + bb0..bb2 + CFG_SUCC
//! bb0→bb1, bb1→bb1 (the loop), bb1→bb2; legacy unchanged (old rows)
//! ```
//!
//! RED before mimir-room's mir.rs: no `Room::mir_body` / `mir_blocks` /
//! `mir_cfg` / `mir_def_use`, and no CFG_SUCC edge kind.
//!
//! The room keeps the BODY (fingerprints, terminators, successors) and
//! drops the `mir_block` rows and their CFG_SUCC edges: the blocks' payloads
//! are read per repo from the lake ([`MirSide::load`]) by the one lens that
//! needs them (RED before: the room held every block node and edge).
use std::collections::BTreeSet;
use std::path::Path;
use mimir_ingest::{CommitDump, Consumer, CrateDump, Dumper, EventLog};
use mimir_room::{MirSide, Room, Scope};
use mimir_schema::{EdgeKind, EdgeRow, Level, NodeRow, Origin, SPAN_NODE, SpanRow, kind, mir, moniker};
use super::common::{open_log, repo_spec, serial};
fn mx(d: &str) -> String {
moniker::moniker("mx", "0.1.0", d)
}
fn func(d: &str) -> NodeRow {
NodeRow::new(mx(d), kind::FN, Level::Item, d.trim_end_matches("()."), "mx", "0.1.0", Origin::Local)
}
fn span(id: &str, start: u32, end: u32) -> SpanRow {
SpanRow { target: SPAN_NODE, key: id.to_string(), file: "src/lib.rs".into(), start_line: start, end_line: end }
}
/// One commit per entry: (nodes, edges, spans) of unit mx@0.1.0:lib.
struct Commits(Vec<(Vec<NodeRow>, Vec<EdgeRow>, Vec<SpanRow>)>);
impl Dumper for Commits {
fn dump(&mut self, _: &Path, _: &str) -> anyhow::Result<CommitDump> {
let (n, e, s) = self.0.remove(0);
Ok(CommitDump {
toolchain: "nightly-fixture".into(),
workspace_crates: ["mx@0.1.0:lib".to_string()].into(),
workspace_targets: ["mx:lib".to_string()].into(),
failed_units: BTreeSet::new(),
owner_disambiguations: 0,
crates: vec![CrateDump {
krate: "mx@0.1.0:lib".into(),
universe: false,
nodes: vec![mimir_schema::nodes_batch(&n)?],
edges: vec![mimir_schema::edges_batch(&e)?],
spans: vec![mimir_schema::spans_batch(&s)?],
}],
binary: false,
binaries: vec![],
})
}
}
fn scratch(dir: &Path, n: usize) {
nornir_git::gitio::init(dir).unwrap();
for i in 0..n {
std::fs::create_dir_all(dir.join("src")).unwrap();
std::fs::write(dir.join("src/lib.rs"), format!("// commit {i}\n")).unwrap();
nornir_git::gitio::commit_all(dir, &format!("c{i}")).unwrap();
}
}
fn old_format() -> (Vec<NodeRow>, Vec<EdgeRow>, Vec<SpanRow>) {
(vec![func("f()."), func("legacy().")], vec![EdgeRow::new(mx("f()."), mx("legacy()."), EdgeKind::Calls)], vec![span(&mx("f()."), 10, 16), span(&mx("legacy()."), 20, 22)])
}
fn with_mir() -> (Vec<NodeRow>, Vec<EdgeRow>, Vec<SpanRow>) {
let (mut nodes, mut edges, spans) = old_format();
let f = mx("f().");
let part = |id: String, k: &str, name: &str, ty: String| NodeRow { parent: Some(f.clone()), ty: Some(ty), ..NodeRow::new(id, k, Level::Part, name, "mx", "0.1.0", Origin::Local) };
let callee = moniker::stable(&mx("legacy().")).into_owned();
let body = mir::encode_body(&[("0".into(), 0xa, "goto", vec!["1".into()]), ("1".into(), 0xb, "call", vec!["1".into(), "2".into()]), ("2".into(), 0xc, "return", vec![])]);
let blocks = [
("0", mir::encode_block(0xa, "goto@1:5", &["_2=use(k:u64)@1:17".into()], &[(2, "u64".into())], &[])),
(
"1",
mir::encode_block(
0xb,
&format!("call {callee}{}_2{}->_3@3:9", mir::ITEM_CALLEE_END, mir::ITEM_CALLEE_ARGS_END),
&["_2=Add(_2,k:u64)@2:9".into()],
&[(2, "u64".into()), (3, "u64".into())],
&[(2, 3)],
),
),
("2", mir::encode_block(0xc, "return@6:1", &["_0=use(_3)@5:5m".into()], &[(0, "u64".into()), (3, "u64".into())], &[(3, 0)])),
];
nodes.push(part(format!("{f}mir:"), kind::MIR_BODY, "mir", body));
edges.push(EdgeRow::new(f.clone(), format!("{f}mir:"), EdgeKind::Contains));
for (b, ty) in blocks {
nodes.push(part(format!("{f}bb{b}:"), kind::MIR_BLOCK, &format!("bb{b}"), ty));
}
for (a, b) in [("0", "1"), ("1", "1"), ("1", "2")] {
edges.push(EdgeRow::new(format!("{f}bb{a}:"), format!("{f}bb{b}:"), EdgeKind::CfgSucc));
}
(nodes, edges, spans)
}
/// [`with_mir`] at the next commit: block 2 rewritten, block 0 gone.
fn with_mir_changed() -> (Vec<NodeRow>, Vec<EdgeRow>, Vec<SpanRow>) {
let (mut nodes, mut edges, spans) = with_mir();
let f = mx("f().");
nodes.retain(|n| n.id != format!("{f}bb0:"));
edges.retain(|e| e.from != format!("{f}bb0:"));
for n in nodes.iter_mut().filter(|n| n.id == format!("{f}bb2:")) {
n.ty = Some(mir::encode_block(0xd, "return@6:1", &["_0=use(_9)@5:5".into()], &[(0, "u64".into()), (9, "u64".into())], &[(9, 0)]));
}
(nodes, edges, spans)
}
async fn consume(log: EventLog, tmp: &Path, commits: Commits) -> EventLog {
let mut c = Consumer::boot(log, commits).await.unwrap();
let rep = c.run(&repo_spec(tmp, "mx")).await.unwrap();
assert!(rep.iter().all(|r| r.status == "ok"), "{rep:?}");
c.into_parts().0
}
#[tokio::test(flavor = "current_thread")]
async fn an_old_dump_answers_without_mir_and_a_new_one_adds_blocks_cfg_and_def_use() {
let _g = serial().await;
Room::clear_cache();
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("src-mx");
scratch(&src, 1);
let log = open_log(tmp.path()).await;
let log = consume(log, tmp.path(), Commits(vec![old_format()])).await;
// seq 1: the old format ingests and answers — no MIR facts, said so
let first = Room::open(&log, &Scope::current()).await.unwrap();
let r = &first.room;
let (f, legacy) = (r.ordinal(&mx("f().")).unwrap(), r.ordinal(&mx("legacy().")).unwrap());
assert!(r.mir_body(f).is_none() && !r.has_mir(legacy));
let side = MirSide::load(&log, "mx", None, usize::MAX).await.unwrap();
assert!(side.is_empty() && !side.capped);
assert!(r.mir_blocks(f, &side).is_empty() && r.mir_cfg(f).is_empty() && r.mir_def_use(f, &side).is_empty());
// seq 2: f recompiled by the new driver — the delta carries its MIR facts
std::fs::write(src.join("src/lib.rs"), "// commit 1\n").unwrap();
nornir_git::gitio::commit_all(&src, "c1").unwrap();
let log = consume(log, tmp.path(), Commits(vec![with_mir()])).await;
let adv = Room::open(&log, &Scope::current()).await.unwrap();
assert!(adv.advanced, "the kept fold advances over the MIR rows like any other");
let r = &adv.room;
let (f, legacy) = (r.ordinal(&mx("f().")).unwrap(), r.ordinal(&mx("legacy().")).unwrap());
assert!(!r.has_mir(legacy), "a fn the new driver did not redump keeps its old facts");
let body = r.mir_body(f).expect("f's body node");
assert_eq!(body.blocks.iter().map(|b| (b.block, b.term)).collect::<Vec<_>>(), vec![("0", "goto"), ("1", "call"), ("2", "return")]);
// the room holds NO block node and NO CFG_SUCC edge: the blocks live in
// the lake, read per repo on demand
assert_eq!(r.ordinal(&format!("{}bb0:", mx("f()."))), None, "a mir_block row is not a room node");
assert_eq!(r.stats().edges.get(&EdgeKind::CfgSucc), None, "CFG_SUCC is not a room edge");
let side = MirSide::load(&log, "mx", None, usize::MAX).await.unwrap();
assert_eq!(side.len(), 3);
assert!(r.has_mir_blocks(f, &side));
let blocks = r.mir_blocks(f, &side);
assert_eq!(blocks.len(), 3);
assert_eq!(blocks.iter().map(|b| b.block).collect::<Vec<_>>(), vec!["0", "1", "2"]);
// positions are relative to f's first line (10)
assert_eq!(blocks[0].lines, vec![11]);
assert_eq!(blocks[1].lines, vec![12, 13]);
assert_eq!(blocks[2].lines, vec![15, 16]);
assert!(blocks[2].facts.stmts[0].pos.is_some_and(|p| p.macro_site));
assert_eq!(blocks[1].facts.callee(), Some(moniker::stable(&mx("legacy().")).as_ref()));
// control flow from the body's successors, the loop included
let mut cfg = r.mir_cfg(f);
cfg.sort();
assert_eq!(cfg, vec![("0", "1"), ("1", "1"), ("1", "2")]);
// local-to-local def-use within the body
assert_eq!(r.mir_def_use(f, &side), vec![(2, 3), (3, 0)]);
// a capped side holds what fit and says so
let small = MirSide::load(&log, "mx", None, 1).await.unwrap();
assert!(small.capped && small.len() < 3);
// the oracle: a fresh full build answers the same
Room::clear_cache();
let full = Room::open(&log, &Scope::current()).await.unwrap();
assert!(!full.advanced);
let f2 = full.room.ordinal(&mx("f().")).unwrap();
assert_eq!(full.room.mir_def_use(f2, &side), r.mir_def_use(f, &side));
assert_eq!(full.room.mir_blocks(f2, &side).iter().map(|b| b.lines.clone()).collect::<Vec<_>>(), blocks.iter().map(|b| b.lines.clone()).collect::<Vec<_>>());
assert_eq!(full.room.stats().edges.get(&EdgeKind::CfgSucc), None);
// the READ-FAST zone answers the same side, and its event tail too: the
// next commit rewrites block 2 and drops block 0
log.materialize("mx").await.unwrap();
assert_eq!(MirSide::load(&log, "mx", None, usize::MAX).await.unwrap(), side, "zone read = event read");
std::fs::write(src.join("src/lib.rs"), "// commit 2\n").unwrap();
nornir_git::gitio::commit_all(&src, "c2").unwrap();
let log = consume(log, tmp.path(), Commits(vec![with_mir_changed()])).await;
let tail = MirSide::load(&log, "mx", None, usize::MAX).await.unwrap();
assert_eq!(tail.len(), 2, "bb0 left with the tail's `-`");
assert!(tail.payload(&format!("{}bb2:", mx("f()."))).is_some_and(|p| p.contains("_0=use(_9)")), "the tail's new payload wins");
// and the time-travel read at seq 2 still sees the three blocks
assert_eq!(MirSide::load(&log, "mx", Some(2), usize::MAX).await.unwrap(), side);
}