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

nordisk/nornir

src/mimir_lens/room_feed/graph.rs

//! The code room as a lens reads it: one room over every consumed repo of
//! the workspace, the request's repo filter as node sets, and the few derived
//! sets several lenses share (callable items, the test-reachable set, call
//! sites per item).

use std::collections::{BTreeMap, BTreeSet};

use mimir_room::{Answer, EdgeOut, EdgeSel, Evidence, Mask, MaskSpec, Room, Scope};
use mimir_schema::{EdgeKind, Origin, kind as nk};

use super::{Basis, CALLS_LOWER_BOUND, Query};
use crate::code_room::{Reach, Refusal, State};

/// The room of one request.
pub struct Graph {
    pub room: Room,
    /// The repos the request covers: its filter, or every consumed repo.
    pub repos: Vec<String>,
    scope: String,
    workspace: String,
    /// Where the consumer's clones are (`<builds>/mimir_v2/clones/<repo>`): a
    /// lens that reads a finding's source text (the literal-body filter).
    pub layout: crate::code_room::Layout,
}

/// One callable item (`fn` / `method`) defined in the request's repos.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Callable {
    pub ordinal: u64,
    pub repo: String,
    /// The file its span lies in (`None` = no span row).
    pub file: Option<String>,
    pub line: u32,
    /// The last line of its span (`line` when the room has none).
    pub end_line: u32,
    /// A `#[test]` / bench fn, or an item inside `#[cfg(test)]` code.
    pub is_test: bool,
    /// CALLS sites arriving at it, from anywhere in the room.
    pub call_sites: u32,
    /// In the test-reachable set (`false` everywhere when the scope has no test).
    pub covered: bool,
}

thread_local! {
    /// As-of rooms this thread folded ([`room_uncached`]): what a test reads
    /// to prove a fold was kept rather than repeated.
    static UNCACHED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}

/// How many uncached rooms this thread has folded so far.
pub fn uncached_builds() -> u64 {
    UNCACHED.with(|n| n.get())
}

/// A room over `scope`, built for this one question and NOT kept in the
/// process cache — the time-travel rooms a diff needs must not push the
/// workspace's own room out of it.
pub fn room_uncached(reach: &Reach<'_>, scope: &Scope) -> Result<Room, Refusal> {
    UNCACHED.with(|n| n.set(n.get() + 1));
    let log = reach.code_room()?.log()?;
    // on a thread of its own, on the lake's runtime: callable from any thread
    std::thread::scope(|s| s.spawn(|| log.block_on(Room::build_uncached(&log, scope))).join().unwrap_or_else(|p| std::panic::resume_unwind(p)))
        .map_err(|e| Refusal::new(State::Internal, format!("code room at {scope:?}: {e:#}")))
}

/// Does a `cfg` predicate, as written, require `test` (`test`,
/// `all(test, feature = "x")`) — and not merely mention it under `not(…)`?
pub fn cfg_requires_test(cfg: &str) -> bool {
    let mentions = cfg.split(|c: char| !(c.is_alphanumeric() || c == '_')).any(|t| t == "test");
    mentions && !cfg.replace(' ', "").contains("not(test")
}

/// A room query failed (the engine, not the data).
pub fn internal(e: anyhow::Error) -> Refusal {
    Refusal::new(State::Internal, format!("code room query failed: {e:#}"))
}

impl Basis {
    /// Take a room answer: its evidence becomes feeds (one per edge kind
    /// walked) and gaps (everything the room said was MISSING).
    pub fn room<T>(&mut self, a: Answer<T>) -> T {
        self.evidence(&a.evidence);
        a.value
    }

    pub fn evidence(&mut self, ev: &Evidence) {
        for (k, n) in &ev.kinds {
            self.feed(format!("code_edges:{k}"), *n as usize, format!("code room {}", ev.scope));
        }
        for m in &ev.missing {
            // an edge kind without edges is already a missing feed row: keep the why
            if m.input.strip_suffix(" edges").is_some_and(|k| EdgeKind::parse(k).is_some()) {
                self.note(format!("{}: {}", m.input, m.why));
                continue;
            }
            match &m.repo {
                Some(r) if !self.repos.is_empty() && !self.repos.contains(r) => {}
                Some(r) => self.gap(format!("{} [{r}]: {}", m.input, m.why)),
                None => self.gap(format!("{}: {}", m.input, m.why)),
            }
        }
        for n in &ev.notes {
            // the sub-query's own emptiness is not the lens's, and how the room
            // looked up linked ring items is not the reader's concern
            if !n.starts_with("empty answer") && !n.starts_with("capped") && !n.starts_with("ring off, but linked ring items") {
                self.note(n.trim_end_matches('.').to_string());
            }
        }
    }
}

