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/src/lib.rs

//! Shared, rustc-free half of mimir-driver: the toolchain pin, the rustc argv
//! reading, the crate-version map (`MIMIR_CRATE_VERSIONS`), the per-artifact
//! sidecar index, and the per-crate stats record. The `mimir-driver` binary
//! (src/main.rs, `rustc_private`) and `cargo-mimir-dump` both use it.
//!
//! ## Extern crate versions (plan §10 "ADDED")
//!
//! rustc metadata carries a crate's NAME and its StableCrateId, never its
//! package version. Two sources, in order:
//!
//! 1. **Sidecar index (exact).** Under `RUSTC_WRAPPER` every rustc invocation of
//!    the build goes through the driver, so when cargo compiles package
//!    `serde 1.0.228` the driver writes `$MIMIR_INDEX_DIR/serde-<hash>.json`
//!    with `CARGO_PKG_NAME` / `CARGO_PKG_VERSION`, keyed by `--crate-name` +
//!    `-C extra-filename` — i.e. by the artifact's file stem
//!    (`libserde-<hash>.rmeta`). A dependent crate later asks
//!    `tcx.used_crate_source(cnum)` for the exact file rustc loaded and looks
//!    that stem up. Two versions of one name have two hashes → two sidecars, no
//!    ambiguity, no guessing from argv. The index lives in the SAME target dir
//!    as the artifacts, so a warm re-check (cargo skips unchanged deps) still
//!    finds them.
//! 2. **The cargo-metadata map (fallback).** `MIMIR_CRATE_VERSIONS` is a JSON
//!    file built from ONE `cargo metadata` (cargo-mimir-dump writes it): every
//!    package with its lib crate name, version and workspace membership. Used
//!    when no sidecar exists (e.g. `RUSTC_WORKSPACE_WRAPPER` mode, where deps are
//!    compiled by plain rustc) and the crate name maps to exactly ONE package.
//!
//!    When the name is ambiguous (two versions of one crate), a DIRECT dep is
//!    still resolved exactly: rustc's argv has `--extern a_old=…/liba-HASH.rmeta`,
//!    the loaded file of the crate is `tcx.used_crate_source(cnum)`, and the map
//!    carries cargo's resolve graph (`extern_deps`: package → extern name →
//!    package@version), so `a_old` → `a 0.0.9`.
//!
//! Otherwise version = `"?"` and the crate is counted in `unresolved_versions`.
//!
//! ## Cargo features (GATED_BY / ENABLES)
//!
//! The same ONE `cargo metadata` carries every package's `[features]` table
//! (implicit `dep:` features of optional dependencies included). The version
//! map keeps it per package ([`FeatureDecl`]), each entry already resolved to
//! the package@version it points at ([`FeatureEnable`]), so the driver writes
//! feature nodes and ENABLES edges without reading a manifest itself. Which
//! features a crate was COMPILED with is not in the map: that is rustc's own
//! `--cfg feature="…"` set, read per compile.
//!
//! [`cfg_scan`] is the second, token-level source for items the build's
//! configuration compiled out (rustc never sees those).

pub mod cfg_scan;

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};

/// The ONE nightly this driver is built against (== rust-toolchain.toml).
pub const MIMIR_NIGHTLY: &str = "nightly-2026-10-05";
/// `rustc -vV` commit-hash of [`MIMIR_NIGHTLY`].
pub const MIMIR_NIGHTLY_COMMIT: &str = "28221559263a3976766cf305940e80e30cf9ba8a";

/// Env: output directory for the Arrow files. Unset → the driver is a plain rustc.
pub const ENV_OUT: &str = "MIMIR_OUT";
/// Env: path of the JSON crate-version map (from one `cargo metadata`).
pub const ENV_VERSIONS: &str = "MIMIR_CRATE_VERSIONS";
/// Env: sidecar index directory (inside the target dir).
pub const ENV_INDEX: &str = "MIMIR_INDEX_DIR";
/// Env: git SHA written as `valid_from_sha`.
pub const ENV_SHA: &str = "MIMIR_SHA";
/// Env: repo name written in `repo` for local nodes.
pub const ENV_REPO: &str = "MIMIR_REPO";
/// Env: `all` (default) dumps every compiled crate (deps too — the universe
/// ring, once per name@version per target dir); `workspace` dumps only
/// `CARGO_PRIMARY_PACKAGE` crates; `off` dumps nothing — the driver is a
/// plain rustc of its nightly (a dependent of a change compiled only to
/// see that it still builds: `BuildWorkspace { only: Affected }`), the
/// sidecars still written, so the units are the ones a dump would compile.
pub const ENV_SCOPE: &str = "MIMIR_DUMP";

/// Where this compile's dump goes: `MIMIR_OUT`, unless `MIMIR_DUMP=off`.
pub fn dump_out(out: Option<std::ffi::OsString>, scope: Option<&str>) -> Option<PathBuf> {
    out.filter(|_| scope != Some("off")).map(PathBuf::from)
}
/// Env: which crates get the token scan ([`cfg_scan`]: compiled-out items
/// and the gates INSIDE items): `local` (default) = the workspace's own
/// crates; `all` = dependencies too (the universe ring, once per
/// name@version); `off` = none. The own gates of the items rustc KEPT,
/// feature nodes and ENABLES are written for every crate regardless.
pub const ENV_CFG_SCAN: &str = "MIMIR_CFG_SCAN";
/// The cfg this driver sets for the crates it compiles from our own sources
/// (`--cfg mimir`), and the tool it registers: `#[cfg_attr(mimir,
/// mimir::table(read, name))]` — the one annotation the table pass reads.
pub const CFG: &str = "mimir";

/// Env: `1` = the toolchain-crate dump (`cargo mimir-dump --std`, i.e. cargo
/// `-Zbuild-std`): every crate the sysroot ships (std, core, alloc and std's
/// private deps) is written under the lang identity (package = crate name,
/// version = rust-analyzer's `https://github.com/rust-lang/rust/library/<crate>`,
/// origin `lang`) — the SAME ids a normal dump gives them as dependencies.
pub const ENV_STD: &str = "MIMIR_STD_DUMP";

/// What the rustc argv says about this invocation.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Invocation {
    pub crate_name: Option<String>,
    pub crate_types: Vec<String>,
    /// `-C extra-filename=` value, e.g. `-1a2b3c4d5e6f7a8b`.
    pub extra_filename: Option<String>,
    /// `-C metadata=` value: cargo passes it for EVERY unit, also the ones
    /// without an extra-filename (an uplifted bin of `cargo build`).
    pub metadata: Option<String>,
    pub is_test: bool,
    pub has_print: bool,
    pub has_input: bool,
    /// The crate's root file as cargo passed it (relative to rustc's cwd, or
    /// absolute): it tells which TARGET of the package this unit is.
    pub input: Option<PathBuf>,
    /// `--extern NAME=PATH` pairs (the name the crate is known by here).
    pub externs: Vec<(String, PathBuf)>,
    /// `--cfg` values (features are `feature="x"`).
    pub cfgs: Vec<String>,
    /// `-C` options that change what the crate IS (`opt-level`,
    /// `debug-assertions`, `overflow-checks`, `target-cpu`, `target-feature`, `panic`).
    pub codegen: Vec<String>,
    /// `--target` (absent: the host).
    pub target: Option<String>,
}

impl Invocation {
    /// Read rustc's argv (WITHOUT the program name).
    pub fn parse(args: &[String]) -> Invocation {
        let mut inv = Invocation::default();
        let mut i = 0;
        while i < args.len() {
            let a = args[i].as_str();
            let next = args.get(i + 1).map(String::as_str);
            let mut take = |flag: &str| -> Option<String> {
                if a == flag {
                    i += 1;
                    next.map(str::to_string)
                } else {
                    a.strip_prefix(flag).and_then(|r| r.strip_prefix('=')).map(str::to_string)
                }
            };
            if let Some(v) = take("--crate-name") {
                inv.crate_name = Some(v);
            } else if let Some(v) = take("--crate-type") {
                inv.crate_types.extend(v.split(',').map(str::to_string));
            } else if let Some(v) = take("--extern") {
                if let Some((n, p)) = v.split_once('=') {
                    // `noprelude:alloc=…` / `priv:x=…` → the bare name
                    let n = n.rsplit(':').next().unwrap_or(n);
                    inv.externs.push((n.to_string(), PathBuf::from(p)));
                }
            } else if let Some(v) = take("--print") {
                let _ = v;
                inv.has_print = true;
            } else if a == "-C" || a == "--codegen" {
                if let Some(v) = next {
                    if let Some(x) = v.strip_prefix("extra-filename=") {
                        inv.extra_filename = Some(x.to_string());
                    } else if let Some(x) = v.strip_prefix("metadata=") {
                        inv.metadata = Some(x.to_string());
                    } else if SEMANTIC_CODEGEN.iter().any(|k| v.starts_with(k)) {
                        inv.codegen.push(v.to_string());
                    }
                }
                i += 1;
            } else if let Some(x) = a.strip_prefix("-Cextra-filename=") {
                inv.extra_filename = Some(x.to_string());
            } else if let Some(x) = a.strip_prefix("-Cmetadata=") {
                inv.metadata = Some(x.to_string());
            } else if let Some(x) = a.strip_prefix("-C").filter(|x| SEMANTIC_CODEGEN.iter().any(|k| x.starts_with(k))) {
                inv.codegen.push(x.to_string());
            } else if let Some(v) = take("--cfg") {
                inv.cfgs.push(v);
            } else if let Some(v) = take("--target") {
                inv.target = Some(v);
            } else if a == "--test" {
                inv.is_test = true;
            } else if a == "-" || (!a.starts_with('-') && (a.ends_with(".rs") || Path::new(a).is_file())) {
                inv.has_input = true;
                if a != "-" {
                    inv.input = Some(PathBuf::from(a));
                }
            } else if a == "-vV" || a == "-V" || a == "--version" {
                inv.has_print = true;
            } else if matches!(
                a,
                "-o" | "--out-dir" | "--edition" | "--emit" | "-L"
                    | "--check-cfg" | "--cap-lints" | "--error-format" | "--json"
                    | "--diagnostic-width" | "-A" | "-W" | "-D" | "-F" | "--sysroot" | "-Z"
                    | "--remap-path-prefix" | "--env-set" | "--explain" | "-l"
            ) {
                // flags that take a separate value: skip the value
                i += 1;
            }
            i += 1;
        }
        inv
    }

