Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

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);
}