impl Graph {
    /// Open the room of this request, universe ring off: the room of the ONE
    /// repo asked for when the lens reads nothing that arrives from the other
    /// repos ([`crate::mimir_lens::REPO_SCOPED`]), else every consumed repo.
    /// Refuses by state: no room in reach, the lake held by another process,
    /// nothing consumed yet, the repo asked for not consumed.
    pub fn open(reach: &Reach<'_>, q: &Query<'_>, basis: &mut Basis) -> Result<Self, Refusal> {
        let handle = reach.code_room()?;
        let room = handle.room(&crate::mimir_lens::scope_of(q.lens, q.repo))?;
        let consumed: Vec<String> = room.scope().repos.keys().cloned().collect();
        if consumed.is_empty() {
            return Err(Refusal::new(
                State::NotConsumed,
                format!("code room of `{}`: no repo has been consumed yet (the log holds no consumer offset)", handle.workspace()),
            ));
        }
        if !q.repo.is_empty() && !consumed.iter().any(|r| r == q.repo) {
            return Err(Refusal::new(
                State::NotConsumed,
                format!("code room of `{}`: repo `{}` is not consumed yet (consumed: {})", handle.workspace(), q.repo, consumed.join(", ")),
            ));
        }
        let scope = room.scope().to_string();
        basis.scope = Some(scope.clone());
        let stats = room.stats();
        for input in &stats.inputs {
            if input.table == "code_node_events" {
                basis.feed("code_nodes", input.open_rows as usize, format!("code room {scope}"));
            }
        }
        let repos = if q.repo.is_empty() { consumed } else { vec![q.repo.to_string()] };
        basis.repos = repos.clone();
        Ok(Self { room, repos, scope, workspace: handle.workspace().to_string(), layout: handle.layout().clone() })
    }

    /// The workspace the room is of (answers name the verb that fills a gap).
    pub fn workspace(&self) -> &str {
        &self.workspace
    }

    pub fn root(&self) -> String {
        format!("code room {}", self.scope)
    }

    /// A mask of the request's repos with `spec`'s other fields.
    pub fn mask(&self, mut spec: MaskSpec) -> Result<Mask, Refusal> {
        spec.repos = self.repos.clone();
        Ok(self.room.mask(&spec).map_err(internal)?.value)
    }

    /// Source definitions of the request's repos' OWN crates, of these kinds
    /// (empty = every kind).
    pub fn local(&self, kinds: &[&str]) -> Result<Mask, Refusal> {
        self.mask(MaskSpec { node_kinds: kinds.iter().map(|k| k.to_string()).collect(), origins: vec![Origin::Local], defined_only: true, ..Default::default() })
    }

    /// The callable items (`fn` + `method`) defined in the request's repos.
    pub fn callables(&self) -> Result<Mask, Refusal> {
        // a kind no node has is reported by the room as missing — here it only
        // means "this repo defines no method", which is no gap
        let mut ords: Vec<u64> = Vec::new();
        for k in [nk::FN, nk::METHOD] {
            ords.extend_from_slice(self.local(&[k])?.ordinals());
        }
        Ok(self.room.mask_of(&ords))
    }

    /// `(repo, file)` of a node's span.
    pub fn file_of(&self, o: u64) -> Option<(String, String)> {
        self.room.span(o).map(|s| (s.repo.to_string(), s.file.to_string()))
    }

    /// The edges of `kinds` between two node sets (`None` = any node). A kind
    /// the room holds no edge of is a MISSING feed of the answer.
    pub fn edges(&self, basis: &mut Basis, kinds: &[EdgeKind], from: Option<&Mask>, to: Option<&Mask>) -> Result<Vec<EdgeOut>, Refusal> {
        let sel = EdgeSel { kinds: kinds.to_vec(), from, to, cap: None, heaviest_first: false };
        Ok(basis.room(self.room.edges(&sel).map_err(internal)?).edges)
    }

    /// [`Self::edges`] for the kinds whose absence is a fact about the code
    /// (GATED_BY, ENABLES, TESTS: the driver writes them whenever there is
    /// something to write): recorded with their count, never as missing.
    pub fn edges_if_any(&self, basis: &mut Basis, kind: EdgeKind, from: Option<&Mask>, to: Option<&Mask>) -> Result<Vec<EdgeOut>, Refusal> {
        let sel = EdgeSel { kinds: vec![kind], from, to, cap: None, heaviest_first: false };
        let a = self.room.edges(&sel).map_err(internal)?;
        let total = a.evidence.kinds.first().map_or(0, |k| k.1 as usize);
        basis.fact(format!("code_edges:{kind}"), total, self.root());
        Ok(a.value.edges)
    }

