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

nordisk/nornir

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