    pub fn is_proc_macro(&self) -> bool {
        self.crate_types.iter().any(|t| t == "proc-macro")
    }

    pub fn is_build_script(&self) -> bool {
        self.crate_name.as_deref().is_some_and(|n| n.starts_with("build_script_"))
    }

    pub fn is_lib(&self) -> bool {
        self.crate_types
            .iter()
            .any(|t| matches!(t.as_str(), "lib" | "rlib" | "dylib" | "cdylib" | "staticlib"))
    }

    /// The unit's SEMANTIC key: what makes its dump what it is — package,
    /// version, source (its manifest dir), crate, target kinds, test harness,
    /// every `--cfg` (features included), the `-C` options that change the
    /// code, the target. NOT the hashes of its dependencies: two compiles of
    /// one crate with one feature set read the same.
    pub fn semkey(&self, package: &str, version: &str, manifest_dir: &str) -> String {
        let mut cfgs = self.cfgs.clone();
        cfgs.sort();
        let mut cg = self.codegen.clone();
        cg.sort();
        let mut types = self.crate_types.clone();
        types.sort();
        format!(
            "{package}@{version}|{manifest_dir}|{}|{}|{}|{}|{}|{}",
            self.crate_name.as_deref().unwrap_or(""),
            types.join(","),
            self.is_test,
            cfgs.join(","),
            cg.join(","),
            self.target.as_deref().unwrap_or("host")
        )
    }

    /// Sidecar / artifact key: `<crate_name><extra_filename>` (= artifact stem without `lib`).
    pub fn artifact_key(&self) -> Option<String> {
        Some(format!("{}{}", self.crate_name.as_deref()?, self.extra_filename.as_deref()?))
    }

    /// The unit part of the output file names — unique per rustc invocation
    /// of one build:
    /// * cargo's `-C extra-filename` hash (what cargo's JSON names the unit by);
    /// * else `m<-C metadata>`: cargo omits the extra-filename for an uplifted
    ///   bin of `cargo build` (release bins), but every unit still has its own
    ///   metadata hash — two bins with one name + version stay apart;
    /// * else `nohash.<pid>`: a rustc run that is not a cargo unit (a build
    ///   script's feature probe). Consumers skip `nohash*` stems.
    pub fn short_hash(&self) -> String {
        if let Some(e) = self.extra_filename.as_deref().map(|e| e.trim_start_matches('-')).filter(|s| !s.is_empty()) {
            return e.to_string();
        }
        if let Some(m) = self.metadata.as_deref().filter(|s| !s.is_empty()) {
            return format!("m{m}");
        }
        format!("{NOHASH}.{}", std::process::id())
    }
}

/// `-C` options that change what a crate's code is (its cfgs, its bodies).
pub const SEMANTIC_CODEGEN: [&str; 6] = ["opt-level", "debug-assertions", "overflow-checks", "target-cpu", "target-feature", "panic"];

/// A short, stable hex name of a semantic key (FNV-1a 64; the same in every
/// process and every build of this driver).
pub fn semhash(semkey: &str) -> String {
    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
    for b in semkey.as_bytes() {
        h ^= u64::from(*b);
        h = h.wrapping_mul(0x0000_0100_0000_01b3);
    }
    format!("{h:016x}")
}

/// **The semantic key a dump is known by** across builds: the unit's own
/// ([`Invocation::semkey`]) plus what decides its ROWS that the unit itself
/// does not say —
/// * the package@version every `--extern` resolves to (its sidecar in
///   `index_dir`; an artifact with no sidecar is named by its file stem, so it
///   never equals another): the edges into a dependency carry its version;
/// * its features' optional dependencies as the reading repo's lockfile
///   resolves them (the feature file of the package at `manifest_dir`, next
///   to `versions_file`): `feature=x ENABLES dep@version`.
/// Two units with one key write the same rows (the repo's commit sha aside);
/// a twin whose dependency is at another version keeps its own (measured
/// 2026-10-16 on the 21 nordisk repos: 31% of the units equal on the bare
/// semantic key wrote other rows, most of them for this).
pub fn unit_semkey(inv: &Invocation, package: &str, version: &str, manifest_dir: &str, index_dir: Option<&Path>, versions_file: Option<&Path>) -> String {
    let mut key = inv.semkey(package, version, manifest_dir);
    let mut deps: Vec<String> = inv
        .externs
        .iter()
        .map(|(name, path)| {
            let stem = artifact_key_of(path).unwrap_or_default();
            match index_dir.and_then(|d| read_sidecar(d, &stem)) {
                Some(s) => format!("{name}={}@{}", s.package, s.version),
                None => format!("{name}=#{stem}"),
            }
        })
        .collect();
    deps.sort();
    deps.dedup();
    key.push_str("|deps:");
    key.push_str(&deps.join(","));
    if let Some(vf) = versions_file {
        let mut opt: Vec<String> = VersionMap::load_features_at(vf, package, version, Path::new(manifest_dir))
            .iter()
            .flat_map(|f| f.enables.iter().filter_map(move |e| Some(format!("{}:{}@{}", f.name, e.dep_package.as_deref()?, e.dep_version.as_deref().unwrap_or("?")))))
            .collect();
        opt.sort();
        opt.dedup();
        key.push_str("|opt:");
        key.push_str(&opt.join(","));
    }
    key
}

/// A source file the crate's build script GENERATED (under cargo's
/// `OUT_DIR`, a directory named by the unit's hash) as `OUT_DIR/<rest>`: the
/// same generated file reads the same in every build of the crate, not under
/// one target dir's hash. `None`: not under `out_dir`.
pub fn out_dir_relative(path: &Path, out_dir: Option<&Path>) -> Option<String> {
    let rest = path.strip_prefix(out_dir?).ok()?;
    Some(format!("OUT_DIR/{}", rest.to_string_lossy()))
}

/// A printed type with every `a::b::C` path cut to its last segment
/// (`std::collections::BTreeMap<u32, crate::Point>` → `BTreeMap<u32, Point>`):
/// the human `ty` column, made from a type printed with FULL paths. The
/// compiler's own trimming is never asked: its `trimmed_def_paths` query makes
/// rustc ICE at the end of a session that emitted no diagnostic (dwarf
/// 2026-10-17: toml_edit and every crate like it).
pub fn shorten_paths(s: &str) -> String {
    let b = s.as_bytes();
    let is_start = |c: u8| c.is_ascii_alphabetic() || c == b'_' || c >= 0x80;
    let is_cont = |c: u8| c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80;
    let mut out = String::with_capacity(s.len());
    let mut i = 0;
    while i < b.len() {
        if is_start(b[i]) && (i == 0 || !is_cont(b[i - 1])) {
            // an identifier, and the `::ident` segments right after it: keep the last
            let mut start = i;
            let mut end = i;
            while end < b.len() && is_cont(b[end]) {
                end += 1;
            }
            while end + 2 < b.len() && &b[end..end + 2] == b"::" && is_start(b[end + 2]) {
                start = end + 2;
                end = start;
                while end < b.len() && is_cont(b[end]) {
                    end += 1;
                }
            }
            out.push_str(&s[start..end]);
            i = end;
        } else {
            let ch = s[i..].chars().next().unwrap_or(' ');
            out.push(ch);
            i += ch.len_utf8();
        }
    }
    out
}

/// [`unit_semkey`] of the unit this process compiles, hashed ([`semhash`]):
/// cargo's environment (`CARGO_PKG_NAME`, `CARGO_PKG_VERSION`,
/// `CARGO_MANIFEST_DIR`) and the driver's (`MIMIR_INDEX_DIR`,
/// `MIMIR_CRATE_VERSIONS`). The ONE spelling of the hash a dump is stored
/// under and a ring check looks for.
pub fn unit_semhash_of_env(inv: &Invocation) -> String {
    let var = |k: &str| std::env::var(k).unwrap_or_default();
    let index = std::env::var_os(ENV_INDEX).map(PathBuf::from);
    let versions = std::env::var_os(ENV_VERSIONS).map(PathBuf::from);
    semhash(&unit_semkey(inv, &var("CARGO_PKG_NAME"), &var("CARGO_PKG_VERSION"), &var("CARGO_MANIFEST_DIR"), index.as_deref(), versions.as_deref()))
}

/// A file of semantic hashes, one per line: dependency units whose dump is
/// already in the log's ring. Such a unit is compiled (its dependents need its
/// metadata) with its table summaries, but not walked or dumped again; the
/// driver leaves `<stem>.ring` in the out dir instead (holding the hash).
pub const ENV_RING_HAVE: &str = "MIMIR_RING_HAVE";

/// Is `hash` in the ring-have file at `path`?
pub fn ring_has(path: &Path, hash: &str) -> bool {
    std::fs::read_to_string(path).is_ok_and(|s| s.lines().any(|l| l.trim() == hash))
}

/// Stem prefix of a dump that is no cargo unit (see [`Invocation::short_hash`]).
pub const NOHASH: &str = "nohash";

/// The crate names the toolchain at `sysroot` ships as rlibs
/// (`lib/rustlib/<target>/lib/lib<crate>-<hash>.rlib`): std, core, alloc,
/// proc_macro, test and std's private deps (hashbrown, gimli …).
pub fn sysroot_crates(sysroot: &Path) -> std::collections::BTreeSet<String> {
    let mut out = std::collections::BTreeSet::new();
    let Ok(targets) = std::fs::read_dir(sysroot.join("lib/rustlib")) else { return out };
    for t in targets.flatten() {
        let Ok(libs) = std::fs::read_dir(t.path().join("lib")) else { continue };
        for f in libs.flatten() {
            let n = f.file_name().to_string_lossy().into_owned();
            if let Some(stem) = n.strip_prefix("lib").and_then(|r| r.strip_suffix(".rlib")) {
                if let Some((name, _hash)) = stem.rsplit_once('-') {
                    out.insert(name.to_string());
                }
            }
        }
    }
    out
}