    /// Call SITES arriving at each node of `targets` (the `count` column
    /// summed over its CALLS in-edges), and the P6 lower-bound note.
    pub fn call_sites_into(&self, basis: &mut Basis, targets: &Mask) -> Result<BTreeMap<u64, u32>, Refusal> {
        basis.lower_bound(CALLS_LOWER_BOUND);
        let mut out: BTreeMap<u64, u32> = BTreeMap::new();
        for e in self.edges(basis, &[EdgeKind::Calls], None, Some(targets))? {
            *out.entry(e.to).or_default() += e.count;
        }
        Ok(out)
    }

    /// The test-reachable set: everything a `#[test]` / bench fn of the
    /// request's repos reaches along CALLS, plus the direct TESTS targets.
    /// `None` = the scope holds no test node (no coverage input at all).
    pub fn test_reach(&self, basis: &mut Basis) -> Result<Option<BTreeSet<u64>>, Refusal> {
        let tests = self.mask(MaskSpec { is_test: Some(true), ..Default::default() })?;
        basis.fact("code_nodes:is_test", tests.len(), self.root());
        if tests.is_empty() {
            return Ok(None);
        }
        basis.lower_bound(CALLS_LOWER_BOUND);
        let walk = basis.room(self.room.reach(&tests, &[EdgeKind::Calls], None, None).map_err(internal)?);
        let mut covered: BTreeSet<u64> = walk.reached.into_iter().collect();
        // TESTS: what a test names directly
        covered.extend(self.edges_if_any(basis, EdgeKind::Tests, Some(&tests), None)?.into_iter().map(|e| e.to));
        Ok(Some(covered))
    }

    /// Is the node inside `#[cfg(test)]` code — itself or a container gated by
    /// a `cfg` that requires `test`?
    pub fn in_test_code(&self, o: u64) -> bool {
        let mut cur = Some(o);
        for _ in 0..64 {
            let Some(c) = cur else { break };
            let n = self.room.node(c);
            if n.cfg.is_some_and(cfg_requires_test) {
                return true;
            }
            cur = n.parent;
        }
        false
    }

    /// Is the node `#[doc(hidden)]` — itself, or a container it sits in (a
    /// hidden module hides what is in it)? The attribute is on the node that
    /// carries it (`doc_hidden`, mimir-schema 3); a node consumed before the
    /// column existed says nothing and counts as not hidden.
    pub fn doc_hidden(&self, o: u64) -> bool {
        let mut cur = Some(o);
        for _ in 0..64 {
            let Some(c) = cur else { break };
            if self.room.attr(c, "doc_hidden").and_then(|v| v.as_bool()) == Some(true) {
                return true;
            }
            cur = self.room.node(c).parent;
        }
        false
    }

    /// Every callable of the request's repos with its call sites and whether
    /// a test reaches it. The second value says whether the scope holds any
    /// test at all (without one, `covered` carries no information).
    pub fn callable_facts(&self, basis: &mut Basis) -> Result<(Vec<Callable>, bool), Refusal> {
        let callables = self.callables()?;
        basis.feed("code_nodes:fn+method", callables.len(), self.root());
        let sites = self.call_sites_into(basis, &callables)?;
        let covered = self.test_reach(basis)?;
        let out = callables
            .ordinals()
            .iter()
            .map(|&o| {
                let n = self.room.node(o);
                Callable {
                    ordinal: o,
                    repo: n.repo.unwrap_or("").to_string(),
                    file: n.file.map(str::to_string),
                    line: n.start_line.unwrap_or(0),
                    end_line: n.end_line.or(n.start_line).unwrap_or(0),
                    is_test: n.is_test || self.in_test_code(o),
                    call_sites: sites.get(&o).copied().unwrap_or(0),
                    covered: covered.as_ref().is_some_and(|c| c.contains(&o)),
                }
            })
            .collect();
        Ok((out, covered.is_some()))
    }

    /// The impure items the room knows — `std::process::Command::new`, the
    /// libc / nix exec and system wrappers, `std::process::exit` …: every node
    /// with a candidate name whose resolved path the villa classifier accepts.
    /// `ordinal → (path, node kind)`.
    pub fn impure_items(&self) -> Result<BTreeMap<u64, (String, String)>, Refusal> {
        use crate::mimir_lens::villa;
        let mut impure = BTreeMap::new();
        for name in villa::IMPURE_NAMES {
            let hits = self.room.lookup(&mimir_room::NameQuery::new(*name).mode(mimir_room::NameMatch::Name)).map_err(internal)?.value;
            for o in hits {
                let kind = self.room.node(o).kind.unwrap_or("").to_string();
                let path = self.room.display_path(o);
                if villa::classify(&path, &kind).is_some() {
                    impure.insert(o, (path, kind));
                }
            }
        }
        Ok(impure)
    }
}