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

nordisk/edda

crates/nornir-build-thing/examples/kconfig_why.rs

//! **The measuring door for the kconfig solver** (lane AP, 2026-08-30).
//!
//! ```text
//! cargo run -p nornir-build-thing --example kconfig_why -- \
//!     <linux-src> <fragment.config> <built .config> <base .config> [--list-unexplained]
//! ```
//!
//! Prints the real per-class counts, the ratio they represent, how many derived
//! symbols now carry a chain, and — by name — the ones that do not. It exists
//! so the numbers in a report are read off a run rather than off a belief.

use std::collections::BTreeMap;
use std::path::PathBuf;

use nornir_build_thing::kconfig_graph::{Cause, KconfigGraph, Solver, Unexplained, kconfig_env};
use nornir_build_thing::kernel_requires::{KernelRequirements, WhyIndex};

fn main() -> anyhow::Result<()> {
    let a: Vec<String> = std::env::args().skip(1).collect();
    if a.len() < 4 {
        eprintln!("usage: kconfig_why <linux-src> <fragment> <built.config> <base.config> [--list-unexplained] [--chains N] [--heads]");
        std::process::exit(2);
    }
    let src = PathBuf::from(&a[0]);
    let fragment = std::fs::read_to_string(&a[1])?;
    let built = std::fs::read_to_string(&a[2])?;
    let base = std::fs::read_to_string(&a[3])?;
    let list = a.iter().any(|x| x == "--list-unexplained");
    let heads = a.iter().any(|x| x == "--heads");
    let dump = a.iter().any(|x| x == "--dump");
    let chains: usize = a
        .windows(2)
        .find(|w| w[0] == "--chains")
        .and_then(|w| w[1].parse().ok())
        .unwrap_or(0);

    let t0 = std::time::Instant::now();
    let g = KconfigGraph::read(&src, kconfig_env("x86_64", "6.12.104"))?;
    let read_ms = t0.elapsed().as_millis();
    eprintln!(
        "graph: {} Kconfig files, {} symbols, {} select edges, {} imply edges, {read_ms} ms",
        g.files_read.len(),
        g.symbols.len(),
        g.selected_by.values().map(Vec::len).sum::<usize>(),
        g.implied_by.values().map(Vec::len).sum::<usize>(),
    );

    for d in g.dangling_declarations(&fragment) {
        println!("DANGLING {}", d.refusal());
    }
    let s = Solver::new(&g, &fragment, &built, &base);
    let derived = s.derived();
    let live = s.config.values.values().filter(|v| v.as_str() != "n").count();
    println!("live symbols in the built .config : {live}");
    println!("named by the merged fragment      : {}", s.declared.len());
    println!(
        "kconfig-derived                   : {} ({:.1} % of live)",
        derived.len(),
        derived.len() as f64 * 100.0 / live as f64
    );

    let t1 = std::time::Instant::now();
    let all = s.explain_all();
    let solve_ms = t1.elapsed().as_millis();

    let mut by_head_class: BTreeMap<&str, usize> = BTreeMap::new();
    let mut unex: BTreeMap<String, Unexplained> = BTreeMap::new();
    let mut declared_head_counts: BTreeMap<String, usize> = BTreeMap::new();
    let mut shown = 0usize;
    for (sym, c) in &all {
        match c {
            Cause::Explained { chain, heads } => {
                *by_head_class.entry(chain.head.class()).or_default() += 1;
                for h in heads {
                    if h.class() == "declared" {
                        *declared_head_counts.entry(h.name().to_string()).or_default() += 1;
                    }
                }
                if shown < chains {
                    println!("  {}", chain.render());
                    shown += 1;
                }
            }
            Cause::Unexplained(u) => {
                unex.insert(sym.clone(), u.clone());
            }
        }
    }
    println!("explained                         : {} ({:.1} %) in {solve_ms} ms", all.len() - unex.len(), (all.len() - unex.len()) as f64 * 100.0 / all.len() as f64);
    for (k, v) in &by_head_class {
        println!("    head = {k:<16} {v}");
    }
    println!("UNEXPLAINED                       : {} ({:.1} %)", unex.len(), unex.len() as f64 * 100.0 / all.len() as f64);
    let mut kinds: BTreeMap<&str, usize> = BTreeMap::new();
    for u in unex.values() {
        *kinds
            .entry(match u {
                Unexplained::NoDeclaration => "no-config-block",
                Unexplained::NoLiveCause => "no-live-cause",
                Unexplained::OpaqueCause(_) => "opaque-cause",
            })
            .or_default() += 1;
    }
    for (k, v) in &kinds {
        println!("    {k:<20} {v}");
    }
    if list {
        for (s, u) in &unex {
            println!("UNEXPLAINED CONFIG_{s} : {u:?}");
        }
    }
    if dump {
        // `SYMBOL \t class:name,class:name` — one line per derived symbol, every
        // head that can reach it. This is what the INVERSE test reads: it must
        // be able to ask "is H the ONLY head of S", not merely "is H one of them".
        for (sym, c) in &all {
            match c {
                Cause::Explained { chain, heads } => {
                    let hs: Vec<String> =
                        heads.iter().map(|h| format!("{}:{}", h.class(), h.name())).collect();
                    println!("DUMP\t{sym}\t{}\t{}", hs.join(","), chain.render());
                }
                Cause::Unexplained(u) => println!("DUMP\t{sym}\t!unexplained\t{u:?}"),
            }
        }
        let gates = s.gate_closure();
        for (sym, hs) in &gates {
            let v: Vec<String> = hs.iter().map(|h| format!("{}:{}", h.class(), h.name())).collect();
            println!("GATE\t{sym}\t{}", v.join(","));
        }
    }
    if let Some(w) = a.windows(2).find(|w| w[0] == "--emit-why").map(|w| w[1].clone()) {
        // The FILE an operator reads, through the crate's own emitter — not a
        // second rendering of the same data (LAW 5).
        let reqs = KernelRequirements::default();
        let why = WhyIndex::of(&fragment, &reqs)
            .with_built_config(&built, &reqs)
            .with_kconfig_chains(&src, &fragment, &built, &base, "x86_64", "6.12.104")?;
        std::fs::write(&w, why.to_toml())?;
        eprintln!("wrote {w}; unexplained derived rows: {}", why.unexplained_derived().len());
    }
    if heads {
        let mut v: Vec<_> = declared_head_counts.into_iter().collect();
        v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
        for (h, n) in v {
            println!("HEAD CONFIG_{h} reaches {n} derived symbols");
        }
    }
    Ok(())
}