/// The artifact key of a file rustc loaded (`.../libserde-1a2b.rmeta` → `serde-1a2b`).
pub fn artifact_key_of(path: &Path) -> Option<String> {
    let stem = path.file_stem()?.to_str()?;
    Some(stem.strip_prefix("lib").unwrap_or(stem).to_string())
}

/// How one entry of a `[features]` value switches something on.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub enum EnableForm {
    /// `"other"` — another feature of the same package.
    Feature,
    /// `"dep:x"` — the optional dependency `x`.
    Dep,
    /// `"x/f"` — dependency `x` AND its feature `f`.
    DepFeature,
    /// `"x?/f"` — feature `f` of `x`, only if `x` is enabled by another route.
    WeakDepFeature,
}

/// One entry of a `[features]` value, resolved against the lockfile.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FeatureEnable {
    /// The manifest string: `std`, `dep:serde`, `serde/derive`, `serde?/derive`.
    pub raw: String,
    pub form: EnableForm,
    /// The dependency's PACKAGE name (dep forms; the manifest key may be a rename).
    pub dep_package: Option<String>,
    /// Its version in the lockfile. `None` = not resolvable: no dependency of
    /// that key, or an optional dependency the resolve did not activate whose
    /// name the lockfile holds in more than one version.
    pub dep_version: Option<String>,
    /// The feature switched on: this package's ([`EnableForm::Feature`]) or
    /// the dependency's (the two `x/f` forms).
    pub feature: Option<String>,
}

/// One declared feature of a package.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FeatureDecl {
    pub name: String,
    /// 1-based line of its key in the manifest's `[features]` table; `None`
    /// for an implicit feature (an optional dependency without one).
    pub line: Option<u32>,
    pub enables: Vec<FeatureEnable>,
}

/// One cargo target of a workspace member.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TargetDecl {
    /// Cargo's target kind — `bin`, `example`, `test`, `bench`,
    /// `custom-build` — and `lib` for the library in any crate type.
    pub kind: String,
    /// The target's crate name (`-` → `_`).
    pub crate_name: String,
    /// Its root file (`src_path`, absolute).
    pub src: String,
}

/// One package in the version map.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PackageVersion {
    pub name: String,
    pub version: String,
    /// lib target crate names (`-` → `_`).
    pub crate_names: Vec<String>,
    /// workspace member?
    pub local: bool,
    /// Every target of a workspace member (empty for a dependency: only its
    /// lib is ever compiled). A unit's target is the one whose root file is
    /// rustc's input ([`VersionMap::target_kind`]).
    #[serde(default)]
    pub targets: Vec<TargetDecl>,
    /// The package's `Cargo.toml`.
    #[serde(default)]
    pub manifest_path: String,
    /// Its `[features]`, by name. NOT in the version-map file: every rustc
    /// invocation of a build loads that file, and all features of all
    /// packages are megabytes — each package's list is its own small file
    /// ([`VersionMap::write`] / [`VersionMap::load_features`]).
    #[serde(skip)]
    pub features: Vec<FeatureDecl>,
}

/// Directory (next to the version-map file) of the per-package feature files.
pub const FEATURES_DIR: &str = "crate-features";

fn features_file(versions_file: &Path, name: &str, version: &str) -> PathBuf {
    versions_file.with_file_name(FEATURES_DIR).join(format!("{name}@{}.json", version.replace(['/', '\\', ':'], "_")))
}

/// The feature file of the `name@version` whose manifest is in `manifest_dir`.
fn features_file_at(versions_file: &Path, name: &str, version: &str, manifest_dir: &Path) -> PathBuf {
    let dir = semhash(&manifest_dir.to_string_lossy());
    versions_file.with_file_name(FEATURES_DIR).join(format!("{name}@{}@{dir}.json", version.replace(['/', '\\', ':'], "_")))
}

/// 1-based line of every key of the `[features]` table in a manifest's text.
/// A line scan, not a TOML parse: the table header alone on its line, keys up
/// to the next `[header]`; multi-line arrays only contribute their first line.
pub fn feature_lines(manifest: &str) -> BTreeMap<String, u32> {
    let mut out = BTreeMap::new();
    let mut inside = false;
    for (i, line) in manifest.lines().enumerate() {
        let t = line.trim();
        if t.starts_with('[') {
            inside = t.split('#').next().map(str::trim) == Some("[features]");
            continue;
        }
        if !inside || t.starts_with('#') {
            continue;
        }
        let Some((key, _)) = t.split_once('=') else { continue };
        let key = key.trim().trim_matches(|c| c == '"' || c == '\'');
        let simple = !key.is_empty() && key.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.'));
        if simple {
            out.entry(key.to_string()).or_insert(i as u32 + 1);
        }
    }
    out
}

/// `Cargo.lock` as `(name, version)` → its dependencies `(name, version)`.
/// The lockfile is the one place that names EVERY package a feature could
/// pull in — `cargo metadata` leaves out an optional dependency nothing
/// enables. A line scan of cargo's own generated format (`[[package]]`,
/// `name`, `version`, `dependencies = [ "name", "name version", … ]`); a
/// dependency written without a version has exactly one in the file.
pub fn lock_dependencies(lock: &str) -> BTreeMap<(String, String), Vec<(String, Option<String>)>> {
    let unquote = |v: &str| v.trim().trim_end_matches(',').trim().trim_matches('"').to_string();
    let mut out: BTreeMap<(String, String), Vec<(String, Option<String>)>> = BTreeMap::new();
    let mut versions: BTreeMap<String, Vec<String>> = BTreeMap::new();
    let (mut name, mut version, mut deps, mut in_deps) = (String::new(), String::new(), Vec::new(), false);
    let mut flush = |name: &mut String, version: &mut String, deps: &mut Vec<String>| {
        if !name.is_empty() {
            versions.entry(name.clone()).or_default().push(version.clone());
            out.insert((std::mem::take(name), std::mem::take(version)), std::mem::take(deps).into_iter().map(|d| (d, None)).collect());
        }
        deps.clear();
    };
    for line in lock.lines() {
        let t = line.trim();
        if t == "[[package]]" {
            flush(&mut name, &mut version, &mut deps);
            in_deps = false;
        } else if in_deps {
            if t.starts_with(']') {
                in_deps = false;
            } else if t.starts_with('"') {
                deps.push(unquote(t));
            }
        } else if let Some(v) = t.strip_prefix("name = ") {
            name = unquote(v);
        } else if let Some(v) = t.strip_prefix("version = ") {
            version = unquote(v);
        } else if let Some(v) = t.strip_prefix("dependencies = [") {
            match v.strip_suffix(']') {
                // one line: `dependencies = ["a", "b 1.0"]`
                Some(inline) => deps.extend(inline.split(',').map(unquote).filter(|d| !d.is_empty())),
                None => in_deps = true,
            }
        }
    }
    flush(&mut name, &mut version, &mut deps);
    for list in out.values_mut() {
        for (dep, ver) in list.iter_mut() {
            // `name version (source)` | `name version` | `name`
            let mut parts = dep.split(' ');
            let n = parts.next().unwrap_or_default().to_string();
            *ver = match parts.next() {
                Some(v) => Some(v.to_string()),
                None => match versions.get(&n).map(Vec::as_slice) {
                    Some([one]) => Some(one.clone()),
                    _ => None,
                },
            };
            *dep = n;
        }
    }
    out
}

/// Split one `[features]` value entry into its form, dependency key and feature.
fn split_enable(raw: &str) -> (EnableForm, Option<&str>, Option<&str>) {
    if let Some(dep) = raw.strip_prefix("dep:") {
        return (EnableForm::Dep, Some(dep), None);
    }
    match raw.split_once('/') {
        Some((dep, f)) => match dep.strip_suffix('?') {
            Some(dep) => (EnableForm::WeakDepFeature, Some(dep), Some(f)),
            None => (EnableForm::DepFeature, Some(dep), Some(f)),
        },
        None => (EnableForm::Feature, None, Some(raw)),
    }
}

/// `MIMIR_CRATE_VERSIONS` file contents.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VersionMap {
    pub workspace_root: String,
    pub repo: String,
    pub packages: Vec<PackageVersion>,
    /// `"<pkg> <version>"` → extern crate name → (pkg, version), from cargo's
    /// resolve graph (direct deps only).
    #[serde(default)]
    pub extern_deps: BTreeMap<String, BTreeMap<String, (String, String)>>,
}

impl VersionMap {
    pub fn load(path: &Path) -> Option<VersionMap> {
        serde_json::from_slice(&std::fs::read(path).ok()?).ok()
    }

    /// Write the map to `path` and each package's features to its own file
    /// under [`FEATURES_DIR`] next to it.
    pub fn write(&self, path: &Path) -> std::io::Result<()> {
        std::fs::write(path, serde_json::to_vec_pretty(self)?)?;
        let dir = path.with_file_name(FEATURES_DIR);
        std::fs::create_dir_all(&dir)?;
        for p in self.packages.iter().filter(|p| !p.features.is_empty()) {
            let body = serde_json::to_vec(&p.features)?;
            std::fs::write(features_file(path, &p.name, &p.version), &body)?;
            // … and by its manifest: two packages of one name and version
            // (a path checkout and its published copy) keep their own
            if let Some(dir) = Path::new(&p.manifest_path).parent().filter(|d| !d.as_os_str().is_empty()) {
                std::fs::write(features_file_at(path, &p.name, &p.version, dir), &body)?;
            }
        }
        Ok(())
    }

    /// The declared features of `name@version`, written by [`Self::write`]
    /// next to the map at `path`. Empty when the package declares none.
    pub fn load_features(path: &Path, name: &str, version: &str) -> Vec<FeatureDecl> {
        std::fs::read(features_file(path, name, version)).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or_default()
    }

