crates/mimir-driver/tests/mir_facts.rs
//! MIR facts: run `cargo mimir-dump` on tests/fixture-mir and read the
//! `mir_body` / `mir_block` nodes and `CFG_SUCC` edges back.
//!
//! RED before walk/mir.rs: the dump has no `mir_body` node at all.
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use std::process::Command;
use mimir_driver::read_tables;
use mimir_schema::{kind, mir, moniker};
fn copy_dir(from: &Path, to: &Path) {
std::fs::create_dir_all(to).unwrap();
for e in std::fs::read_dir(from).unwrap().flatten() {
let p = e.path();
let name = e.file_name();
if name == "target" || name == "Cargo.lock" {
continue;
}
if p.is_dir() {
copy_dir(&p, &to.join(&name));
} else {
std::fs::copy(&p, to.join(&name)).unwrap();
}
}
}
const APP: &str = "rust-analyzer cargo app 0.1.0 ";
const DEP: &str = "rust-analyzer cargo dep 0.2.0 ";
struct Dump {
/// id → (kind, name, parent, ty)
nodes: BTreeMap<String, (String, String, String, String)>,
/// (kind, from, to)
edges: BTreeSet<(String, String, String)>,
stats: Vec<serde_json::Value>,
}
fn dump() -> Dump {
let src = PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixture-mir"));
let work = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join(format!("mir-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&work);
copy_dir(&src, &work);
let out = work.join("out");
let st = Command::new(env!("CARGO_BIN_EXE_cargo-mimir-dump"))
.current_dir(&work)
.args(["--out", out.to_str().unwrap(), "--driver", env!("CARGO_BIN_EXE_mimir-driver")])
.status()
.unwrap();
assert!(st.success(), "cargo mimir-dump failed");
assert!(!out.join(".errors").exists(), "walk errors: {:?}", std::fs::read_dir(out.join(".errors")).ok().map(|d| d.flatten().map(|e| std::fs::read_to_string(e.path()).unwrap_or_default()).collect::<Vec<_>>()));
let n = read_tables(&out, "nodes").unwrap();
let e = read_tables(&out, "edges").unwrap();
let (id, k, name, parent, ty) = (n.col("id"), n.col("kind"), n.col("name"), n.col("parent"), n.col("ty"));
let nodes = n.rows.iter().map(|r| (r[id].clone(), (r[k].clone(), r[name].clone(), r[parent].clone(), r[ty].clone()))).collect();
let (f, t, ek) = (e.col("from"), e.col("to"), e.col("kind"));
let edges = e.rows.iter().map(|r| (r[ek].clone(), r[f].clone(), r[t].clone())).collect();
let stats = std::fs::read_dir(&out)
.unwrap()
.flatten()
.filter(|e| e.file_name().to_string_lossy().ends_with(".stats.json"))
.map(|e| serde_json::from_slice(&std::fs::read(e.path()).unwrap()).unwrap())
.collect();
Dump { nodes, edges, stats }
}
impl Dump {
fn body(&self, fn_id: &str) -> Vec<mir::BodyBlock<'_>> {
let (_, (k, _, parent, ty)) = self
.nodes
.iter()
.find(|(_, (k, _, p, _))| k == kind::MIR_BODY && p == fn_id)
.unwrap_or_else(|| panic!("no mir_body under {fn_id}"));
assert_eq!((k.as_str(), parent.as_str()), (kind::MIR_BODY, fn_id));
mir::parse_body(ty).unwrap_or_else(|| panic!("not a body payload: {ty:?}"))
}
/// The fn's `mir_block` nodes: id → (name, parsed payload).
fn blocks(&self, fn_id: &str) -> BTreeMap<&str, (&str, mir::Block<'_>)> {
self.nodes
.iter()
.filter(|(_, (k, _, p, _))| k == kind::MIR_BLOCK && p == fn_id)
.map(|(id, (_, name, _, ty))| (id.as_str(), (name.as_str(), mir::parse_block(ty).unwrap_or_else(|| panic!("not a block payload: {ty:?}")))))
.collect()
}
}
#[test]
fn mir_facts_of_an_if_a_loop_and_a_call() {
let d = dump();
let sample = format!("{APP}sample().");
let body = d.body(&sample);
let blocks = d.blocks(&sample);
assert!(body.len() >= 6, "an if, a loop, a call: {body:#?}");
assert_eq!(blocks.len(), body.len(), "one block node per body entry (local crate = full)");
// the terminators: a branch, a call, a return
let kinds: BTreeSet<&str> = body.iter().map(|b| b.term).collect();
for k in ["switch", "call", "return"] {
assert!(kinds.contains(k), "{k} in {kinds:?}");
}
// the call names the RESOLVED callee by its stable moniker
let helper = moniker::stable(&format!("{APP}helper().")).into_owned();
assert!(blocks.values().any(|(_, b)| b.callee() == Some(helper.as_str())), "a call to helper: {:#?}", blocks.values().map(|(_, b)| b.term.text).collect::<Vec<_>>());
// statements: the `%`, the `+=` (with its overflow check) and a compare
let texts: Vec<&str> = blocks.values().flat_map(|(_, b)| b.stmts.iter().map(|s| s.text)).collect();
assert!(texts.iter().any(|t| t.contains("Rem(")), "{texts:#?}");
assert!(texts.iter().any(|t| t.contains("Lt(")), "{texts:#?}");
assert!(blocks.values().any(|(_, b)| b.term.text.starts_with("assert.overflow.Add")), "the checked `+=`");
// every block's payload carries the same fingerprint the body names
for bb in &body {
let id = format!("{sample}bb{}:", bb.block);
let (name, b) = blocks.get(id.as_str()).unwrap_or_else(|| panic!("no block node {id}"));
assert_eq!(*name, format!("bb{}", bb.block));
assert_eq!(b.fp, bb.fp);
assert_eq!(b.term_kind(), bb.term);
}
// control flow is a graph: CFG_SUCC edges, matching the body's successors, with a back edge (the loop)
let succ: BTreeSet<(String, String)> = d.edges.iter().filter(|(k, f, _)| k == "CFG_SUCC" && f.starts_with(&sample)).map(|(_, f, t)| (f.clone(), t.clone())).collect();
let from_body: BTreeSet<(String, String)> = body.iter().flat_map(|b| b.succ.iter().map(|s| (format!("{sample}bb{}:", b.block), format!("{sample}bb{s}:")))).collect();
assert_eq!(succ, from_body);
let num = |id: &str| id.trim_end_matches(':').rsplit("bb").next().unwrap().parse::<u32>().unwrap();
assert!(succ.iter().any(|(f, t)| num(t) <= num(f)), "a loop has a back edge: {succ:#?}");
// def-use pairs: the loop counter feeds the compare and the `%`
let du: usize = blocks.values().map(|(_, b)| b.def_use.len()).sum();
assert!(du >= 3, "local def-use pairs: {du}");
// positions: relative to the fn's first line, columns 1-based
let pos: Vec<mir::Pos> = blocks.values().flat_map(|(_, b)| b.positions().collect::<Vec<_>>()).collect();
assert!(pos.iter().any(|p| p.dline == 3 && p.col == 11 && !p.macro_site), "`while i < n` is line +3, col 11: {pos:?}");
assert!(pos.iter().all(|p| p.col >= 1 && p.dline >= 0 && p.dline <= 12), "{pos:?}");
// the locals table names types, never a binding name
assert!(blocks.values().all(|(_, b)| b.locals.iter().all(|(_, t)| !t.contains("acc"))));
assert!(blocks.values().any(|(_, b)| b.locals.iter().any(|(_, t)| *t == "u64")));
// a macro's statements sit at the macro call site, marked
let wm = d.blocks(&format!("{APP}with_macro()."));
assert!(wm.values().flat_map(|(_, b)| b.positions().collect::<Vec<_>>()).any(|p| p.macro_site && p.dline == 2), "assert! on line +2");
// a closure's body is folded into its fn (`c1.` blocks)
let wc = d.body(&format!("{APP}with_closure()."));
assert!(wc.iter().any(|b| b.block.starts_with("c1.")), "{wc:#?}");
// a dependency: fingerprints only — the body node, no block nodes
let weigh = format!("{DEP}weigh().");
assert!(d.body(&weigh).len() >= 3);
assert!(d.blocks(&weigh).is_empty(), "no block nodes for a dependency");
let dep_stats = d.stats.iter().find(|s| s["package"] == "dep").expect("dep stats");
assert_eq!(dep_stats["mir_mode"], "fingerprint");
let app_stats = d.stats.iter().find(|s| s["package"] == "app" && s["test_harness"] == false).expect("app stats");
assert_eq!(app_stats["mir_mode"], "full");
assert_eq!(app_stats["mir_facts"], mimir_driver::MIR_FACTS);
assert_eq!(app_stats["mir_missing"], 0);
assert!(app_stats["mir_bodies"].as_u64().unwrap() >= 6);
}
/// Two functions that differ only in their locals' names give equal block
/// fingerprints, block for block.
#[test]
fn renamed_locals_give_equal_block_fingerprints() {
let d = dump();
let a = d.body(&format!("{APP}twin_a()."));
let b = d.body(&format!("{APP}twin_b()."));
let fa: Vec<(&str, &str)> = a.iter().map(|x| (x.fp, x.term)).collect();
let fb: Vec<(&str, &str)> = b.iter().map(|x| (x.fp, x.term)).collect();
assert_eq!(fa, fb);
// … and the statements are the same text (positions aside)
let strip = |m: BTreeMap<&str, (&str, mir::Block<'_>)>| -> Vec<(String, Vec<String>)> {
m.into_values().map(|(n, b)| (n.to_string(), b.stmts.iter().map(|s| s.text.to_string()).collect())).collect()
};
assert_eq!(strip(d.blocks(&format!("{APP}twin_a()."))), strip(d.blocks(&format!("{APP}twin_b()."))));
// and a different function does not share them all
let s = d.body(&format!("{APP}sample()."));
assert_ne!(s.iter().map(|x| x.fp).collect::<Vec<_>>(), a.iter().map(|x| x.fp).collect::<Vec<_>>());
}