    /// The declared features of the `name@version` whose manifest is in
    /// `manifest_dir`: one build can hold two packages of one name and version
    /// (a path checkout and its published copy), each with its own manifest.
    pub fn load_features_at(path: &Path, name: &str, version: &str, manifest_dir: &Path) -> Vec<FeatureDecl> {
        match std::fs::read(features_file_at(path, name, version, manifest_dir)) {
            Ok(b) => serde_json::from_slice(&b).unwrap_or_default(),
            Err(_) => Self::load_features(path, name, version),
        }
    }

    /// The cargo target kind of the unit of `package@version` whose root
    /// file is `input` (rustc's input, relative to the cwd or absolute):
    /// [`TargetDecl::kind`]. `None` = not a target this map knows (a
    /// dependency, a file cargo did not declare).
    pub fn target_kind(&self, package: &str, version: &str, input: &Path) -> Option<&str> {
        let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
        let abs = if input.is_absolute() { input.to_path_buf() } else { std::env::current_dir().ok()?.join(input) };
        let input = canon(&abs);
        self.packages
            .iter()
            .filter(|p| p.name == package && p.version == version)
            .flat_map(|p| &p.targets)
            .find(|t| !t.src.is_empty() && canon(Path::new(&t.src)) == input)
            .map(|t| t.kind.as_str())
    }

    /// Is the unit cargo is compiling — `package@version`, with its manifest
    /// in `manifest_dir` (cargo's `CARGO_MANIFEST_DIR`) — a MEMBER of this
    /// workspace?
    ///
    /// By its manifest, not by name and version alone: one build can hold a
    /// member AND the published copy of that same member (`edda`:
    /// `nornir-cli-outcome 0.1.4` is `crates/nornir-cli-outcome` and also comes
    /// from the registry through a dependency that names the published
    /// crate). The copy is a library; calling it local made the dump step see
    /// a "local" dump of a unit cargo calls a dependency. Without a manifest
    /// dir (a run outside cargo) name and version are all there is.
    pub fn is_member(&self, package: &str, version: &str, manifest_dir: Option<&Path>) -> bool {
        let canon = |p: &Path| std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
        self.packages.iter().filter(|p| p.local && p.name == package && p.version == version).any(|p| match (manifest_dir, Path::new(&p.manifest_path).parent()) {
            (Some(dir), Some(own)) if !p.manifest_path.is_empty() => canon(dir) == canon(own),
            _ => true,
        })
    }

    /// Packages whose lib crate is called `crate_name`.
    pub fn by_crate(&self, crate_name: &str) -> Vec<&PackageVersion> {
        self.packages
            .iter()
            .filter(|p| p.crate_names.iter().any(|c| c == crate_name))
            .collect()
    }

    /// Build from `cargo metadata --format-version 1` JSON, reading the
    /// workspace's `Cargo.lock` next to it (see [`Self::with_lock`]).
    pub fn from_cargo_metadata(meta: &serde_json::Value, repo: &str) -> VersionMap {
        let lock = std::fs::read_to_string(Path::new(meta["workspace_root"].as_str().unwrap_or(".")).join("Cargo.lock")).ok();
        Self::with_lock(meta, repo, lock.as_deref())
    }

    /// [`Self::from_cargo_metadata`] with the lockfile's text given: it
    /// resolves the dependencies of `[features]` entries that the metadata's
    /// resolve does not hold (an optional dependency nothing enables).
    pub fn with_lock(meta: &serde_json::Value, repo: &str, lock: Option<&str>) -> VersionMap {
        let members: Vec<&str> = meta["workspace_members"]
            .as_array()
            .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
            .unwrap_or_default();
        let mut packages = Vec::new();
        for p in meta["packages"].as_array().into_iter().flatten() {
            let id = p["id"].as_str().unwrap_or_default();
            let crate_names = p["targets"]
                .as_array()
                .into_iter()
                .flatten()
                .filter(|t| {
                    t["kind"].as_array().into_iter().flatten().any(|k| {
                        matches!(
                            k.as_str(),
                            Some("lib" | "rlib" | "dylib" | "cdylib" | "staticlib" | "proc-macro")
                        )
                    })
                })
                .filter_map(|t| t["name"].as_str().map(|n| n.replace('-', "_")))
                .collect();
            let local = members.contains(&id);
            let targets = p["targets"]
                .as_array()
                .into_iter()
                .flatten()
                .filter(|_| local)
                .filter_map(|t| {
                    let kinds: Vec<&str> = t["kind"].as_array().into_iter().flatten().filter_map(|k| k.as_str()).collect();
                    let kind = if kinds.iter().any(|k| mimir_schema::moniker::LIB_TARGET_KINDS.contains(k)) { "lib" } else { kinds.first().copied()? };
                    Some(TargetDecl {
                        kind: kind.to_string(),
                        crate_name: t["name"].as_str()?.replace('-', "_"),
                        src: t["src_path"].as_str().unwrap_or_default().to_string(),
                    })
                })
                .collect();
            packages.push(PackageVersion {
                name: p["name"].as_str().unwrap_or_default().to_string(),
                version: p["version"].as_str().unwrap_or_default().to_string(),
                crate_names,
                local,
                targets,
                manifest_path: p["manifest_path"].as_str().unwrap_or_default().to_string(),
                features: Vec::new(),
            });
        }
        // package id → (name, version)
        let mut by_id: BTreeMap<&str, (String, String)> = BTreeMap::new();
        for p in meta["packages"].as_array().into_iter().flatten() {
            by_id.insert(
                p["id"].as_str().unwrap_or_default(),
                (
                    p["name"].as_str().unwrap_or_default().to_string(),
                    p["version"].as_str().unwrap_or_default().to_string(),
                ),
            );
        }
        // ---- features: every `[features]` entry resolved to package@version
        let locked = lock.map(lock_dependencies).unwrap_or_default();
        // package id → the dependencies the resolve ACTIVATED: (extern name, package, version)
        let mut activated: BTreeMap<&str, Vec<(&str, &str, &str)>> = BTreeMap::new();
        for n in meta["resolve"]["nodes"].as_array().into_iter().flatten() {
            let Some(id) = n["id"].as_str() else { continue };
            for d in n["deps"].as_array().into_iter().flatten() {
                if let (Some(name), Some((dn, dv))) = (d["name"].as_str(), d["pkg"].as_str().and_then(|i| by_id.get(i))) {
                    activated.entry(id).or_default().push((name, dn.as_str(), dv.as_str()));
                }
            }
        }
        for (pkg, p) in packages.iter_mut().zip(meta["packages"].as_array().into_iter().flatten()) {
            let id = p["id"].as_str().unwrap_or_default();
            let me = (pkg.name.clone(), pkg.version.clone());
            // manifest dependency key (the rename, else the package name) → package name
            let mut dep_keys: BTreeMap<&str, &str> = BTreeMap::new();
            for d in p["dependencies"].as_array().into_iter().flatten() {
                let Some(name) = d["name"].as_str() else { continue };
                dep_keys.entry(d["rename"].as_str().unwrap_or(name)).or_insert(name);
            }
            let resolve_dep = |key: &str| -> (Option<String>, Option<String>) {
                let Some(dep_pkg) = dep_keys.get(key).copied() else { return (None, None) };
                let ext = key.replace('-', "_");
                let act = activated.get(id).map(Vec::as_slice).unwrap_or_default();
                let of_pkg: Vec<&(&str, &str, &str)> = act.iter().filter(|(_, n, _)| *n == dep_pkg).collect();
                let version = match of_pkg.as_slice() {
                    [one] => Some(one.2.to_string()),
                    // the same package twice (two versions under two keys): the extern name decides
                    many if !many.is_empty() => many.iter().find(|(e, ..)| *e == ext).map(|x| x.2.to_string()),
                    // not activated (an optional dependency that is off): the
                    // lockfile still pins it — exact when this package depends
                    // on ONE version of that name
                    _ => {
                        let mine: Vec<&(String, Option<String>)> =
                            locked.get(&me).into_iter().flatten().filter(|(n, _)| n == dep_pkg).collect();
                        match mine.as_slice() {
                            [(_, v)] => v.clone(),
                            _ => None,
                        }
                    }
                };
                (Some(dep_pkg.to_string()), version)
            };
            let lines = std::fs::read_to_string(&pkg.manifest_path).map(|t| feature_lines(&t)).unwrap_or_default();
            for (name, vals) in p["features"].as_object().into_iter().flatten() {
                let mut enables = Vec::new();
                for raw in vals.as_array().into_iter().flatten().filter_map(|v| v.as_str()) {
                    let (form, dep, feature) = split_enable(raw);
                    let (dep_package, dep_version) = dep.map(&resolve_dep).unwrap_or((None, None));
                    enables.push(FeatureEnable { raw: raw.to_string(), form, dep_package, dep_version, feature: feature.map(str::to_string) });
                }
                pkg.features.push(FeatureDecl { name: name.clone(), line: lines.get(name).copied(), enables });
            }
            pkg.features.sort_by(|a, b| a.name.cmp(&b.name));
        }
        let mut extern_deps = BTreeMap::new();
        for n in meta["resolve"]["nodes"].as_array().into_iter().flatten() {
            let Some((pn, pv)) = n["id"].as_str().and_then(|id| by_id.get(id)) else { continue };
            let mut m = BTreeMap::new();
            for d in n["deps"].as_array().into_iter().flatten() {
                if let (Some(name), Some(dep)) =
                    (d["name"].as_str(), d["pkg"].as_str().and_then(|id| by_id.get(id)))
                {
                    m.insert(name.to_string(), dep.clone());
                }
            }
            extern_deps.insert(format!("{pn} {pv}"), m);
        }
        VersionMap {
            workspace_root: meta["workspace_root"].as_str().unwrap_or_default().to_string(),
            repo: repo.to_string(),
            packages,
            extern_deps,
        }
    }
}

/// One sidecar: what cargo told the driver when it compiled one artifact.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Sidecar {
    pub package: String,
    pub version: String,
    pub crate_name: String,
    /// `CARGO_PRIMARY_PACKAGE` was set (a selected workspace member).
    pub primary: bool,
    /// The unit is a workspace member by its manifest
    /// ([`VersionMap::is_member`]) — what a crate that loads this artifact
    /// reads its origin from. Absent in a sidecar written before the field
    /// existed: the reader then falls back to name and version.
    #[serde(default)]
    pub member: Option<bool>,
    /// The unit's `CARGO_MANIFEST_DIR`. `primary` and `member` describe the
    /// build that COMPILED the artifact; a target dir shared by several
    /// workspaces hands the same artifact to another build, so a reader with
    /// a version map decides membership in ITS build by this dir
    /// ([`Sidecar::local_in`]).
    #[serde(default)]
    pub manifest_dir: Option<String>,
}

impl Sidecar {
    /// Is this artifact a member of the build `versions` describes? By its
    /// manifest dir when the sidecar has one and the reader has a map; else
    /// what the compiling build said (`primary` / `member`), else by name and
    /// version.
    pub fn local_in(&self, versions: Option<&VersionMap>) -> bool {
        match (&self.manifest_dir, versions) {
            (Some(dir), Some(m)) => m.is_member(&self.package, &self.version, Some(Path::new(dir))),
            _ => {
                self.primary
                    || self.member.unwrap_or_else(|| versions.is_some_and(|m| m.packages.iter().any(|x| x.local && x.name == self.package && x.version == self.version)))
            }
        }
    }
}

pub fn sidecar_path(index_dir: &Path, key: &str) -> PathBuf {
    index_dir.join(format!("{key}.json"))
}

pub fn write_sidecar(index_dir: &Path, key: &str, s: &Sidecar) -> std::io::Result<()> {
    std::fs::create_dir_all(index_dir)?;
    let p = sidecar_path(index_dir, key);
    let tmp = p.with_extension(format!("json.tmp{}", std::process::id()));
    std::fs::write(&tmp, serde_json::to_vec(s)?)?;
    std::fs::rename(tmp, p)
}

pub fn read_sidecar(index_dir: &Path, key: &str) -> Option<Sidecar> {
    serde_json::from_slice(&std::fs::read(sidecar_path(index_dir, key)).ok()?).ok()
}

/// What a value in a fn body says about WHICH table it is (see the driver's
/// `walk/tables.rs`).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum TableRef {
    /// A name the compiler resolved (a literal, a const, a static).
    Const(String),
    /// Whatever the caller passes as the fn's parameter with this index
    /// (`self` is 0).
    Param(u32),
    /// A runtime value.
    Unknown,
}

/// What ONE fn does with the tables its parameters name — the fact a CALLER
/// of that fn needs, and the only thing about table access that crosses a
/// crate boundary ([`TableSidecar`]).
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct TableSummary {
    /// parameter index → access bits (`mimir_schema::Access::READ` /
    /// `WRITE`): the fn reads / writes the table that parameter names (a
    /// table name, an ident, a table handle, a collection of them)
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub effects: BTreeMap<u32, u8>,
    /// the tables the fn's RETURN value stands for (a handle it opened)
    #[serde(default, skip_serializing_if = "std::collections::BTreeSet::is_empty")]
    pub ret: std::collections::BTreeSet<TableRef>,
}

impl TableSummary {
    pub fn is_empty(&self) -> bool {
        self.effects.is_empty() && self.ret.is_empty()
    }

    /// Union `other` in; `true` when that changed something.
    pub fn absorb(&mut self, other: &TableSummary) -> bool {
        let mut changed = false;
        for (p, bits) in &other.effects {
            let e = self.effects.entry(*p).or_insert(0);
            if *e | bits != *e {
                *e |= bits;
                changed = true;
            }
        }
        for r in &other.ret {
            changed |= self.ret.insert(r.clone());
        }
        changed
    }
}

/// The table summaries of one compiled crate, written next to its sidecar
/// (`<key>.tables.json`) so a dependent crate's compile knows what a call
/// into this crate does with a table argument. Keys are rustc's
/// `DefPathHash` (32 hex digits): the same for the crate that defines an
/// item and for every crate that loads it.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct TableSidecar {
    /// [`TABLE_PASS`] of the driver that wrote it. A sidecar of another
    /// version is not read: its crate counts as not summarised
    /// ([`CrateStats::table_sidecars_stale`]).
    #[serde(default)]
    pub v: u32,
    /// The table pass FAILED on this crate (it panicked): nothing below is
    /// to be trusted, and a dependent crate counts it as not summarised.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
    /// fns / methods of THIS crate
    #[serde(default)]
    pub fns: BTreeMap<String, TableSummary>,
    /// trait methods (of any crate) an impl in THIS crate implements, with
    /// that impl's summary: a call through the trait (`dyn`, a generic) may
    /// land in it
    #[serde(default)]
    pub trait_methods: BTreeMap<String, TableSummary>,
}

/// The version of the table pass's summaries. Bump when what a summary says
/// changes: artifacts cargo keeps (dependencies compiled by an older driver)
/// then count as not summarised until they are compiled again.
pub const TABLE_PASS: u32 = 1;

pub fn table_sidecar_path(index_dir: &Path, key: &str) -> PathBuf {
    index_dir.join(format!("{key}.tables.json"))
}

pub fn write_table_sidecar(index_dir: &Path, key: &str, s: &TableSidecar) -> std::io::Result<()> {
    std::fs::create_dir_all(index_dir)?;
    let p = table_sidecar_path(index_dir, key);
    let tmp = p.with_extension(format!("json.tmp{}", std::process::id()));
    std::fs::write(&tmp, serde_json::to_vec(s)?)?;
    std::fs::rename(tmp, p)
}

/// The sidecar of `key`, if this driver's version of the pass wrote it.
pub fn read_table_sidecar(index_dir: &Path, key: &str) -> Option<TableSidecar> {
    let s: TableSidecar = serde_json::from_slice(&std::fs::read(table_sidecar_path(index_dir, key)).ok()?).ok()?;
    (s.v == TABLE_PASS).then_some(s)
}

/// One crate a compile saw, with the identity rustc mangles into its symbols.
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct CrateId {
    pub crate_name: String,
    /// `tcx.stable_crate_id(cnum)` as 16 lowercase hex digits. v0 mangling puts
    /// it on every path root (`C s<base62(id-2)>_ <name>`), and rustc names the
    /// crate's codegen units `<crate>.<hex>-cgu.N` (the DWARF CU name).
    pub stable_crate_id: String,
    pub package: String,
    pub version: String,
    pub origin: String,
    /// the moniker descriptor prefix of this crate's items
    /// (`moniker::target_prefix`): `` `bin=<crate>`: `` … for a non-lib target
    /// (bin, integration test, bench, example), empty for a lib
    #[serde(default)]
    pub prefix: String,
}

/// `StableCrateId` → the 16-hex form [`CrateId::stable_crate_id`] uses.
pub fn stable_crate_id_hex(id: u64) -> String {
    format!("{id:016x}")
}

/// Per-crate dump record (`<stem>.stats.json` next to the Arrow files).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CrateStats {
    pub crate_name: String,
    pub package: String,
    pub version: String,
    pub hash: String,
    pub origin: String,
    pub crate_types: Vec<String>,
    pub test_harness: bool,
    pub nodes: usize,
    pub nodes_defined: usize,
    /// Semantic edges `(from, to, kind)`.
    pub edges: usize,
    /// Sum of the edges' `count`: distinct sites.
    #[serde(default)]
    pub edge_sites: usize,
    #[serde(default)]
    pub spans: usize,
    pub edges_by_kind: BTreeMap<String, usize>,
    /// edges whose target lives in another crate
    pub cross_crate_edges: usize,
    /// edges from a local (workspace) crate to a library / lang crate
    pub cross_repo_edges: usize,
    /// crates whose version could not be resolved (version "?")
    pub unresolved_versions: Vec<String>,
    /// how each extern crate's version was resolved: sidecar / map / lang / unresolved
    pub version_sources: BTreeMap<String, usize>,
    /// locally defined nodes that got a moniker another local node already had
    #[serde(default)]
    pub rustc_release: String,
    pub moniker_collisions: usize,
    #[serde(default)]
    pub collision_samples: Vec<String>,
    pub walk_ms: u128,
    pub write_ms: u128,
    pub nodes_file: String,
    pub edges_file: String,
    /// `code_spans` rows: node spans + positioned edge sites.
    #[serde(default)]
    pub spans_file: String,
    /// [`semhash`] of the unit's [`Invocation::semkey`]: a dump with the same
    /// one says the same.
    #[serde(default)]
    pub semhash: String,
    /// this crate's StableCrateId (16 hex), see [`CrateId`]
    #[serde(default)]
    pub stable_crate_id: String,
    /// every crate this compile loaded, local first: the exact
    /// StableCrateId → package@version map for an artifact built by this compile
    #[serde(default)]
    pub crate_ids: Vec<CrateId>,
    /// `-C symbol-mangling-version` of this compile (`v0` / `legacy` / `hashed`)
    #[serde(default)]
    pub symbol_mangling: String,
    /// rows in `symbols_file` (rustc's symbol name per non-generic local item)
    #[serde(default)]
    pub symbols: usize,
    #[serde(default)]
    pub symbols_file: String,
    /// symbol-name computations that panicked (item skipped, counted)
    #[serde(default)]
    pub symbol_errors: usize,
    #[serde(default)]
    pub symbols_ms: u128,
    /// defined nodes per `visibility` value (`pub(in …)` folded into one key)
    #[serde(default)]
    pub nodes_by_visibility: BTreeMap<String, usize>,
    /// defined nodes per `effective_visibility` value
    #[serde(default)]
    pub nodes_by_effective_visibility: BTreeMap<String, usize>,
    /// the nameable API (`direct` / `reexported`) in rustc's `missing_docs`
    /// sense: no impl blocks, no items of a trait impl, no generic params
    #[serde(default)]
    pub api_items: usize,
    /// … of which carry a doc comment
    #[serde(default)]
    pub api_documented: usize,
    /// defined nodes that carry a doc comment (any visibility)
    #[serde(default)]
    pub documented_nodes: usize,
    /// feature nodes the package's manifest declares
    #[serde(default)]
    pub feature_nodes: usize,
    /// … of which this compile was built with (`--cfg feature="…"`)
    #[serde(default)]
    pub features_active: usize,
    /// feature nodes only referenced (a dependency's feature; a `feature = "…"`
    /// in a cfg that no manifest declares)
    #[serde(default)]
    pub feature_refs: usize,
    /// `[features]` entries whose dependency could not be resolved to a
    /// package@version (no ENABLES edge written for them)
    #[serde(default)]
    pub feature_enables_unresolved: Vec<String>,
    /// GATED_BY edges from items the compile kept, by their OWN `#[cfg]`
    /// (rustc's cfg evaluation; `partial = false`)
    #[serde(default)]
    pub gated_by_active: usize,
    /// GATED_BY edges with `partial = true`: a feature gates something INSIDE
    /// the item (a statement, an expression, a `use`, a macro call, `cfg!`)
    #[serde(default)]
    pub gated_by_partial: usize,
    /// `cfg_gated_out` stub nodes (token scan; rustc never saw these items)
    #[serde(default)]
    pub gated_out_stubs: usize,
    /// GATED_BY edges from those stubs, by their own `#[cfg]`
    #[serde(default)]
    pub gated_by_stubs: usize,
    /// source files the cfg scan read (rustc's own + compiled-out module files)
    #[serde(default)]
    pub cfg_scan_files: usize,
    /// … that did not lex (no stubs from them)
    #[serde(default)]
    pub cfg_scan_failed: Vec<String>,
    /// compiled-out `mod x;` whose file was not found
    #[serde(default)]
    pub cfg_scan_unresolved_mods: Vec<String>,
    /// every `feature = "…"` atom in a cfg context of the scanned files
    /// (`#[cfg]`, `#![cfg]`, `cfg!`, the predicate of `#[cfg_attr]`)
    #[serde(default)]
    pub feature_cfg_mentions: usize,
    /// … of which sit on an item's own `#[cfg]` (a GATED_BY site, `partial =
    /// false`)
    #[serde(default)]
    pub feature_cfg_attributed: usize,
    /// … of which gate a PART of an item (a GATED_BY site, `partial = true`).
    /// `mentions − attributed − partial` is what is NOT an edge: the
    /// predicates of `cfg_attr`. GATED_BY is a lower bound by that much (plus
    /// the files in `cfg_scan_failed`).
    #[serde(default)]
    pub feature_cfg_partial: usize,
    #[serde(default)]
    pub cfg_scan_ms: u128,
    /// the table pass ran: the storage engine's crate is this crate or one
    /// it (transitively) depends on. `false` = no TOUCHES_TABLE edge can come
    /// from this crate, and none was looked for.
    #[serde(default)]
    pub table_pass: bool,
    /// the table pass panicked: this crate's TOUCHES_TABLE edges are missing
    #[serde(default)]
    pub table_pass_error: Option<String>,
    /// places where a table meets a read / write (the sum of the
    /// TOUCHES_TABLE edges' `count`, before sites on one line fold)
    #[serde(default)]
    pub table_sites: usize,
    /// … of which name no table the compiler could resolve (edges to the
    /// `<unresolved>` table node)
    #[serde(default)]
    pub table_sites_unresolved: usize,
    /// the tables this crate's fns touch by name
    #[serde(default)]
    pub tables: Vec<String>,
    /// fns of this crate with a table summary (a parameter that names a
    /// table, or a table handle returned)
    #[serde(default)]
    pub table_summaries: usize,
    /// fn bodies the pass looked at (the callers of a summarised fn, to a
    /// fixpoint), and how many looks that took
    #[serde(default)]
    pub table_bodies: usize,
    #[serde(default)]
    pub table_body_visits: usize,
    /// dependency crates whose table sidecar was read / crates in the closure
    #[serde(default)]
    pub table_sidecars: (usize, usize),
    /// dependency crates (not the toolchain's) with NO sidecar of this
    /// driver's [`TABLE_PASS`]: compiled by another driver, or without the
    /// sidecar index. What they do with a table is unknown here — the
    /// TOUCHES_TABLE edges of this crate are a lower bound until they are
    /// compiled again (a fresh target dir).
    #[serde(default)]
    pub table_sidecars_stale: Vec<String>,
    #[serde(default)]
    pub tables_ms: u128,
    /// `#[mimir::table(…)]` attributes on this crate's own fns / methods
    #[serde(default)]
    pub table_annotations: usize,
    /// … of which say something this driver does not understand (another
    /// access word, a name that is no parameter of the fn): NO summary came
    /// of them
    #[serde(default)]
    pub table_annotations_bad: Vec<String>,
    /// The MIR fact format this dump carries ([`MIR_FACTS`]); 0 = a dump
    /// from a driver before MIR facts (no `mir_body` / `mir_block` nodes).
    #[serde(default)]
    pub mir_facts: u32,
    /// `full` (block nodes + CFG_SUCC edges + the body node), `fingerprint`
    /// (the body node only) or `off` — [`ENV_MIR`] and the crate's origin.
    #[serde(default)]
    pub mir_mode: String,
    /// fn / method bodies with MIR facts (closures folded into their fn).
    #[serde(default)]
    pub mir_bodies: usize,
    /// … closure / coroutine bodies among them, folded into their fn.
    #[serde(default)]
    pub mir_nested_bodies: usize,
    /// Basic blocks described (cleanup blocks excluded).
    #[serde(default)]
    pub mir_blocks: usize,
    /// MIR statements written (full mode; storage markers, fake reads and
    /// retags excluded).
    #[serde(default)]
    pub mir_statements: usize,
    /// Bodies whose MIR was gone (stolen) before the walk: no facts.
    #[serde(default)]
    pub mir_missing: usize,
    /// Bytes of MIR payload (the `ty` column of `mir_body` / `mir_block`).
    #[serde(default)]
    pub mir_payload_bytes: usize,
    #[serde(default)]
    pub mir_ms: u128,
    /// The MIR pass failed on this crate (its facts are left out).
    #[serde(default)]
    pub mir_error: Option<String>,
}

/// The MIR fact format the driver writes (see `walk/mir.rs`): `mir_body` /
/// `mir_block` PART nodes and `CFG_SUCC` edges, the payload in `ty`. Carried
/// per crate in `.stats.json` (`mir_facts`); a dump without it (0) simply has
/// no MIR facts. Bumped only when the payload grammar changes. NOT part of the
/// lake schema ([`mimir_schema::SCHEMA_VERSION`] is unchanged: no column was
/// added) and not of any unit hash, so a new driver forces no re-dump.
pub const MIR_FACTS: u32 = mimir_schema::mir::FACTS_VERSION;

/// Env: which crates get MIR facts. `local` (default) = full facts for the
/// workspace's own crates, block fingerprints only (one `mir_body` node per
/// fn) for dependencies; `all` = full facts everywhere; `fingerprint` =
/// fingerprints everywhere; `off` = none.
pub const ENV_MIR: &str = "MIMIR_MIR";

/// A dumped table read back as rows of strings (null → empty), keyed by column
/// name — for tests and `cargo mimir-dump --cat`.
#[derive(Debug, Clone, Default)]
pub struct Table {
    pub columns: Vec<String>,
    pub rows: Vec<Vec<String>>,
}

impl Table {
    pub fn col(&self, name: &str) -> usize {
        self.columns.iter().position(|c| c == name).unwrap_or_else(|| panic!("no column {name}"))
    }
}

/// Read every `*.<suffix>.arrow` file in `dir` (suffix = `nodes` / `edges`).
pub fn read_tables(dir: &Path, suffix: &str) -> std::io::Result<Table> {
    use arrow_array::Array;
    use arrow_array::cast::AsArray;
    let mut t = Table::default();
    let mut files: Vec<PathBuf> = std::fs::read_dir(dir)?
        .flatten()
        .map(|e| e.path())
        .filter(|p| p.to_string_lossy().ends_with(&format!(".{suffix}.arrow")))
        .collect();
    files.sort();
    for f in files {
        let r = arrow_ipc::reader::FileReader::try_new(std::fs::File::open(&f)?, None)
            .map_err(std::io::Error::other)?;
        let schema = r.schema();
        if t.columns.is_empty() {
            t.columns = schema.fields().iter().map(|f| f.name().clone()).collect();
        }
        for b in r {
            let b = b.map_err(std::io::Error::other)?;
            let cols = b.columns();
            for i in 0..b.num_rows() {
                let row = cols
                    .iter()
                    .map(|c| {
                        if c.is_null(i) {
                            return String::new();
                        }
                        match c.data_type() {
                            arrow_schema::DataType::Utf8 => c.as_string::<i32>().value(i).to_string(),
                            arrow_schema::DataType::UInt32 => {
                                c.as_primitive::<arrow_array::types::UInt32Type>().value(i).to_string()
                            }
                            arrow_schema::DataType::Boolean => c.as_boolean().value(i).to_string(),
                            _ => "?".into(),
                        }
                    })
                    .collect();
                t.rows.push(row);
            }
        }
    }
    Ok(t)
}

#[cfg(test)]
mod tests {

    /// A target dir shared by several workspaces hands one artifact to many
    /// builds: whether a crate is a MEMBER is the reading build's question
    /// The semantic key of a unit: its cfgs (features) and code-changing
    /// options are in it, in any order; its extra-filename (the hash cargo
    /// mixes its dependencies into) is not.

    /// `MIMIR_DUMP=off`: no out dir, whatever `MIMIR_OUT` says.


    /// A generated file under the unit's OUT_DIR is named without the hash
    /// of the dir (`OUT_DIR/private.rs`); anything else is not touched.

    /// Paths are cut to their last segment, nothing else is touched.
    #[test]
    fn printed_types_keep_only_the_last_path_segment() {
        let cut = crate::shorten_paths;
        assert_eq!(cut("std::collections::BTreeMap<u32, clean::Point>"), "BTreeMap<u32, Point>");
        assert_eq!(cut("&'a mut alloc::vec::Vec<(u8, core::option::Option<std::string::String>)>"), "&'a mut Vec<(u8, Option<String>)>");
        assert_eq!(cut("<T as core::iter::Iterator>::Item"), "<T as Iterator>::Item");
        assert_eq!(cut("dyn std::ops::Fn(u8) -> u8 + Send"), "dyn Fn(u8) -> u8 + Send");
        assert_eq!(cut("[u8; 4]"), "[u8; 4]");
        assert_eq!(cut("{closure@src/lib.rs:3:9: 3:11}"), "{closure@src/lib.rs:3:9: 3:11}");
        assert_eq!(cut("Point"), "Point");
    }

    #[test]
    fn a_generated_file_is_named_from_out_dir() {
        let out = std::path::Path::new("/t/debug/build/serde/b82cacc85af76fd3/out");
        assert_eq!(crate::out_dir_relative(std::path::Path::new("/t/debug/build/serde/b82cacc85af76fd3/out/private.rs"), Some(out)).as_deref(), Some("OUT_DIR/private.rs"));
        assert_eq!(crate::out_dir_relative(std::path::Path::new("/r/serde/src/lib.rs"), Some(out)), None);
        assert_eq!(crate::out_dir_relative(std::path::Path::new("/t/debug/build/serde/b82cacc85af76fd3/out/private.rs"), None), None);
    }

    /// The full semantic key: the same unit with its dependency at ANOTHER
    /// version, or a feature's optional dependency resolved to another
    /// version, is another key (its rows differ); the same versions under
    /// other artifact hashes are one key.
    #[test]
    fn a_twin_whose_dependency_is_at_another_version_has_another_key() {
        let tmp = std::env::temp_dir().join(format!("mimir-unit-semkey-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);
        let index = tmp.join("index");
        let side = |key: &str, version: &str| {
            crate::write_sidecar(&index, key, &crate::Sidecar { package: "serde_core".into(), version: version.into(), crate_name: "serde_core".into(), primary: false, member: None, manifest_dir: None }).unwrap();
        };
        side("serde_core-aaaa", "1.0.228");
        side("serde_core-bbbb", "1.0.229");
        side("serde_core-cccc", "1.0.229");
        let inv = |dep: &str, extra: &str| {
            let a: Vec<String> = ["--crate-name", "serde", "--crate-type", "lib", "src/lib.rs", "-C", &format!("extra-filename=-{extra}"), "--extern", &format!("serde_core=/t/deps/libserde_core-{dep}.rmeta")].iter().map(|s| s.to_string()).collect();
            Invocation::parse(&a)
        };
        let key = |i: &Invocation, vf: Option<&std::path::Path>| crate::unit_semkey(i, "serde", "1.0.229", "/r/serde", Some(&index), vf);
        assert_ne!(key(&inv("aaaa", "1"), None), key(&inv("bbbb", "2"), None), "serde_core 1.0.228 vs 1.0.229: other edges, another key");
        assert_eq!(key(&inv("bbbb", "1"), None), key(&inv("cccc", "2"), None), "the same versions under other hashes: one key");
        assert_ne!(key(&inv("dddd", "1"), None), key(&inv("eeee", "2"), None), "no sidecar: never equal");
        // the reading repo's lockfile resolves an optional dependency
        let features = |dep_version: &str| {
            let vf = tmp.join(format!("v{dep_version}/crate-versions.json"));
            let map = VersionMap {
                packages: vec![PackageVersion {
                    name: "serde".into(),
                    version: "1.0.229".into(),
                    crate_names: vec!["serde".into()],
                    local: false,
                    targets: vec![],
                    manifest_path: "/r/serde/Cargo.toml".into(),
                    features: vec![FeatureDecl { name: "rc".into(), line: Some(3), enables: vec![FeatureEnable { raw: "dep:arc".into(), form: EnableForm::Dep, dep_package: Some("arc".into()), dep_version: Some(dep_version.into()), feature: None }] }],
                }],
                ..Default::default()
            };
            std::fs::create_dir_all(vf.parent().unwrap()).unwrap();
            map.write(&vf).unwrap();
            vf
        };
        let (v1, v2) = (features("0.1.0"), features("0.2.0"));
        assert_ne!(key(&inv("bbbb", "1"), Some(&v1)), key(&inv("bbbb", "1"), Some(&v2)), "an optional dependency at another version");
        let _ = std::fs::remove_dir_all(&tmp);
    }

    /// Two packages of one name and version (a path checkout and its
    /// published copy) each keep their own features, found by the manifest.
    #[test]
    fn features_are_found_by_the_packages_manifest() {
        let tmp = std::env::temp_dir().join(format!("mimir-features-at-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&tmp);
        let pkg = |dir: &str, line: u32| PackageVersion {
            name: "rotaryengine".into(),
            version: "0.1.13".into(),
            crate_names: vec!["gatling".into()],
            local: false,
            targets: vec![],
            manifest_path: format!("{dir}/Cargo.toml"),
            features: vec![FeatureDecl { name: "testmatrix".into(), line: Some(line), enables: vec![] }],
        };
        let map = VersionMap { packages: vec![pkg("/clones/zz/gatling", 30), pkg("/registry/rotaryengine-0.1.13", 29)], ..Default::default() };
        let vf = tmp.join("crate-versions.json");
        std::fs::create_dir_all(&tmp).unwrap();
        map.write(&vf).unwrap();
        let line = |dir: &str| VersionMap::load_features_at(&vf, "rotaryengine", "0.1.13", std::path::Path::new(dir)).first().and_then(|f| f.line);
        assert_eq!(line("/clones/zz/gatling"), Some(30));
        assert_eq!(line("/registry/rotaryengine-0.1.13"), Some(29));
        let _ = std::fs::remove_dir_all(&tmp);
    }

    #[test]
    fn dump_off_writes_no_dump() {
        let out = || Some(std::ffi::OsString::from("/o"));
        assert_eq!(crate::dump_out(out(), None), Some(std::path::PathBuf::from("/o")));
        assert_eq!(crate::dump_out(out(), Some("workspace")), Some(std::path::PathBuf::from("/o")));
        assert_eq!(crate::dump_out(out(), Some("off")), None);
        assert_eq!(crate::dump_out(None, None), None);
    }

    #[test]
    fn the_semantic_key_is_the_unit_without_its_dependency_hashes() {
        let argv = |extra: &str, cfgs: &[&str]| -> Vec<String> {
            let mut v: Vec<String> = ["--crate-name", "serde", "--crate-type", "lib", "src/lib.rs", "-C", &format!("extra-filename=-{extra}"), "-C", "opt-level=0"].iter().map(|s| s.to_string()).collect();
            for c in cfgs {
                v.push("--cfg".into());
                v.push(c.to_string());
            }
            v
        };
        let key = |a: Vec<String>| Invocation::parse(&a).semkey("serde", "1.0.0", "/r/serde-1.0.0");
        let a = key(argv("aaaa", &["feature=\"std\"", "feature=\"derive\""]));
        assert_eq!(a, key(argv("bbbb", &["feature=\"derive\"", "feature=\"std\""])), "another dependency hash, same features: one unit");
        assert_ne!(a, key(argv("aaaa", &["feature=\"std\""])), "another feature set: another unit");
        assert!(a.contains("opt-level=0") && a.contains("feature=\"derive\""), "{a}");
        assert_eq!(semhash(&a), semhash(&a.clone()));
        assert_eq!(semhash(&a).len(), 16);
    }

    /// (its version map, by manifest dir), not what the compiling build said.
    #[test]
    fn a_sidecar_is_local_by_the_reading_builds_map() {
        let pkg = |name: &str, local: bool, manifest: &str| PackageVersion {
            name: name.into(),
            version: "0.7.0".into(),
            crate_names: vec![name.into()],
            local,
            targets: vec![],
            manifest_path: manifest.into(),
            features: vec![],
        };
        let skade_build = VersionMap { workspace_root: "/c/skade".into(), repo: "skade".into(), packages: vec![pkg("skade", true, "/c/skade/skade/Cargo.toml")], ..Default::default() };
        let nornir_build = VersionMap { workspace_root: "/c/nornir".into(), repo: "nornir".into(), packages: vec![pkg("skade", false, "/c/skade/skade/Cargo.toml")], ..Default::default() };
        // compiled in skade's own build: primary, a member there
        let sc = Sidecar { package: "skade".into(), version: "0.7.0".into(), crate_name: "skade".into(), primary: true, member: Some(true), manifest_dir: Some("/c/skade/skade".into()) };
        assert!(sc.local_in(Some(&skade_build)));
        assert!(!sc.local_in(Some(&nornir_build)), "read in nornir's build it is a dependency");
        // a sidecar written before the field: what the compiling build said
        let old = Sidecar { manifest_dir: None, ..sc.clone() };
        assert!(old.local_in(Some(&nornir_build)));
    }

    use super::{feature_lines, lock_dependencies, semhash, EnableForm, FeatureDecl, FeatureEnable, Invocation, PackageVersion, Sidecar, VersionMap, NOHASH};

    /// A member and the PUBLISHED copy of that member in one build (what
    /// `edda` holds of `nornir-cli-outcome 0.1.4`): the unit compiled from the
    /// member's own manifest is a member, the one from the registry is not —
    /// same name, same version.
    ///
    /// RED WHEN REVERTED: decide by name and version alone and the registry
    /// copy is a member too.
    #[test]
    fn a_member_is_told_from_its_published_copy_by_its_manifest() {
        let meta: serde_json::Value = serde_json::from_str(
            r#"{
              "workspace_root": "/nonexistent/ws",
              "workspace_members": ["own-id"],
              "packages": [
                {"id": "own-id", "name": "outcome", "version": "0.1.4", "manifest_path": "/nonexistent/ws/crates/outcome/Cargo.toml",
                 "targets": [{"kind": ["lib"], "name": "outcome"}], "dependencies": [], "features": {}},
                {"id": "registry-id", "name": "outcome", "version": "0.1.4", "manifest_path": "/nonexistent/registry/outcome-0.1.4/Cargo.toml",
                 "targets": [{"kind": ["lib"], "name": "outcome"}], "dependencies": [], "features": {}}
              ],
              "resolve": {"nodes": [{"id": "own-id", "deps": []}, {"id": "registry-id", "deps": []}]}
            }"#,
        )
        .unwrap();
        let m = VersionMap::from_cargo_metadata(&meta, "ws");
        let at = |dir: &str| m.is_member("outcome", "0.1.4", Some(std::path::Path::new(dir)));
        assert!(at("/nonexistent/ws/crates/outcome"), "compiled from the member's own manifest");
        assert!(!at("/nonexistent/registry/outcome-0.1.4"), "the published copy is a library");
        assert!(m.is_member("outcome", "0.1.4", None), "no manifest dir: name and version are all there is");
        assert!(!m.is_member("outcome", "0.1.5", Some(std::path::Path::new("/nonexistent/ws/crates/outcome"))));
    }

    /// What `cargo metadata` prints for: member `b` with deps `a` (0.1.0),
    /// `a_old` (= package `a` 0.0.9) and the OPTIONAL `c` (off, so `c` is in
    /// neither `packages` nor the resolve — only in the lockfile).
    const META: &str = r#"{
      "workspace_root": "/nonexistent/ws",
      "workspace_members": ["b-id"],
      "packages": [
        {"id": "a-id", "name": "a", "version": "0.1.0", "manifest_path": "/nonexistent/ws/a/Cargo.toml",
         "targets": [{"kind": ["lib"], "name": "a"}], "dependencies": [],
         "features": {"default": ["on"], "on": [], "extra": ["on"]}},
        {"id": "a-old-id", "name": "a", "version": "0.0.9", "manifest_path": "/nonexistent/ws/a-old/Cargo.toml",
         "targets": [{"kind": ["lib"], "name": "a"}], "dependencies": [], "features": {"legacy": []}},
        {"id": "b-id", "name": "b", "version": "0.2.0", "manifest_path": "/nonexistent/ws/b/Cargo.toml",
         "targets": [{"kind": ["lib"], "name": "b"}, {"kind": ["bin"], "name": "bctl"}],
         "dependencies": [
           {"name": "a", "rename": null, "optional": false},
           {"name": "a", "rename": "a_old", "optional": false},
           {"name": "c", "rename": null, "optional": true},
           {"name": "gone", "rename": null, "optional": true}],
         "features": {"fast": ["a/extra"], "old": ["a_old/legacy"], "with-c": ["dep:c"], "c-turbo": ["c?/turbo"],
                      "c": ["dep:c"], "lost": ["dep:gone"], "nodep": ["nothing/x"]}}
      ],
      "resolve": {"nodes": [
        {"id": "a-id", "deps": []},
        {"id": "a-old-id", "deps": []},
        {"id": "b-id", "deps": [{"name": "a", "pkg": "a-id"}, {"name": "a_old", "pkg": "a-old-id"}]}
      ]}
    }"#;

    const LOCK: &str = "version = 4\n\n[[package]]\nname = \"a\"\nversion = \"0.0.9\"\n\n[[package]]\nname = \"a\"\nversion = \"0.1.0\"\n\n[[package]]\nname = \"b\"\nversion = \"0.2.0\"\ndependencies = [\n \"a 0.0.9\",\n \"a 0.1.0\",\n \"c\",\n]\n\n[[package]]\nname = \"c\"\nversion = \"0.3.0\"\n";

    #[test]
    fn the_lockfile_names_every_dependency_with_its_version() {
        let l = lock_dependencies(LOCK);
        assert_eq!(
            l[&("b".to_string(), "0.2.0".to_string())],
            vec![("a".to_string(), Some("0.0.9".to_string())), ("a".to_string(), Some("0.1.0".to_string())), ("c".to_string(), Some("0.3.0".to_string()))]
        );
        assert!(l[&("c".to_string(), "0.3.0".to_string())].is_empty());
        // a registry entry: `name version (source)`
        let l = lock_dependencies("[[package]]\nname = \"x\"\nversion = \"1.0.0\"\ndependencies = [\n \"y 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)\",\n]\n");
        assert_eq!(l[&("x".to_string(), "1.0.0".to_string())], vec![("y".to_string(), Some("2.0.0".to_string()))]);
    }

    #[test]
    fn feature_entries_resolve_to_package_and_version() {
        let meta: serde_json::Value = serde_json::from_str(META).unwrap();
        let m = VersionMap::with_lock(&meta, "fixture", Some(LOCK));
        let b = m.packages.iter().find(|p| p.name == "b").unwrap();
        let get = |f: &str| {
            let e = &b.features.iter().find(|x| x.name == f).unwrap_or_else(|| panic!("no feature {f}")).enables[0];
            (e.form, e.dep_package.clone().unwrap_or_default(), e.dep_version.clone(), e.feature.clone().unwrap_or_default())
        };
        // an ACTIVATED dependency: the resolve graph, the rename told apart by its extern name
        assert_eq!(get("fast"), (EnableForm::DepFeature, "a".into(), Some("0.1.0".into()), "extra".into()));
        assert_eq!(get("old"), (EnableForm::DepFeature, "a".into(), Some("0.0.9".into()), "legacy".into()));
        // an optional dependency that is OFF: not in the metadata at all — the lockfile pins it
        assert_eq!(get("with-c"), (EnableForm::Dep, "c".into(), Some("0.3.0".into()), String::new()));
        assert_eq!(get("c-turbo"), (EnableForm::WeakDepFeature, "c".into(), Some("0.3.0".into()), "turbo".into()));
        // not in the lockfile either: a known package, NO version — never a guess
        assert_eq!(get("lost"), (EnableForm::Dep, "gone".into(), None, String::new()));
        // no dependency of that key at all
        assert_eq!(get("nodep"), (EnableForm::DepFeature, String::new(), None, "x".into()));
        let a = m.packages.iter().find(|p| p.name == "a" && p.version == "0.1.0").unwrap();
        let extra = a.features.iter().find(|f| f.name == "extra").unwrap();
        assert_eq!((extra.enables[0].form, extra.enables[0].feature.as_deref()), (EnableForm::Feature, Some("on")));
        // features come out sorted by name (a stable dump)
        assert_eq!(a.features.iter().map(|f| f.name.as_str()).collect::<Vec<_>>(), ["default", "extra", "on"]);

        // the features travel in per-package files, not in the map every
        // rustc invocation loads
        let dir = std::env::temp_dir().join(format!("mimir-vmap-{}", std::process::id()));
        std::fs::create_dir_all(&dir).unwrap();
        let file = dir.join("crate-versions.json");
        m.write(&file).unwrap();
        let back = VersionMap::load(&file).unwrap();
        assert!(back.packages.iter().all(|p| p.features.is_empty()), "the map file carries no features");
        assert!(!std::fs::read_to_string(&file).unwrap().contains("dep_version"));
        assert_eq!(VersionMap::load_features(&file, "b", "0.2.0"), b.features);
        assert_eq!(VersionMap::load_features(&file, "a", "0.0.9").len(), 1, "two versions of one name: two files");
        assert!(VersionMap::load_features(&file, "nobody", "1.0.0").is_empty());
        std::fs::remove_dir_all(&dir).unwrap();

        // WITHOUT the lockfile the off dependency stays unresolved (red-proof
        // of the lockfile read)
        let no_lock = VersionMap::with_lock(&meta, "fixture", None);
        let b = no_lock.packages.iter().find(|p| p.name == "b").unwrap();
        assert_eq!(b.features.iter().find(|x| x.name == "with-c").unwrap().enables[0].dep_version, None);
    }

    #[test]
    fn feature_lines_are_the_keys_of_the_features_table_only() {
        let toml = "[package]\nname = \"a\"\ndefault = 1\n\n[features] # the table\ndefault = [\"on\"]\n# on = []\non = []\n\"quoted-one\" = [\n  \"on\",\n]\n\n[dependencies]\nextra = \"1\"\n";
        let l = feature_lines(toml);
        assert_eq!(l.get("default"), Some(&6));
        assert_eq!(l.get("on"), Some(&8));
        assert_eq!(l.get("quoted-one"), Some(&9));
        assert_eq!(l.get("extra"), None, "a key of another table is not a feature");
        assert_eq!(l.len(), 3);
    }

    fn argv(s: &str) -> Vec<String> {
        s.split_whitespace().map(str::to_string).collect()
    }

    #[test]
    fn the_file_stem_is_unique_per_unit_also_without_an_extra_filename() {
        let check = Invocation::parse(&argv("--crate-name bctl --crate-type bin -C metadata=1af1 -C extra-filename=-e793 src/main.rs"));
        assert_eq!(check.short_hash(), "e793");
        // `cargo build --release`: an uplifted bin has NO extra-filename. Two
        // such bins with one crate name + version (two packages, two profiles,
        // two targets) used to share the stem `nohash` and overwrite each other.
        let a = Invocation::parse(&argv("--crate-name bctl --crate-type bin -C metadata=a763 src/main.rs"));
        let b = Invocation::parse(&argv("--crate-name bctl --crate-type bin -Cmetadata=bfd0 src/main.rs"));
        assert_eq!((a.short_hash().as_str(), b.short_hash().as_str()), ("ma763", "mbfd0"));
        assert_ne!(a.short_hash(), b.short_hash());
        // a build-script probe (no cargo unit): skipped by consumers
        let probe = Invocation::parse(&argv("--crate-name anyhow --crate-type lib probe.rs"));
        assert!(probe.short_hash().starts_with(NOHASH));
    }
}