diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index 2c118f303..d58f60009 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -1,3 +1,10 @@ +## 2026-11-02 — lance-graph PR #1920 (merged `5b967b130`, branch `claude/clone-repositories-71a5sw`) — D-CODEGRAPH-0 slice B record; rev2 fixture deletes `area_dyn` + +- **Added:** `fixtures/codegraph/rev2` also deletes `cg_lib::area_dyn` and drops its call from `cg_app`, so a refresh that skips the dependent leaves one dangling edge; the slice B section of the gap-map entry; STATUS_BOARD D-CODEGRAPH-0-B LANDED for the offline store. +- **Companion:** ruff PR #130 (merged `95be2e480b`, offline store in `ruff_charon`) and #131 (merged `2033be48fe`, four Codex findings on #129/#130). +- **Deferred:** residency in lance-graph's SoA. +- **Confidence:** incremental refresh byte-identical to a clean import; five disable runs red. + ## 2026-11-02 — lance-graph PR #1918 (merged `c11e4262e`, branch `claude/clone-repositories-71a5sw`, head `c0f81a2a8`) — D-CODEGRAPH-0 gap map and slice A evidence; D-DQM M1 timing arms; dt64/xlog CI lines - **Added:** diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index e7757b80a..217217d0b 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -239,7 +239,7 @@ Plan: [d-codegraph-0-resident-codegraph-v1.md](../plans/d-codegraph-0-resident-c |---|---|---|---| | **D-CODEGRAPH-0-A** | ruff `ruff_charon`: LLBC → Kythe facts with a witness status per call, for one real crate | SHIPPED: ruff #129 (merged `9913e90946`), lance-graph #1918 (merged `c11e4262e`): calls byte-identical to `llbc_calls.py` on the fixture and `mask-risc`; receipt conserved (16,149); Kythe `/xrefs` 3/3 call pins | rust-analyzer oracle (`sag-ra-probe GRAPH=1`) and rustc on `fixtures/codegraph`; `POSSIBLE` never counted as a confirmed caller | | **D-CODEGRAPH-0-B** | resident rows via the ruff sink; per-file digests; incremental invalidation | LANDED (offline form): ruff #130 (merged `95be2e480b`; offline store in `ruff_charon`; `RefinedTruthSink` not reused, typed to R2IL). Incremental == clean on the fixture, byte-identical; skipped dependent leaves exactly one dangling edge. Follow-up fixes in ruff #131 (open). Lance-graph SoA residency still open | clean import == incremental import | -| **D-CODEGRAPH-0-C** | callers / callees / impact / diagnostics; tiled closure; body → R2IL design check | QUEUED | each guard disable-verified; unused import proven only without macros, traits, globs, re-exports | +| **D-CODEGRAPH-0-C** | callers / callees / impact / diagnostics; tiled closure; body → R2IL design check | IN PR: ruff #132 (merged `ed8fd4194f`, rows v2) + `crates/lance-graph-codegraph` (queries as confirmed / possible / unknown masks over the xlog cube; dead function, unresolved call, dangling call; unused import NOT_RUN). 27 tests, 12 disable runs red. Tiled closure deferred: refusals `ClosureWidth` (> 64) and `TooMany` (> 256) are surfaced as typed UNKNOWN | each guard disable-verified; unused import proven only without macros, traits, globs, re-exports | | **D-CODEGRAPH-0-D** | local stdio MCP over C | QUEUED | only after B and C gates | | **D-CODEGRAPH-0-E** | token measurement with and without the Codegraph | QUEUED | same revision, questions, model; nothing claimed before measured | diff --git a/.claude/board/entries/2026-11-02-d-codegraph-0-gap-map.md b/.claude/board/entries/2026-11-02-d-codegraph-0-gap-map.md index 2ef3e1611..4e85cc541 100644 --- a/.claude/board/entries/2026-11-02-d-codegraph-0-gap-map.md +++ b/.claude/board/entries/2026-11-02-d-codegraph-0-gap-map.md @@ -85,3 +85,20 @@ Five disable runs each turn their own tests red. G5 (resident, incremental store) is closed for the offline form. Still open: residency in lance-graph's SoA (a lance-graph-side reader of these artifacts), then the queries of slice C. ruff #131 (open) corrects four Codex findings on #129/#130: a body Charon left unstructured is still harvested, a call through a known trait impl resolves to the impl's method or the trait default, temporary files are unique per call, and a temporary name left by an interrupted import is skipped instead of aborting the import. + +## Slice C: queries and diagnostics (ruff #132 merged `ed8fd4194f`; `crates/lance-graph-codegraph`) + +Rows v2 in ruff adds the facts the queries need: `PUBLIC`, `IMPLEMENTS` (impl method → `Trait::method`), `SYNTHETIC`, `REFERENCES` (function pointers), a `CRATE … COMPLETE|PARTIAL` row, and `MAY_DISPATCH` naming the method. Charon marks trait-impl methods public, so cg_lib rev1 has 9 `PUBLIC` rows for its 7 `pub fn`s. + +`lance-graph-codegraph` reads the artifacts by column name (no dependency on ruff_charon) and builds one xlog cube over 15 positions (rev1). Answers are masks graded confirmed / possible / unknown; `MAY_DISPATCH` is never confirmed, and a dyn or pointer call is unknown. On the fixture: +- `impact(double)` rev1: confirmed {Square::area, run}, possible {area_of}, unknown {area_dyn}; rev2: confirmed 4 including the 2-hop `checked`. +- `callees(run)` rev2: 3 callees, 4 sites. +- Dead function: rev1 `private_unused`, fnref `unused_private` (the pointer-only `helper` stays silent); rustc's `dead_code` agrees on both. +- Dangling call: mixed store (cg_lib rev2 + cg_app rev1) exactly `run → area_dyn`, status stale; rustc reports E0425 at `cg_app/src/lib.rs:7` on the same tree. +- Unused import: NOT_RUN, reason NO_IMPORT_FACTS (LLBC has no `use` items). +- Release timing, ns per call: callers 5103, callees 3643, references 184, impact 387; open 166 µs and first closure 5 µs (cold, reported, not gated). +- `cube().stats().rows_materialized == 0` after the whole suite. + +Twelve disable runs each turn their own test red. The one that first stayed green (dropping the impl-method exclusion from the dead-function check) is now covered by a synthetic store: Charon marks every impl method of a public trait public, so the fixtures never reach that case. + +Open: tiled closure for more than 64 positions and multi-tile cubes for more than 256 (mask-risc has 459 functions, so every cube query on it is refused today, as a typed UNKNOWN); import facts for unused imports; body → R2IL (G4, G7). diff --git a/Cargo.toml b/Cargo.toml index eb5f99d38..b1e281752 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ members = [ "crates/lance-graph-hydrate", ] exclude = [ + "crates/lance-graph-codegraph", "crates/lance-graph-sap", # D-HOLO-H14c Lance 13 masked-I/O probe: own narrow dependency set. "crates/holo-lance-masked-io-probe", diff --git a/crates/lance-graph-codegraph/Cargo.toml b/crates/lance-graph-codegraph/Cargo.toml new file mode 100644 index 000000000..a6d9d01e1 --- /dev/null +++ b/crates/lance-graph-codegraph/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "lance-graph-codegraph" +version = "0.1.0" +edition = "2021" +publish = false +description = "Codegraph queries and structural diagnostics (D-CODEGRAPH-0 slice C) over the ruff_charon store artifacts, folded through the xlog cube; no dependency on ruff_charon." + +# Own workspace root, excluded from the lance-graph workspace (as sag-ra-probe). +[workspace] + +[dependencies] +lance-graph-xlog = { path = "../lance-graph-xlog" } +lance-graph-dt64 = { path = "../lance-graph-dt64" } +lance-graph-quack = { path = "../lance-graph-quack" } +lance-graph-mask-risc = { path = "../lance-graph-mask-risc" } +ndarray = { git = "https://github.com/AdaWorldAPI/ndarray", branch = "master", default-features = false, features = ["std"] } diff --git a/crates/lance-graph-codegraph/examples/timing.rs b/crates/lance-graph-codegraph/examples/timing.rs new file mode 100644 index 000000000..40167e191 --- /dev/null +++ b/crates/lance-graph-codegraph/examples/timing.rs @@ -0,0 +1,61 @@ +//! Per-call timing of the four queries on the rev1 fixture, in nanoseconds. +//! Cold costs are reported in microseconds, not gated. + +use std::hint::black_box; +use std::path::PathBuf; +use std::time::Instant; + +use lance_graph_codegraph::Codegraph; + +const ITERS: u32 = 10_000; + +fn dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/data/rev1") +} + +fn per_call_ns(mut f: impl FnMut()) -> f64 { + for _ in 0..ITERS { + f(); + } + let t = Instant::now(); + for _ in 0..ITERS { + f(); + } + t.elapsed().as_nanos() as f64 / f64::from(ITERS) +} + +fn main() { + let t = Instant::now(); + let mut g = Codegraph::open(&dir()).expect("rev1 opens"); + println!( + "open: {} us (cold, reported, not gated)", + t.elapsed().as_micros() + ); + + let double = g.pos("cg_lib::util::double").expect("double"); + let run = g.pos("cg_app::run").expect("run"); + + let t = Instant::now(); + black_box(g.impact(double).expect("impact")); + println!( + "first closure build (impact): {} us (cold, reported, not gated)", + t.elapsed().as_micros() + ); + + let ns = per_call_ns(|| { + black_box(g.callers(double).unwrap()); + }); + println!("callers: {ns:.1} ns/call"); + let ns = per_call_ns(|| { + black_box(g.callees(run).unwrap()); + }); + println!("callees: {ns:.1} ns/call"); + let ns = per_call_ns(|| { + black_box(g.references(double)); + }); + println!("references: {ns:.1} ns/call"); + let ns = per_call_ns(|| { + black_box(g.impact(double).unwrap()); + }); + println!("impact: {ns:.1} ns/call"); +} diff --git a/crates/lance-graph-codegraph/src/artifact.rs b/crates/lance-graph-codegraph/src/artifact.rs new file mode 100644 index 000000000..b88b104ad --- /dev/null +++ b/crates/lance-graph-codegraph/src/artifact.rs @@ -0,0 +1,128 @@ +//! The reader for `*.rows.tsv` artifacts (version 2). +//! +//! Lines stream straight into column lanes: one push per field per line, no +//! per-row struct. Columns are located by name from the `#schema` line, so a +//! file with permuted columns reads identically. + +use std::fs; +use std::path::Path; + +use crate::graph::IntakeRefusal; + +/// The only artifact version this reader accepts. +pub const ROWS_VERSION: u32 = 2; + +/// Columns every artifact must carry. +const REQUIRED: [&str; 7] = [ + "predicate", + "subject", + "object", + "grade", + "file", + "start", + "end", +]; + +/// Marks an absent (`-`) span bound in [`Lanes::start`] / [`Lanes::end`]. +pub const NO_SPAN: u32 = u32::MAX; + +/// All rows of a directory as parallel column lanes; index `i` of every lane +/// is row `i`. Invariant: all lanes have equal length. +#[derive(Debug, Default)] +pub struct Lanes { + /// Relation name of each row. + pub predicate: Vec, + /// Subject symbol of each row. + pub subject: Vec, + /// Object text of each row. + pub object: Vec, + /// Grade (provenance) of each row. + pub grade: Vec, + /// Source file of each row, `-` when none. + pub file: Vec, + /// Span start, [`NO_SPAN`] when `-`. + pub start: Vec, + /// Span end, [`NO_SPAN`] when `-`. + pub end: Vec, +} + +/// Read every `*.rows.tsv` in `dir`, sorted by file name so the row order is +/// deterministic. +pub fn read_dir(dir: &Path) -> Result { + let mut paths = Vec::new(); + for entry in fs::read_dir(dir).map_err(IntakeRefusal::Io)? { + let path = entry.map_err(IntakeRefusal::Io)?.path(); + let is_rows = path + .file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.ends_with(".rows.tsv")); + if is_rows { + paths.push(path); + } + } + paths.sort(); + let mut lanes = Lanes::default(); + for p in &paths { + let text = fs::read_to_string(p).map_err(IntakeRefusal::Io)?; + read_text(&text, &mut lanes)?; + } + Ok(lanes) +} + +fn number(field: &str) -> Option { + if field == "-" { + Some(NO_SPAN) + } else { + field.parse().ok() + } +} + +/// Append one file's rows to `lanes`. +pub fn read_text(text: &str, lanes: &mut Lanes) -> Result<(), IntakeRefusal> { + let mut lines = text.lines(); + let found = lines + .next() + .and_then(|l| l.strip_prefix("#version ")) + .and_then(|v| v.trim().parse::().ok()) + .unwrap_or(0); + if found != ROWS_VERSION { + return Err(IntakeRefusal::Version { found }); + } + let schema = lines + .next() + .and_then(|l| l.strip_prefix("#schema ")) + .ok_or_else(|| IntakeRefusal::Schema { + missing: "#schema".to_string(), + })?; + let names: Vec<&str> = schema.split('\t').collect(); + let mut at = [0usize; 7]; + for (slot, want) in at.iter_mut().zip(REQUIRED) { + *slot = names + .iter() + .position(|n| *n == want) + .ok_or_else(|| IntakeRefusal::Schema { + missing: want.to_string(), + })?; + } + for (i, line) in lines.enumerate() { + // Lines 1 and 2 were the header; data starts at line 3. + let lineno = i + 3; + if line.is_empty() { + continue; + } + let f: Vec<&str> = line.split('\t').collect(); + if f.len() != names.len() { + return Err(IntakeRefusal::Arity { line: lineno }); + } + let start = number(f[at[5]]).ok_or(IntakeRefusal::Arity { line: lineno })?; + let end = number(f[at[6]]).ok_or(IntakeRefusal::Arity { line: lineno })?; + lanes.predicate.push(f[at[0]].to_string()); + lanes.subject.push(f[at[1]].to_string()); + lanes.object.push(f[at[2]].to_string()); + lanes.grade.push(f[at[3]].to_string()); + lanes.file.push(f[at[4]].to_string()); + lanes.start.push(start); + lanes.end.push(end); + } + Ok(()) +} diff --git a/crates/lance-graph-codegraph/src/diag.rs b/crates/lance-graph-codegraph/src/diag.rs new file mode 100644 index 000000000..7124dffd0 --- /dev/null +++ b/crates/lance-graph-codegraph/src/diag.rs @@ -0,0 +1,222 @@ +//! Compiler-graded diagnostics read off the codegraph's masks. +//! +//! Review semantics follow crai-rs CR-H0: the grade is `COMPILER` (the facts +//! come from the compiler's own LLBC, not from a heuristic); `POSSIBLE` is a +//! qualifier, never a grade; a run is `NOT_RUN` / `PARTIAL` / `COMPLETE`, and +//! a diagnostic that did not run is never reported as "clean" (zero findings +//! with a `NotRun` or `Refused` run means "no verdict", not "no problems"). + +use ndarray::simd::mask_andnot; +use ndarray::simd::mask_andnot_assign; + +use crate::graph::{Codegraph, RunStatus}; + +/// Which diagnostic a [`Finding`] belongs to. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Kind { + /// A function nothing reaches. + DeadFunction, + /// A call whose target the compiler could not resolve. + UnresolvedCall, + /// A resolved call into a stored crate that defines no such function. + DanglingCall, + /// A `use` that nothing needs. + UnusedImport, +} + +/// How a finding was established. Only the compiler grade exists here. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Grade { + /// Derived from compiler-emitted facts. + Compiler, +} + +/// A qualifier on a finding; never a grade. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Qualifier { + /// The finding holds on some resolutions, not all. + Possible, +} + +/// Lifecycle of a finding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FindingStatus { + /// Current as of the loaded artifact. + Open, + /// The artifact set is an incomplete refresh; the finding may be an + /// artifact of what was and was not re-extracted. + Stale, +} + +/// Whether the diagnostic ran, and how far. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Run { + /// Every stored crate's extractor finished. + Complete, + /// At least one stored crate's CRATE row is not `COMPLETE`. + Partial, + /// The artifact carries no facts for this diagnostic; no verdict. + NotRun { + /// Why nothing ran. + reason: &'static str, + }, + /// The diagnostic declined to answer for this input. + Refused { + /// Why it declined. + reason: &'static str, + }, +} + +/// Whether the analysed crate is a library or a binary. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CrateKind { + /// A library: its public items are entry points. + Lib, + /// A binary: entry reachability is not in the rows. + Bin, +} + +/// One diagnostic's result: masks, not decoded tuples. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Finding { + /// Which diagnostic produced this. + pub kind: Kind, + /// Position mask (functions), `words()` words. + pub positions: Vec, + /// Row mask (call sites), `rows().div_ceil(64)` words; an empty `Vec` + /// for position-only findings. + pub rows: Vec, + /// How the finding was established. + pub grade: Grade, + /// Qualifier, if any. + pub qualifier: Option, + /// Lifecycle status. + pub status: FindingStatus, + /// Whether and how far the diagnostic ran. + pub run: Run, +} + +fn set_bit(words: &mut [u64], p: usize) { + if let Some(w) = words.get_mut(p / 64) { + *w |= 1 << (p % 64); + } +} + +fn has_bit(words: &[u64], p: usize) -> bool { + words.get(p / 64).is_some_and(|w| w >> (p % 64) & 1 == 1) +} + +impl Codegraph { + /// The four diagnostics, in the order DeadFunction, UnresolvedCall, + /// DanglingCall, UnusedImport. + /// + /// The overall run is [`Run::Partial`] when any `CRATE` row carries a + /// status other than `COMPLETE`, else [`Run::Complete`]; a `Partial` run + /// still reports its findings, but the caller must not read a short list + /// as "clean". + pub fn diagnostics(&self, kind: CrateKind) -> [Finding; 4] { + let words = self.words(); + let row_words = self.rows().div_ceil(64); + + // One lane scan per diagnostic, into masks (not a tuple decode): + // CRATE rows for the run status, UNRESOLVED_CALL rows, and the + // CALLS_RESOLVED rows that dangle. + let mut partial = false; + let mut unresolved_rows = vec![0u64; row_words]; + let mut dangling_rows = vec![0u64; row_words]; + let mut dangling_callers = vec![0u64; words]; + for r in 0..self.rows() { + match self.row_predicate(r) { + "CRATE" => { + if self.crate_status(self.row_subject(r)) == Some(RunStatus::Partial) { + partial = true; + } + } + "UNRESOLVED_CALL" => set_bit(&mut unresolved_rows, r), + "CALLS_RESOLVED" => { + let (Some(caller), Some(callee)) = + (self.pos(self.row_subject(r)), self.pos(self.row_object(r))) + else { + continue; + }; + let callee = usize::from(callee); + if !has_bit(self.fns(), callee) && has_bit(self.stored_owner(), callee) { + set_bit(&mut dangling_rows, r); + set_bit(&mut dangling_callers, usize::from(caller)); + } + } + _ => {} + } + } + let overall = if partial { Run::Partial } else { Run::Complete }; + + // DeadFunction: defined, not public, not an impl, not synthetic, and + // neither called nor referenced. Silent for a binary, whose entry + // reachability the rows do not carry. + let (dead, dead_run) = match kind { + CrateKind::Bin => ( + vec![0u64; words], + Run::Refused { + reason: "NO_ENTRY_FACTS", + }, + ), + CrateKind::Lib => { + let mut m = vec![0u64; words]; + mask_andnot(self.fns(), self.public(), &mut m); + mask_andnot_assign(&mut m, self.impl_subjects()); + mask_andnot_assign(&mut m, self.synthetic()); + mask_andnot_assign(&mut m, self.called()); + mask_andnot_assign(&mut m, self.referenced()); + (m, overall) + } + }; + + [ + Finding { + kind: Kind::DeadFunction, + positions: dead, + rows: Vec::new(), + grade: Grade::Compiler, + qualifier: None, + status: FindingStatus::Open, + run: dead_run, + }, + // UnresolvedCall: fires for every UNRESOLVED_CALL row; silent + // only when the artifact has none. + Finding { + kind: Kind::UnresolvedCall, + positions: self.dynamic_callers().to_vec(), + rows: unresolved_rows, + grade: Grade::Compiler, + qualifier: None, + status: FindingStatus::Open, + run: overall, + }, + // DanglingCall: the callee belongs to a stored crate yet no + // stored crate defines it. Callees of crates not in the store + // (core::..., std::...) and MAY_DISPATCH rows never count. + Finding { + kind: Kind::DanglingCall, + positions: dangling_callers, + rows: dangling_rows, + grade: Grade::Compiler, + qualifier: None, + status: FindingStatus::Stale, + run: overall, + }, + // UnusedImport: LLBC carries no `use` items, so there is no + // verdict, and the empty masks must not be read as "clean". + Finding { + kind: Kind::UnusedImport, + positions: vec![0u64; words], + rows: vec![0u64; row_words], + grade: Grade::Compiler, + qualifier: None, + status: FindingStatus::Open, + run: Run::NotRun { + reason: "NO_IMPORT_FACTS", + }, + }, + ] + } +} diff --git a/crates/lance-graph-codegraph/src/graph.rs b/crates/lance-graph-codegraph/src/graph.rs new file mode 100644 index 000000000..7400fd81b --- /dev/null +++ b/crates/lance-graph-codegraph/src/graph.rs @@ -0,0 +1,382 @@ +//! The codegraph: artifact rows interned into one tile of symbol positions +//! and folded into six relations of an xlog [`Cube`]. + +use std::cell::OnceCell; +use std::collections::HashMap; +use std::path::Path; + +use lance_graph_dt64::dr::{LocalMap, MapRefusal, MapTable, TileId, Width}; +use lance_graph_xlog::cube::{Closure, ClosureStats, Cube, CubeRefusal, RelId}; + +use crate::artifact::{self, Lanes, NO_SPAN}; + +/// A symbol's local position in the tile (at most 256 symbols). +pub type Pos = u8; + +/// Why a directory could not become a [`Codegraph`]. +#[derive(Debug)] +pub enum IntakeRefusal { + /// A directory or file could not be read. + Io(std::io::Error), + /// The file's `#version` line is not 2 (0 when absent or unparsable). + Version { + /// The version found. + found: u32, + }, + /// The `#schema` line, or a required column, is missing. + Schema { + /// The missing column (or `#schema`). + missing: String, + }, + /// A data line whose field count differs from the schema, or whose + /// span bounds are not numbers; 1-based line within its file. + Arity { + /// The offending line. + line: usize, + }, + /// More than 256 symbols: one tile cannot hold them. Carries the count. + TooMany(usize), + /// The tile map was refused. + Map(MapRefusal), + /// The cube was refused. + Cube(CubeRefusal), +} + +/// Whether the extractor finished a crate. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RunStatus { + /// The crate's CRATE row says `COMPLETE`. + Complete, + /// Any other CRATE status. + Partial, +} + +/// `CALLS`: (caller, callee) for resolved calls. +pub const CALLS: RelId = 0; +/// `NODE`: (x, x) for every position. +pub const NODE: RelId = 1; +/// `MAY`: (caller, Trait::method) for may-dispatch rows. +pub const MAY: RelId = 2; +/// `IMPL`: (impl fn, Trait::method). +pub const IMPL: RelId = 3; +/// `RCONF`: (callee, caller), the confirmed reverse edges. +pub const RCONF: RelId = 4; +/// `RPOSS`: the possible reverse edges, including dispatch and `?dynamic`. +pub const RPOSS: RelId = 5; + +const TILE: TileId = 0; +const NONE: u32 = u32::MAX; +const DYNAMIC: &str = "?dynamic"; + +/// Position masks, one bit per position, `words()` words each. +#[derive(Default)] +struct Masks { + fns: Vec, + public: Vec, + impl_subjects: Vec, + synthetic: Vec, + referenced: Vec, + called: Vec, + may_targets: Vec, + dynamic_callers: Vec, + stored_owner: Vec, +} + +/// The queryable codegraph. Owns the artifact lanes, the symbol table, the +/// tile map and the cube; relations were dropped after the cube took them. +pub struct Codegraph { + lanes: Lanes, + names: Vec, + index: HashMap, + crates: HashMap, + table: MapTable, + cube: Cube, + masks: Masks, + rconf: OnceCell<(Closure, ClosureStats)>, + rposs: OnceCell<(Closure, ClosureStats)>, +} + +fn intern(names: &mut Vec, index: &mut HashMap, s: &str) -> u32 { + if let Some(&p) = index.get(s) { + return p; + } + let p = names.len() as u32; + names.push(s.to_string()); + index.insert(s.to_string(), p); + p +} + +/// The `Trait::method` part of a MAY_DISPATCH object. +fn dispatch_target(object: &str) -> &str { + object.split(" via ").next().unwrap_or(object) +} + +fn set(words: &mut [u64], p: u32) { + words[p as usize / 64] |= 1 << (p % 64); +} + +impl Codegraph { + /// Read every `*.rows.tsv` in `dir` and build the graph. + pub fn open(dir: &Path) -> Result { + let lanes = artifact::read_dir(dir)?; + let rows = lanes.predicate.len(); + + // Pass 1: intern symbols in first-seen row order; remember each row's + // subject and object ids in lanes (NONE when the row has no symbol). + let mut names: Vec = Vec::new(); + let mut idx: HashMap = HashMap::new(); + let mut sid = vec![NONE; rows]; + let mut oid = vec![NONE; rows]; + let mut any_dynamic = false; + let mut crates = HashMap::new(); + for i in 0..rows { + let (s, o) = (lanes.subject[i].as_str(), lanes.object[i].as_str()); + match lanes.predicate[i].as_str() { + "DEFINES" | "CALLS_RESOLVED" | "PUBLIC" | "IMPLEMENTS" | "SYNTHETIC" + | "REFERENCES" => { + sid[i] = intern(&mut names, &mut idx, s); + oid[i] = intern(&mut names, &mut idx, o); + } + "MAY_DISPATCH" => { + sid[i] = intern(&mut names, &mut idx, s); + oid[i] = intern(&mut names, &mut idx, dispatch_target(o)); + } + // The caller is a symbol; the object ("dynamic") is not. + "UNRESOLVED_CALL" => { + sid[i] = intern(&mut names, &mut idx, s); + any_dynamic = true; + } + "CRATE" => { + let st = if o == "COMPLETE" { + RunStatus::Complete + } else { + RunStatus::Partial + }; + crates.insert(s.to_string(), st); + } + _ => {} + } + } + let dyn_id = any_dynamic.then(|| intern(&mut names, &mut idx, DYNAMIC)); + let n = names.len(); + let width = match n { + 0..=64 => Width::W64, + 65..=256 => Width::W256, + _ => return Err(IntakeRefusal::TooMany(n)), + }; + let words = n.div_ceil(64); + + // Pass 2: relations and masks. + let mut rel: [Vec<(u32, u32)>; 6] = Default::default(); + let mut m = Masks { + fns: vec![0; words], + public: vec![0; words], + impl_subjects: vec![0; words], + synthetic: vec![0; words], + referenced: vec![0; words], + called: vec![0; words], + may_targets: vec![0; words], + dynamic_callers: vec![0; words], + stored_owner: vec![0; words], + }; + rel[NODE as usize] = (0..n as u32).map(|p| (p, p)).collect(); + for i in 0..rows { + let (s, o) = (sid[i], oid[i]); + match lanes.predicate[i].as_str() { + "DEFINES" => set(&mut m.fns, s), + "PUBLIC" => set(&mut m.public, s), + "SYNTHETIC" => set(&mut m.synthetic, s), + "REFERENCES" => set(&mut m.referenced, o), + "CALLS_RESOLVED" => { + set(&mut m.called, o); + rel[CALLS as usize].push((s, o)); + rel[RCONF as usize].push((o, s)); + rel[RPOSS as usize].push((o, s)); + } + "IMPLEMENTS" => { + set(&mut m.impl_subjects, s); + rel[IMPL as usize].push((s, o)); + rel[RPOSS as usize].push((s, o)); + } + "MAY_DISPATCH" => { + set(&mut m.may_targets, o); + rel[MAY as usize].push((s, o)); + rel[RPOSS as usize].push((o, s)); + } + "UNRESOLVED_CALL" => { + set(&mut m.dynamic_callers, s); + if let Some(d) = dyn_id { + rel[RPOSS as usize].push((d, s)); + } + } + _ => {} + } + } + for (p, name) in names.iter().enumerate() { + let owner = name.split("::").next().unwrap_or(name); + if crates.contains_key(owner) { + set(&mut m.stored_owner, p as u32); + } + } + + let ids: Vec = (0..n as u64).collect(); + let map = LocalMap::prepare(width, &ids).map_err(IntakeRefusal::Map)?; + let mut table = MapTable::new(1, 1); + table.advance(1, 1, vec![(TILE, Some(map))]); + let frame = table.seal(); + let slices: Vec<&[(u32, u32)]> = rel.iter().map(Vec::as_slice).collect(); + let cube = Cube::new(&table, &frame, TILE, &slices).map_err(IntakeRefusal::Cube)?; + + let index = idx.into_iter().map(|(k, v)| (k, v as Pos)).collect(); + Ok(Self { + lanes, + names, + index, + crates, + table, + cube, + masks: m, + rconf: OnceCell::new(), + rposs: OnceCell::new(), + }) + } + + /// The position of `symbol`, if interned. + pub fn pos(&self, symbol: &str) -> Option { + self.index.get(symbol).copied() + } + + /// The symbol at position `p`. + pub fn symbol(&self, p: Pos) -> Option<&str> { + self.names.get(usize::from(p)).map(String::as_str) + } + + /// Mask words per position mask: `n.div_ceil(64)`. + pub fn words(&self) -> usize { + self.names.len().div_ceil(64) + } + + /// A mask with the bits of `symbols` set; unknown symbols are ignored. + pub fn mask_of(&self, symbols: &[&str]) -> Vec { + let mut w = vec![0u64; self.words()]; + for s in symbols { + if let Some(p) = self.pos(s) { + set(&mut w, u32::from(p)); + } + } + w + } + + /// The extractor status of `krate`, from its CRATE row. + pub fn crate_status(&self, krate: &str) -> Option { + self.crates.get(krate).copied() + } + + /// Data rows read. + pub fn rows(&self) -> usize { + self.lanes.predicate.len() + } + + /// Predicate of `row`; panics only on an out-of-range row, like indexing. + pub fn row_predicate(&self, row: usize) -> &str { + &self.lanes.predicate[row] + } + + /// Subject of `row`. + pub fn row_subject(&self, row: usize) -> &str { + &self.lanes.subject[row] + } + + /// Object of `row`, verbatim from the artifact. + pub fn row_object(&self, row: usize) -> &str { + &self.lanes.object[row] + } + + /// `(file, start, end)` of `row`, `None` when the row has no site (`-`). + pub fn site(&self, row: usize) -> Option<(&str, u32, u32)> { + let (f, s, e) = ( + self.lanes.file.get(row)?, + self.lanes.start[row], + self.lanes.end[row], + ); + if f == "-" || s == NO_SPAN || e == NO_SPAN { + None + } else { + Some((f.as_str(), s, e)) + } + } + + /// The cube holding the six relations. + pub fn cube(&self) -> &Cube { + &self.cube + } + + /// The cube, mutably (views are built through `&mut`). + pub fn cube_mut(&mut self) -> &mut Cube { + &mut self.cube + } + + /// The cube and the map table it reads under, borrowed together: views are + /// built through `&mut Cube` and every view constructor needs the table. + pub(crate) fn cube_and_table_mut(&mut self) -> (&mut Cube, &MapTable) { + (&mut self.cube, &self.table) + } + + /// The tile map table the cube reads under. + pub fn table(&self) -> &MapTable { + &self.table + } + + /// Subjects of DEFINES rows. + pub fn fns(&self) -> &[u64] { + &self.masks.fns + } + /// Subjects of PUBLIC rows. + pub fn public(&self) -> &[u64] { + &self.masks.public + } + /// Subjects of IMPLEMENTS rows. + pub fn impl_subjects(&self) -> &[u64] { + &self.masks.impl_subjects + } + /// Subjects of SYNTHETIC rows. + pub fn synthetic(&self) -> &[u64] { + &self.masks.synthetic + } + /// Objects of REFERENCES rows. + pub fn referenced(&self) -> &[u64] { + &self.masks.referenced + } + /// Objects of CALLS_RESOLVED rows. + pub fn called(&self) -> &[u64] { + &self.masks.called + } + /// `Trait::method` objects of MAY_DISPATCH rows. + pub fn may_targets(&self) -> &[u64] { + &self.masks.may_targets + } + /// Subjects of UNRESOLVED_CALL rows. + pub fn dynamic_callers(&self) -> &[u64] { + &self.masks.dynamic_callers + } + /// Symbols whose first path segment names a crate with a CRATE row. + pub fn stored_owner(&self) -> &[u64] { + &self.masks.stored_owner + } + + /// `edge⁺` of [`RCONF`] or [`RPOSS`], computed once and cached. Any other + /// relation is `UnknownRel`; more than 64 positions is `ClosureWidth` + /// (the cube's limit). + pub fn closure(&self, rel: RelId) -> Result<&(Closure, ClosureStats), CubeRefusal> { + let cell = match rel { + RCONF => &self.rconf, + RPOSS => &self.rposs, + other => return Err(CubeRefusal::UnknownRel(other)), + }; + if let Some(c) = cell.get() { + return Ok(c); + } + let computed = self.cube.closure(&self.table, rel)?; + Ok(cell.get_or_init(|| computed)) + } +} diff --git a/crates/lance-graph-codegraph/src/lib.rs b/crates/lance-graph-codegraph/src/lib.rs new file mode 100644 index 000000000..aa97fb89c --- /dev/null +++ b/crates/lance-graph-codegraph/src/lib.rs @@ -0,0 +1,11 @@ +//! Codegraph queries and structural diagnostics over the ruff_charon store +//! artifacts (`*.rows.tsv`), folded through the xlog cube. This crate reads +//! the artifact format itself and has no dependency on ruff_charon. +#![forbid(unsafe_code)] + +pub mod artifact; +pub mod diag; +pub mod graph; +pub mod query; + +pub use graph::{Codegraph, IntakeRefusal, Pos, RunStatus}; diff --git a/crates/lance-graph-codegraph/src/query.rs b/crates/lance-graph-codegraph/src/query.rs new file mode 100644 index 000000000..29150e1a1 --- /dev/null +++ b/crates/lance-graph-codegraph/src/query.rs @@ -0,0 +1,284 @@ +//! Graded codegraph queries. Every answer is a mask over symbol positions (or +//! over artifact rows); no names and no tuples are built. +//! +//! Grading: **confirmed** means a `CALLS_RESOLVED` / `REFERENCES` row backs +//! it. **possible** means only `MAY_DISPATCH` (plus `IMPLEMENTS`) backs it; a +//! `MAY_DISPATCH` row is never confirmed. **unknown** means a dynamic call +//! (`UNRESOLVED_CALL`, the `?dynamic` position) or a function-pointer +//! reference could reach it; it is never reported as "unaffected". + +use std::collections::HashSet; + +use lance_graph_xlog::cube::{CubeRefusal, RelId}; +use ndarray::simd::{mask_and, mask_andnot_assign, mask_or_assign}; + +use crate::graph::{Codegraph, Pos, CALLS, IMPL, MAY, NODE, RCONF, RPOSS}; + +/// An answer in three grades, each a position mask of `words()` words. +/// `confirmed` is backed by resolved rows only; `possible` by `MAY_DISPATCH` +/// (never confirmed); `unknown` is reachable through a dynamic call. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Graded { + /// Backed by `CALLS_RESOLVED` / `REFERENCES` rows. + pub confirmed: Vec, + /// Backed only by `MAY_DISPATCH` through an `IMPLEMENTS` edge. + pub possible: Vec, + /// Reachable by a dynamic call or function pointer; not decidable. + pub unknown: Vec, +} + +/// A graded caller/callee answer. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Calls { + /// The three grades. + pub graded: Graded, + /// Confirmed call sites (`CALLS_RESOLVED` rows) behind `graded.confirmed`. + pub sites: u64, +} + +/// Rows that mention a symbol, as a row mask. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Sites { + /// Row mask, `rows().div_ceil(64)` words. + pub rows: Vec, + /// Rows that are confirmed (`CALLS_RESOLVED` / `REFERENCES`). + pub confirmed: u64, + /// Rows that are only possible (`MAY_DISPATCH` through an impl). + pub possible: u64, +} + +/// Why a query was refused. +#[derive(Debug)] +pub enum QueryRefusal { + /// The closure refused: more than 64 positions. The answer is UNKNOWN, + /// never "unaffected". + ClosureWidth(usize), + /// Any other cube refusal. + Cube(CubeRefusal), +} + +fn refuse(e: CubeRefusal) -> QueryRefusal { + match e { + CubeRefusal::ClosureWidth(n) => QueryRefusal::ClosureWidth(n), + other => QueryRefusal::Cube(other), + } +} + +fn has(mask: &[u64], p: Pos) -> bool { + mask.get(usize::from(p) / 64) + .is_some_and(|w| w >> (p % 64) & 1 == 1) +} + +fn intersects(a: &[u64], b: &[u64]) -> bool { + a.iter().zip(b).any(|(x, y)| x & y != 0) +} + +fn bits(mask: &[u64]) -> impl Iterator + '_ { + (0..mask.len() * 64) + .filter(|&p| mask[p / 64] >> (p % 64) & 1 == 1) + .filter_map(|p| Pos::try_from(p).ok()) +} + +impl Codegraph { + /// Partners of `v` in `rel`, as a position mask and the derivation count. + /// `into`: positions `x` with `(x, v)`; otherwise positions `y` with + /// `(v, y)`. The join with `NODE` only restricts to interned positions. + /// An out-of-range `v` has no partners: an empty mask and zero. + fn partners(&mut self, rel: RelId, v: Pos, into: bool) -> Result<(Vec, u64), CubeRefusal> { + if self.symbol(v).is_none() { + return Ok((vec![0; self.words()], 0)); + } + let (bound_col, join_col) = if into { (1, 0) } else { (0, 1) }; + let (cube, table) = self.cube_and_table_mut(); + let scan = cube.scan(rel)?; + let bound = cube.bind(table, scan, bound_col, u32::from(v))?; + let node = cube.scan(NODE)?; + let join = cube.join(bound, join_col, node, 0)?; + let mask = cube.support(table, join)?; + let counts = cube.bag_count(table, join)?; + let sites = counts.iter().fold(0u64, |a, &c| { + a.saturating_add(u64::try_from(c).unwrap_or(0)) + }); + Ok((mask, sites)) + } + + /// OR into `acc` the partners of every set bit of `from`. + fn or_partners( + &mut self, + acc: &mut [u64], + from: &[u64], + rel: RelId, + into: bool, + ) -> Result<(), CubeRefusal> { + for t in bits(from) { + let (m, _) = self.partners(rel, t, into)?; + mask_or_assign(acc, &m); + } + Ok(()) + } + + /// Who calls `g`. Confirmed: callers in `CALLS` (`CALLS_RESOLVED` rows), + /// with `sites` their row count. Possible: callers that `MAY_DISPATCH` to + /// a `Trait::method` that `g` implements (never confirmed). Unknown: the + /// dynamic callers, when `g` is an impl or is referenced (a dyn call or + /// fn-pointer call may reach it). An out-of-range `g` yields empty masks. + pub fn callers(&mut self, g: Pos) -> Result { + let w = self.words(); + let (confirmed, sites) = self.partners(CALLS, g, true).map_err(refuse)?; + + let (traits, _) = self.partners(IMPL, g, false).map_err(refuse)?; + let mut possible = vec![0u64; w]; + self.or_partners(&mut possible, &traits, MAY, true) + .map_err(refuse)?; + mask_andnot_assign(&mut possible, &confirmed); + + let mut unknown = vec![0u64; w]; + let reachable = has(self.impl_subjects(), g) || has(self.referenced(), g); + if reachable { + unknown.copy_from_slice(self.dynamic_callers()); + let mut seen = confirmed.clone(); + mask_or_assign(&mut seen, &possible); + mask_andnot_assign(&mut unknown, &seen); + } + Ok(Calls { + graded: Graded { + confirmed, + possible, + unknown, + }, + sites, + }) + } + + /// What `f` calls. Confirmed: callees in `CALLS`, `sites` their row + /// count. Possible: impls of every `Trait::method` that `f` may dispatch + /// to (never confirmed). Unknown: the `?dynamic` position, when `f` has an + /// unresolved call. An out-of-range `f` yields empty masks. + pub fn callees(&mut self, f: Pos) -> Result { + let w = self.words(); + let (confirmed, sites) = self.partners(CALLS, f, false).map_err(refuse)?; + + let (traits, _) = self.partners(MAY, f, false).map_err(refuse)?; + let mut possible = vec![0u64; w]; + self.or_partners(&mut possible, &traits, IMPL, true) + .map_err(refuse)?; + mask_andnot_assign(&mut possible, &confirmed); + + let mut unknown = vec![0u64; w]; + if let Some(d) = self.pos("?dynamic") { + if self.symbol(f).is_some() && has(self.dynamic_callers(), f) { + unknown[usize::from(d) / 64] |= 1 << (d % 64); + } + } + Ok(Calls { + graded: Graded { + confirmed, + possible, + unknown, + }, + sites, + }) + } + + /// Artifact rows that refer to `g`. Confirmed: `CALLS_RESOLVED` and + /// `REFERENCES` rows whose object is `g`. Possible: `MAY_DISPATCH` rows + /// whose `Trait::method` target (the part before " via ") is implemented + /// by `g`; never confirmed. This is a scan over the row lanes producing a + /// row mask, not a tuple decode. An out-of-range `g` yields no rows. + pub fn references(&self, g: Pos) -> Sites { + let rows = self.rows(); + let mut mask = vec![0u64; rows.div_ceil(64)]; + let (mut confirmed, mut possible) = (0u64, 0u64); + let Some(name) = self.symbol(g) else { + return Sites { + rows: mask, + confirmed, + possible, + }; + }; + // The Trait::method nodes `g` implements, read from the same lanes. + let impls: HashSet<&str> = (0..rows) + .filter(|&i| self.row_predicate(i) == "IMPLEMENTS" && self.row_subject(i) == name) + .map(|i| self.row_object(i)) + .collect(); + for i in 0..rows { + let object = self.row_object(i); + let hit = match self.row_predicate(i) { + "CALLS_RESOLVED" | "REFERENCES" if object == name => { + confirmed += 1; + true + } + "MAY_DISPATCH" + if impls.contains(object.split(" via ").next().unwrap_or(object)) => + { + possible += 1; + true + } + _ => false, + }; + if hit { + mask[i / 64] |= 1 << (i % 64); + } + } + Sites { + rows: mask, + confirmed, + possible, + } + } + + /// Functions affected if `g` changes, from the reverse closures. + /// Confirmed: functions in `RCONF⁺` of `g`. Possible: in `RPOSS⁺` but not + /// confirmed (dispatch edges). Unknown: when `g`, or anything it reaches + /// in `RPOSS⁺`, is an impl or referenced and a `?dynamic` position exists, + /// the dynamic callers (`RPOSS⁺` of `?dynamic`) not already graded. All + /// masks are restricted to functions. A closure wider than 64 positions + /// is refused as `ClosureWidth`; an out-of-range `g` yields empty masks. + pub fn impact(&self, g: Pos) -> Result { + let w = self.words(); + let cc = &self.closure(RCONF).map_err(refuse)?.0; + let cp = &self.closure(RPOSS).map_err(refuse)?.0; + let row = |c: &lance_graph_xlog::cube::Closure, p: Pos| -> Vec { + let mut m = vec![0u64; w]; + if let (Some(r), Some(slot)) = (c.rows().get(usize::from(p)), m.first_mut()) { + *slot = *r; + } + m + }; + if self.symbol(g).is_none() { + let z = vec![0u64; w]; + return Ok(Graded { + confirmed: z.clone(), + possible: z.clone(), + unknown: z, + }); + } + + let mut confirmed = vec![0u64; w]; + mask_and(&row(cc, g), self.fns(), &mut confirmed); + + let reach_poss = row(cp, g); + let mut possible = vec![0u64; w]; + mask_and(&reach_poss, self.fns(), &mut possible); + mask_andnot_assign(&mut possible, &confirmed); + + let mut unknown = vec![0u64; w]; + if let Some(d) = self.pos("?dynamic") { + let mut reach = reach_poss; + reach[usize::from(g) / 64] |= 1 << (g % 64); + let mut dyn_reachable = self.impl_subjects().to_vec(); + mask_or_assign(&mut dyn_reachable, self.referenced()); + if intersects(&reach, &dyn_reachable) { + mask_and(&row(cp, d), self.fns(), &mut unknown); + let mut seen = confirmed.clone(); + mask_or_assign(&mut seen, &possible); + mask_andnot_assign(&mut unknown, &seen); + } + } + Ok(Graded { + confirmed, + possible, + unknown, + }) + } +} diff --git a/crates/lance-graph-codegraph/tests/data/README.md b/crates/lance-graph-codegraph/tests/data/README.md new file mode 100644 index 000000000..22bd2f79f --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/README.md @@ -0,0 +1,8 @@ +# lance-graph-codegraph test data + +Store artifacts (rows v2) written by ruff `crates/ruff_charon` (`ruff_charon import `, ruff PR #132, merged `ed8fd4194f`) from the LLBC in ruff `crates/ruff_charon/tests/data`. That LLBC comes from the AdaWorldAPI Charon fork `c69e231` over `crates/sag-ra-probe/fixtures/codegraph`. + +- `rev1/`: `cg_lib` and `cg_app` at rev1. +- `rev2/`: both at rev2 (deletes `triple` and `area_dyn`, drops the `area_dyn` call). +- `mixed/`: `cg_lib` at rev2 and `cg_app` at rev1, so `cg_app::run → cg_lib::area_dyn` dangles (`ruff_charon dangling` reports exactly that edge). +- `fnref/`: the one-file crate `fnref` (a function used only as a pointer, an unused private function, a call through a pointer argument). diff --git a/crates/lance-graph-codegraph/tests/data/fnref/fnref.files.tsv b/crates/lance-graph-codegraph/tests/data/fnref/fnref.files.tsv new file mode 100644 index 000000000..59f04ad0b --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/fnref/fnref.files.tsv @@ -0,0 +1,3 @@ +#version 1 +#schema path fnv64 +fnref.rs fc6a742005829ba6 diff --git a/crates/lance-graph-codegraph/tests/data/fnref/fnref.residuals.tsv b/crates/lance-graph-codegraph/tests/data/fnref/fnref.residuals.tsv new file mode 100644 index 000000000..5ff2b1d84 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/fnref/fnref.residuals.tsv @@ -0,0 +1,2 @@ +#version 1 +#schema reason subject diff --git a/crates/lance-graph-codegraph/tests/data/fnref/fnref.rows.tsv b/crates/lance-graph-codegraph/tests/data/fnref/fnref.rows.tsv new file mode 100644 index 000000000..0aa6f4f38 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/fnref/fnref.rows.tsv @@ -0,0 +1,11 @@ +#version 2 +#schema predicate subject object grade file start end +DEFINES fnref::helper fnref::helper COMPILER fnref.rs 133 171 +DEFINES fnref::unused_private fnref::unused_private COMPILER fnref.rs 172 208 +DEFINES fnref::take fnref::take COMPILER fnref.rs 209 255 +PUBLIC fnref::take fnref::take COMPILER fnref.rs 209 255 +REFERENCES fnref::take fnref::helper COMPILER fnref.rs 247 253 +DEFINES fnref::apply fnref::apply COMPILER fnref.rs 256 307 +PUBLIC fnref::apply fnref::apply COMPILER fnref.rs 256 307 +UNRESOLVED_CALL fnref::apply dynamic COMPILER fnref.rs 301 305 +CRATE fnref COMPLETE COMPILER - - - diff --git a/crates/lance-graph-codegraph/tests/data/mixed/cg_app.files.tsv b/crates/lance-graph-codegraph/tests/data/mixed/cg_app.files.tsv new file mode 100644 index 000000000..22c8c4dc6 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/mixed/cg_app.files.tsv @@ -0,0 +1,3 @@ +#version 1 +#schema path fnv64 +cg_app/src/lib.rs 0effe8b102e60ce2 diff --git a/crates/lance-graph-codegraph/tests/data/mixed/cg_app.residuals.tsv b/crates/lance-graph-codegraph/tests/data/mixed/cg_app.residuals.tsv new file mode 100644 index 000000000..5ff2b1d84 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/mixed/cg_app.residuals.tsv @@ -0,0 +1,2 @@ +#version 1 +#schema reason subject diff --git a/crates/lance-graph-codegraph/tests/data/mixed/cg_app.rows.tsv b/crates/lance-graph-codegraph/tests/data/mixed/cg_app.rows.tsv new file mode 100644 index 000000000..172b2267d --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/mixed/cg_app.rows.tsv @@ -0,0 +1,9 @@ +#version 2 +#schema predicate subject object grade file start end +DEFINES cg_app::run cg_app::run COMPILER cg_app/src/lib.rs 52 202 +PUBLIC cg_app::run cg_app::run COMPILER cg_app/src/lib.rs 52 202 +CALLS_RESOLVED cg_app::run cg_lib::area_of COMPILER cg_app/src/lib.rs 121 140 +CALLS_RESOLVED cg_app::run cg_lib::area_dyn COMPILER cg_app/src/lib.rs 143 163 +CALLS_RESOLVED cg_app::run cg_lib::shapes::impl_Shape_for_Square::area COMPILER cg_app/src/lib.rs 166 174 +CALLS_RESOLVED cg_app::run cg_lib::util::double COMPILER cg_app/src/lib.rs 177 200 +CRATE cg_app COMPLETE COMPILER - - - diff --git a/crates/lance-graph-codegraph/tests/data/mixed/cg_lib.files.tsv b/crates/lance-graph-codegraph/tests/data/mixed/cg_lib.files.tsv new file mode 100644 index 000000000..c2c978b4f --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/mixed/cg_lib.files.tsv @@ -0,0 +1,6 @@ +#version 1 +#schema path fnv64 +cg_lib/src/lib.rs ad16bfdf300e3739 +cg_lib/src/macro_use.rs a26281fdc227b28d +cg_lib/src/shapes.rs 95d410730d1c194b +cg_lib/src/util.rs b48c0582b6c4f7ed diff --git a/crates/lance-graph-codegraph/tests/data/mixed/cg_lib.residuals.tsv b/crates/lance-graph-codegraph/tests/data/mixed/cg_lib.residuals.tsv new file mode 100644 index 000000000..5ff2b1d84 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/mixed/cg_lib.residuals.tsv @@ -0,0 +1,2 @@ +#version 1 +#schema reason subject diff --git a/crates/lance-graph-codegraph/tests/data/mixed/cg_lib.rows.tsv b/crates/lance-graph-codegraph/tests/data/mixed/cg_lib.rows.tsv new file mode 100644 index 000000000..42b4b8ad0 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/mixed/cg_lib.rows.tsv @@ -0,0 +1,29 @@ +#version 2 +#schema predicate subject object grade file start end +DEFINES cg_lib::area_of cg_lib::area_of COMPILER cg_lib/src/lib.rs 81 144 +PUBLIC cg_lib::area_of cg_lib::area_of COMPILER cg_lib/src/lib.rs 81 144 +MAY_DISPATCH cg_lib::area_of cg_lib::shapes::Shape::area via Clause COMPILER cg_lib/src/lib.rs 134 142 +DEFINES cg_lib::macro_use::checked cg_lib::macro_use::checked COMPILER cg_lib/src/macro_use.rs 31 90 +PUBLIC cg_lib::macro_use::checked cg_lib::macro_use::checked COMPILER cg_lib/src/macro_use.rs 31 90 +CALLS_RESOLVED cg_lib::macro_use::checked cg_lib::util::twice COMPILER cg_lib/src/macro_use.rs 72 77 +CALLS_RESOLVED cg_lib::macro_use::checked core::panicking::assert_failed COMPILER cg_lib/src/macro_use.rs 61 81 +DEFINES cg_lib::util::double cg_lib::util::double COMPILER cg_lib/src/util.rs 28 70 +PUBLIC cg_lib::util::double cg_lib::util::double COMPILER cg_lib/src/util.rs 28 70 +DEFINES cg_lib::util::twice cg_lib::util::twice COMPILER cg_lib/src/util.rs 72 117 +PUBLIC cg_lib::util::twice cg_lib::util::twice COMPILER cg_lib/src/util.rs 72 117 +CALLS_RESOLVED cg_lib::util::twice cg_lib::util::double COMPILER cg_lib/src/util.rs 106 115 +DEFINES cg_lib::util::unit cg_lib::util::unit COMPILER cg_lib/src/util.rs 119 160 +PUBLIC cg_lib::util::unit cg_lib::util::unit COMPILER cg_lib/src/util.rs 119 160 +DEFINES cg_lib::util::private_unused cg_lib::util::private_unused COMPILER cg_lib/src/util.rs 162 198 +DEFINES cg_lib::shapes::impl_Shape_for_Square::area cg_lib::shapes::impl_Shape_for_Square::area COMPILER cg_lib/src/shapes.rs 167 232 +PUBLIC cg_lib::shapes::impl_Shape_for_Square::area cg_lib::shapes::impl_Shape_for_Square::area COMPILER cg_lib/src/shapes.rs 167 232 +IMPLEMENTS cg_lib::shapes::impl_Shape_for_Square::area cg_lib::shapes::Shape::area COMPILER cg_lib/src/shapes.rs 167 232 +CALLS_RESOLVED cg_lib::shapes::impl_Shape_for_Square::area cg_lib::util::double COMPILER cg_lib/src/shapes.rs 199 213 +DEFINES cg_lib::shapes::impl_Shape_for_Rect::area cg_lib::shapes::impl_Shape_for_Rect::area COMPILER cg_lib/src/shapes.rs 262 315 +PUBLIC cg_lib::shapes::impl_Shape_for_Rect::area cg_lib::shapes::impl_Shape_for_Rect::area COMPILER cg_lib/src/shapes.rs 262 315 +IMPLEMENTS cg_lib::shapes::impl_Shape_for_Rect::area cg_lib::shapes::Shape::area COMPILER cg_lib/src/shapes.rs 262 315 +DEFINES cg_lib::shapes::{Square}::side cg_lib::shapes::{Square}::side COMPILER cg_lib/src/shapes.rs 337 385 +PUBLIC cg_lib::shapes::{Square}::side cg_lib::shapes::{Square}::side COMPILER cg_lib/src/shapes.rs 337 385 +DEFINES cg_lib::macro_use::checked::{promoted_const} cg_lib::macro_use::checked::{promoted_const} COMPILER cg_lib/src/macro_use.rs 61 81 +SYNTHETIC cg_lib::macro_use::checked::{promoted_const} cg_lib::macro_use::checked::{promoted_const} COMPILER cg_lib/src/macro_use.rs 61 81 +CRATE cg_lib COMPLETE COMPILER - - - diff --git a/crates/lance-graph-codegraph/tests/data/rev1/cg_app.files.tsv b/crates/lance-graph-codegraph/tests/data/rev1/cg_app.files.tsv new file mode 100644 index 000000000..22c8c4dc6 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev1/cg_app.files.tsv @@ -0,0 +1,3 @@ +#version 1 +#schema path fnv64 +cg_app/src/lib.rs 0effe8b102e60ce2 diff --git a/crates/lance-graph-codegraph/tests/data/rev1/cg_app.residuals.tsv b/crates/lance-graph-codegraph/tests/data/rev1/cg_app.residuals.tsv new file mode 100644 index 000000000..5ff2b1d84 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev1/cg_app.residuals.tsv @@ -0,0 +1,2 @@ +#version 1 +#schema reason subject diff --git a/crates/lance-graph-codegraph/tests/data/rev1/cg_app.rows.tsv b/crates/lance-graph-codegraph/tests/data/rev1/cg_app.rows.tsv new file mode 100644 index 000000000..172b2267d --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev1/cg_app.rows.tsv @@ -0,0 +1,9 @@ +#version 2 +#schema predicate subject object grade file start end +DEFINES cg_app::run cg_app::run COMPILER cg_app/src/lib.rs 52 202 +PUBLIC cg_app::run cg_app::run COMPILER cg_app/src/lib.rs 52 202 +CALLS_RESOLVED cg_app::run cg_lib::area_of COMPILER cg_app/src/lib.rs 121 140 +CALLS_RESOLVED cg_app::run cg_lib::area_dyn COMPILER cg_app/src/lib.rs 143 163 +CALLS_RESOLVED cg_app::run cg_lib::shapes::impl_Shape_for_Square::area COMPILER cg_app/src/lib.rs 166 174 +CALLS_RESOLVED cg_app::run cg_lib::util::double COMPILER cg_app/src/lib.rs 177 200 +CRATE cg_app COMPLETE COMPILER - - - diff --git a/crates/lance-graph-codegraph/tests/data/rev1/cg_lib.files.tsv b/crates/lance-graph-codegraph/tests/data/rev1/cg_lib.files.tsv new file mode 100644 index 000000000..9981bdb80 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev1/cg_lib.files.tsv @@ -0,0 +1,6 @@ +#version 1 +#schema path fnv64 +cg_lib/src/lib.rs d6caded9012e13ab +cg_lib/src/macro_use.rs b1eefec3d65a4f9a +cg_lib/src/shapes.rs e03268a07c7564ba +cg_lib/src/util.rs bf9b50c91bfbd98a diff --git a/crates/lance-graph-codegraph/tests/data/rev1/cg_lib.residuals.tsv b/crates/lance-graph-codegraph/tests/data/rev1/cg_lib.residuals.tsv new file mode 100644 index 000000000..5ff2b1d84 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev1/cg_lib.residuals.tsv @@ -0,0 +1,2 @@ +#version 1 +#schema reason subject diff --git a/crates/lance-graph-codegraph/tests/data/rev1/cg_lib.rows.tsv b/crates/lance-graph-codegraph/tests/data/rev1/cg_lib.rows.tsv new file mode 100644 index 000000000..27aa71c8e --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev1/cg_lib.rows.tsv @@ -0,0 +1,31 @@ +#version 2 +#schema predicate subject object grade file start end +DEFINES cg_lib::area_of cg_lib::area_of COMPILER cg_lib/src/lib.rs 81 144 +PUBLIC cg_lib::area_of cg_lib::area_of COMPILER cg_lib/src/lib.rs 81 144 +MAY_DISPATCH cg_lib::area_of cg_lib::shapes::Shape::area via Clause COMPILER cg_lib/src/lib.rs 134 142 +DEFINES cg_lib::area_dyn cg_lib::area_dyn COMPILER cg_lib/src/lib.rs 146 208 +PUBLIC cg_lib::area_dyn cg_lib::area_dyn COMPILER cg_lib/src/lib.rs 146 208 +UNRESOLVED_CALL cg_lib::area_dyn dynamic COMPILER cg_lib/src/lib.rs 198 206 +DEFINES cg_lib::macro_use::checked cg_lib::macro_use::checked COMPILER cg_lib/src/macro_use.rs 32 91 +PUBLIC cg_lib::macro_use::checked cg_lib::macro_use::checked COMPILER cg_lib/src/macro_use.rs 32 91 +CALLS_RESOLVED cg_lib::macro_use::checked cg_lib::util::triple COMPILER cg_lib/src/macro_use.rs 73 78 +CALLS_RESOLVED cg_lib::macro_use::checked core::panicking::assert_failed COMPILER cg_lib/src/macro_use.rs 62 82 +DEFINES cg_lib::util::double cg_lib::util::double COMPILER cg_lib/src/util.rs 28 70 +PUBLIC cg_lib::util::double cg_lib::util::double COMPILER cg_lib/src/util.rs 28 70 +DEFINES cg_lib::util::triple cg_lib::util::triple COMPILER cg_lib/src/util.rs 72 114 +PUBLIC cg_lib::util::triple cg_lib::util::triple COMPILER cg_lib/src/util.rs 72 114 +DEFINES cg_lib::util::unit cg_lib::util::unit COMPILER cg_lib/src/util.rs 116 157 +PUBLIC cg_lib::util::unit cg_lib::util::unit COMPILER cg_lib/src/util.rs 116 157 +DEFINES cg_lib::util::private_unused cg_lib::util::private_unused COMPILER cg_lib/src/util.rs 159 195 +DEFINES cg_lib::shapes::impl_Shape_for_Square::area cg_lib::shapes::impl_Shape_for_Square::area COMPILER cg_lib/src/shapes.rs 192 257 +PUBLIC cg_lib::shapes::impl_Shape_for_Square::area cg_lib::shapes::impl_Shape_for_Square::area COMPILER cg_lib/src/shapes.rs 192 257 +IMPLEMENTS cg_lib::shapes::impl_Shape_for_Square::area cg_lib::shapes::Shape::area COMPILER cg_lib/src/shapes.rs 192 257 +CALLS_RESOLVED cg_lib::shapes::impl_Shape_for_Square::area cg_lib::util::double COMPILER cg_lib/src/shapes.rs 224 238 +DEFINES cg_lib::shapes::impl_Shape_for_Rect::area cg_lib::shapes::impl_Shape_for_Rect::area COMPILER cg_lib/src/shapes.rs 287 340 +PUBLIC cg_lib::shapes::impl_Shape_for_Rect::area cg_lib::shapes::impl_Shape_for_Rect::area COMPILER cg_lib/src/shapes.rs 287 340 +IMPLEMENTS cg_lib::shapes::impl_Shape_for_Rect::area cg_lib::shapes::Shape::area COMPILER cg_lib/src/shapes.rs 287 340 +DEFINES cg_lib::shapes::{Square}::side cg_lib::shapes::{Square}::side COMPILER cg_lib/src/shapes.rs 362 410 +PUBLIC cg_lib::shapes::{Square}::side cg_lib::shapes::{Square}::side COMPILER cg_lib/src/shapes.rs 362 410 +DEFINES cg_lib::macro_use::checked::{promoted_const} cg_lib::macro_use::checked::{promoted_const} COMPILER cg_lib/src/macro_use.rs 62 82 +SYNTHETIC cg_lib::macro_use::checked::{promoted_const} cg_lib::macro_use::checked::{promoted_const} COMPILER cg_lib/src/macro_use.rs 62 82 +CRATE cg_lib COMPLETE COMPILER - - - diff --git a/crates/lance-graph-codegraph/tests/data/rev2/cg_app.files.tsv b/crates/lance-graph-codegraph/tests/data/rev2/cg_app.files.tsv new file mode 100644 index 000000000..d04238ffd --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev2/cg_app.files.tsv @@ -0,0 +1,3 @@ +#version 1 +#schema path fnv64 +cg_app/src/lib.rs f1878b233a4e4a50 diff --git a/crates/lance-graph-codegraph/tests/data/rev2/cg_app.residuals.tsv b/crates/lance-graph-codegraph/tests/data/rev2/cg_app.residuals.tsv new file mode 100644 index 000000000..5ff2b1d84 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev2/cg_app.residuals.tsv @@ -0,0 +1,2 @@ +#version 1 +#schema reason subject diff --git a/crates/lance-graph-codegraph/tests/data/rev2/cg_app.rows.tsv b/crates/lance-graph-codegraph/tests/data/rev2/cg_app.rows.tsv new file mode 100644 index 000000000..b6198d3ba --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev2/cg_app.rows.tsv @@ -0,0 +1,9 @@ +#version 2 +#schema predicate subject object grade file start end +DEFINES cg_app::run cg_app::run COMPILER cg_app/src/lib.rs 52 201 +PUBLIC cg_app::run cg_app::run COMPILER cg_app/src/lib.rs 52 201 +CALLS_RESOLVED cg_app::run cg_lib::area_of COMPILER cg_app/src/lib.rs 121 140 +CALLS_RESOLVED cg_app::run cg_lib::area_of COMPILER cg_app/src/lib.rs 143 162 +CALLS_RESOLVED cg_app::run cg_lib::shapes::impl_Shape_for_Square::area COMPILER cg_app/src/lib.rs 165 173 +CALLS_RESOLVED cg_app::run cg_lib::util::double COMPILER cg_app/src/lib.rs 176 199 +CRATE cg_app COMPLETE COMPILER - - - diff --git a/crates/lance-graph-codegraph/tests/data/rev2/cg_lib.files.tsv b/crates/lance-graph-codegraph/tests/data/rev2/cg_lib.files.tsv new file mode 100644 index 000000000..c2c978b4f --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev2/cg_lib.files.tsv @@ -0,0 +1,6 @@ +#version 1 +#schema path fnv64 +cg_lib/src/lib.rs ad16bfdf300e3739 +cg_lib/src/macro_use.rs a26281fdc227b28d +cg_lib/src/shapes.rs 95d410730d1c194b +cg_lib/src/util.rs b48c0582b6c4f7ed diff --git a/crates/lance-graph-codegraph/tests/data/rev2/cg_lib.residuals.tsv b/crates/lance-graph-codegraph/tests/data/rev2/cg_lib.residuals.tsv new file mode 100644 index 000000000..5ff2b1d84 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev2/cg_lib.residuals.tsv @@ -0,0 +1,2 @@ +#version 1 +#schema reason subject diff --git a/crates/lance-graph-codegraph/tests/data/rev2/cg_lib.rows.tsv b/crates/lance-graph-codegraph/tests/data/rev2/cg_lib.rows.tsv new file mode 100644 index 000000000..42b4b8ad0 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/rev2/cg_lib.rows.tsv @@ -0,0 +1,29 @@ +#version 2 +#schema predicate subject object grade file start end +DEFINES cg_lib::area_of cg_lib::area_of COMPILER cg_lib/src/lib.rs 81 144 +PUBLIC cg_lib::area_of cg_lib::area_of COMPILER cg_lib/src/lib.rs 81 144 +MAY_DISPATCH cg_lib::area_of cg_lib::shapes::Shape::area via Clause COMPILER cg_lib/src/lib.rs 134 142 +DEFINES cg_lib::macro_use::checked cg_lib::macro_use::checked COMPILER cg_lib/src/macro_use.rs 31 90 +PUBLIC cg_lib::macro_use::checked cg_lib::macro_use::checked COMPILER cg_lib/src/macro_use.rs 31 90 +CALLS_RESOLVED cg_lib::macro_use::checked cg_lib::util::twice COMPILER cg_lib/src/macro_use.rs 72 77 +CALLS_RESOLVED cg_lib::macro_use::checked core::panicking::assert_failed COMPILER cg_lib/src/macro_use.rs 61 81 +DEFINES cg_lib::util::double cg_lib::util::double COMPILER cg_lib/src/util.rs 28 70 +PUBLIC cg_lib::util::double cg_lib::util::double COMPILER cg_lib/src/util.rs 28 70 +DEFINES cg_lib::util::twice cg_lib::util::twice COMPILER cg_lib/src/util.rs 72 117 +PUBLIC cg_lib::util::twice cg_lib::util::twice COMPILER cg_lib/src/util.rs 72 117 +CALLS_RESOLVED cg_lib::util::twice cg_lib::util::double COMPILER cg_lib/src/util.rs 106 115 +DEFINES cg_lib::util::unit cg_lib::util::unit COMPILER cg_lib/src/util.rs 119 160 +PUBLIC cg_lib::util::unit cg_lib::util::unit COMPILER cg_lib/src/util.rs 119 160 +DEFINES cg_lib::util::private_unused cg_lib::util::private_unused COMPILER cg_lib/src/util.rs 162 198 +DEFINES cg_lib::shapes::impl_Shape_for_Square::area cg_lib::shapes::impl_Shape_for_Square::area COMPILER cg_lib/src/shapes.rs 167 232 +PUBLIC cg_lib::shapes::impl_Shape_for_Square::area cg_lib::shapes::impl_Shape_for_Square::area COMPILER cg_lib/src/shapes.rs 167 232 +IMPLEMENTS cg_lib::shapes::impl_Shape_for_Square::area cg_lib::shapes::Shape::area COMPILER cg_lib/src/shapes.rs 167 232 +CALLS_RESOLVED cg_lib::shapes::impl_Shape_for_Square::area cg_lib::util::double COMPILER cg_lib/src/shapes.rs 199 213 +DEFINES cg_lib::shapes::impl_Shape_for_Rect::area cg_lib::shapes::impl_Shape_for_Rect::area COMPILER cg_lib/src/shapes.rs 262 315 +PUBLIC cg_lib::shapes::impl_Shape_for_Rect::area cg_lib::shapes::impl_Shape_for_Rect::area COMPILER cg_lib/src/shapes.rs 262 315 +IMPLEMENTS cg_lib::shapes::impl_Shape_for_Rect::area cg_lib::shapes::Shape::area COMPILER cg_lib/src/shapes.rs 262 315 +DEFINES cg_lib::shapes::{Square}::side cg_lib::shapes::{Square}::side COMPILER cg_lib/src/shapes.rs 337 385 +PUBLIC cg_lib::shapes::{Square}::side cg_lib::shapes::{Square}::side COMPILER cg_lib/src/shapes.rs 337 385 +DEFINES cg_lib::macro_use::checked::{promoted_const} cg_lib::macro_use::checked::{promoted_const} COMPILER cg_lib/src/macro_use.rs 61 81 +SYNTHETIC cg_lib::macro_use::checked::{promoted_const} cg_lib::macro_use::checked::{promoted_const} COMPILER cg_lib/src/macro_use.rs 61 81 +CRATE cg_lib COMPLETE COMPILER - - - diff --git a/crates/lance-graph-codegraph/tests/guards.rs b/crates/lance-graph-codegraph/tests/guards.rs new file mode 100644 index 000000000..f7f59f529 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/guards.rs @@ -0,0 +1,187 @@ +//! Refusal and robustness tests over stores written into a temp dir. + +use std::fmt::Write as _; +use std::path::PathBuf; + +use lance_graph_codegraph::diag::{CrateKind, Run}; +use lance_graph_codegraph::graph::IntakeRefusal; +use lance_graph_codegraph::query::QueryRefusal; +use lance_graph_codegraph::{Codegraph, Pos}; + +const HEADER: &str = "#version 2\n#schema predicate\tsubject\tobject\tgrade\tfile\tstart\tend\n"; + +/// Write `content` as `store.rows.tsv` in a fresh per-test temp dir. +fn write(name: &str, content: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!("lgcg_{name}_{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("store.rows.tsv"), content).unwrap(); + dir +} + +fn row(pred: &str, s: &str, o: &str) -> String { + format!("{pred}\t{s}\t{o}\tCOMPILER\tf.rs\t1\t2\n") +} + +fn pop(mask: &[u64]) -> u32 { + mask.iter().map(|w| w.count_ones()).sum() +} + +/// Fails if a 70-symbol chain is refused at open, or if impact does not +/// refuse with ClosureWidth(70) (the answer must be UNKNOWN, not empty). +#[test] +fn closure_wider_than_64_is_refused() { + let mut text = String::from(HEADER); + for i in 0..70 { + text += &row("DEFINES", &format!("chain::f{i}"), &format!("chain::f{i}")); + } + for i in 0..69 { + text += &row( + "CALLS_RESOLVED", + &format!("chain::f{i}"), + &format!("chain::f{}", i + 1), + ); + } + text += "CRATE\tchain\tCOMPLETE\tCOMPILER\t-\t-\t-\n"; + let dir = write("chain", &text); + let g = Codegraph::open(&dir).expect("70 symbols fit a tile"); + let f0 = g.pos("chain::f0").unwrap(); + let r = g.impact(f0); + assert!(matches!(r, Err(QueryRefusal::ClosureWidth(70))), "{r:?}"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Fails if 300 symbols are accepted into one 256-position tile. +#[test] +fn too_many_symbols_is_refused() { + let mut text = String::from(HEADER); + for i in 0..300 { + text += &row("DEFINES", &format!("big::s{i}"), &format!("big::s{i}")); + } + let dir = write("many", &text); + let r = Codegraph::open(&dir); + assert!( + matches!(r, Err(IntakeRefusal::TooMany(300))), + "{:?}", + r.err() + ); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Fails if a version-1 file is read instead of refused. +#[test] +fn old_version_is_refused() { + let text = "#version 1\n#schema predicate\tsubject\tobject\tgrade\tfile\tstart\tend\n"; + let dir = write("v1", text); + let r = Codegraph::open(&dir); + assert!( + matches!(r, Err(IntakeRefusal::Version { found: 1 })), + "{:?}", + r.err() + ); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Fails if a data line with a missing field is accepted or misread. +#[test] +fn short_line_is_an_arity_refusal() { + let mut text = String::from(HEADER); + text += &row("DEFINES", "a::f", "a::f"); + text += "DEFINES\ta::g\ta::g\tCOMPILER\tf.rs\t1\n"; + let dir = write("arity", &text); + let r = Codegraph::open(&dir); + assert!( + matches!(r, Err(IntakeRefusal::Arity { .. })), + "{:?}", + r.err() + ); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Fails if column order matters: the same rows with permuted columns must +/// give identical symbols and masks at every position. +#[test] +fn permuted_columns_read_identically() { + let rows = [ + ["DEFINES", "lib::a", "lib::a"], + ["PUBLIC", "lib::a", "lib::a"], + ["DEFINES", "lib::b", "lib::b"], + ["CALLS_RESOLVED", "lib::a", "lib::b"], + ["MAY_DISPATCH", "lib::a", "lib::T::m via Clause"], + ["IMPLEMENTS", "lib::b", "lib::T::m"], + ["CRATE", "lib", "COMPLETE"], + ]; + let mut plain = String::from(HEADER); + let mut perm = + String::from("#version 2\n#schema object\tpredicate\tsubject\tgrade\tfile\tstart\tend\n"); + for [p, s, o] in rows { + writeln!(plain, "{p}\t{s}\t{o}\tCOMPILER\tf.rs\t1\t2").unwrap(); + writeln!(perm, "{o}\t{p}\t{s}\tCOMPILER\tf.rs\t1\t2").unwrap(); + } + let da = write("plain", &plain); + let db = write("perm", &perm); + let a = Codegraph::open(&da).unwrap(); + let b = Codegraph::open(&db).unwrap(); + assert_eq!(a.rows(), b.rows()); + assert_eq!(a.words(), b.words()); + assert!(pop(a.fns()) > 0 && pop(a.called()) > 0 && pop(a.may_targets()) > 0); + for pos in 0..=u8::MAX { + let pos: Pos = pos; + assert_eq!(a.symbol(pos), b.symbol(pos)); + } + assert_eq!(a.fns(), b.fns()); + assert_eq!(a.public(), b.public()); + assert_eq!(a.impl_subjects(), b.impl_subjects()); + assert_eq!(a.synthetic(), b.synthetic()); + assert_eq!(a.referenced(), b.referenced()); + assert_eq!(a.called(), b.called()); + assert_eq!(a.may_targets(), b.may_targets()); + assert_eq!(a.dynamic_callers(), b.dynamic_callers()); + assert_eq!(a.stored_owner(), b.stored_owner()); + let _ = std::fs::remove_dir_all(&da); + let _ = std::fs::remove_dir_all(&db); +} + +/// Fails if the CRATE row status does not decide Complete vs Partial for the +/// dead-code run. +#[test] +fn partial_crate_gives_partial_run() { + let build = |status: &str| { + let mut t = String::from(HEADER); + t += &row("DEFINES", "lib::a", "lib::a"); + t += &row("DEFINES", "lib::b", "lib::b"); + t += &row("CALLS_RESOLVED", "lib::a", "lib::b"); + t += &format!("CRATE\tlib\t{status}\tCOMPILER\t-\t-\t-\n"); + t + }; + let dp = write("partial", &build("PARTIAL")); + let dc = write("complete", &build("COMPLETE")); + let p = Codegraph::open(&dp).unwrap(); + let c = Codegraph::open(&dc).unwrap(); + assert_eq!(p.diagnostics(CrateKind::Lib)[0].run, Run::Partial); + assert_eq!(c.diagnostics(CrateKind::Lib)[0].run, Run::Complete); + let _ = std::fs::remove_dir_all(&dp); + let _ = std::fs::remove_dir_all(&dc); +} + +/// Fails if a trait-impl method with no direct caller is reported dead. +/// Charon marks impl methods public whenever the trait is, so the fixtures +/// never reach this case; an impl of a private trait is not public and is +/// called only through dispatch. +#[test] +fn an_uncalled_private_impl_method_is_not_dead() { + let mut t = String::from(HEADER); + t += &row("DEFINES", "lib::impl_T_for_S::m", "lib::impl_T_for_S::m"); + t += &row("IMPLEMENTS", "lib::impl_T_for_S::m", "lib::T::m"); + t += &row("DEFINES", "lib::dead", "lib::dead"); + t += "CRATE\tlib\tCOMPLETE\tCOMPILER\t-\t-\t-\n"; + let d = write("private_impl", &t); + let g = Codegraph::open(&d).unwrap(); + let dead = &g.diagnostics(CrateKind::Lib)[0].positions; + assert_eq!( + *dead, + g.mask_of(&["lib::dead"]), + "the impl method must stay silent" + ); + let _ = std::fs::remove_dir_all(&d); +} diff --git a/crates/lance-graph-codegraph/tests/oracle.rs b/crates/lance-graph-codegraph/tests/oracle.rs new file mode 100644 index 000000000..01df2765e --- /dev/null +++ b/crates/lance-graph-codegraph/tests/oracle.rs @@ -0,0 +1,272 @@ +//! Oracle tests: exact answers over the committed fixture stores, checked by +//! hand against `tests/data/*/*.rows.tsv`. + +use std::path::PathBuf; + +use lance_graph_codegraph::diag::{CrateKind, FindingStatus, Kind, Run}; +use lance_graph_codegraph::{Codegraph, Pos}; + +fn open(store: &str) -> Codegraph { + let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests/data") + .join(store); + Codegraph::open(&dir).expect("fixture store opens") +} + +fn m(g: &Codegraph, names: &[&str]) -> Vec { + g.mask_of(names) +} + +fn p(g: &Codegraph, name: &str) -> Pos { + g.pos(name) + .unwrap_or_else(|| panic!("symbol {name} not in store")) +} + +fn pop(mask: &[u64]) -> u32 { + mask.iter().map(|w| w.count_ones()).sum() +} + +const SQ: &str = "cg_lib::shapes::impl_Shape_for_Square::area"; +const RECT: &str = "cg_lib::shapes::impl_Shape_for_Rect::area"; +const DOUBLE: &str = "cg_lib::util::double"; +const TRIPLE: &str = "cg_lib::util::triple"; +const TWICE: &str = "cg_lib::util::twice"; +const AREA_OF: &str = "cg_lib::area_of"; +const AREA_DYN: &str = "cg_lib::area_dyn"; +const CHECKED: &str = "cg_lib::macro_use::checked"; +const ASSERT_FAILED: &str = "core::panicking::assert_failed"; +const RUN: &str = "cg_app::run"; + +/// Fails if `double`'s confirmed callers are not exactly {Square::area, run}, +/// if it reports 2 sites wrongly, or if any possible/unknown caller appears. +#[test] +fn rev1_callers_of_double() { + let mut g = open("rev1"); + let c = g.callers(p(&g, DOUBLE)).unwrap(); + assert_eq!(c.graded.confirmed, m(&g, &[SQ, RUN])); + assert_eq!(c.sites, 2); + assert_eq!(c.graded.possible, m(&g, &[])); + assert_eq!(c.graded.unknown, m(&g, &[])); +} + +/// Fails if `triple`'s only confirmed caller is not `checked` with 1 site. +#[test] +fn rev1_callers_of_triple() { + let mut g = open("rev1"); + let c = g.callers(p(&g, TRIPLE)).unwrap(); + assert_eq!(c.graded.confirmed, m(&g, &[CHECKED])); + assert_eq!(c.sites, 1); +} + +/// Fails if the dispatch caller `area_of` is promoted to confirmed, or if the +/// dynamic caller `area_dyn` is not unknown. +#[test] +fn rev1_callers_of_square_area_grades_dispatch() { + let mut g = open("rev1"); + let c = g.callers(p(&g, SQ)).unwrap(); + assert_eq!(c.graded.confirmed, m(&g, &[RUN])); + assert_eq!(c.graded.possible, m(&g, &[AREA_OF])); + assert_eq!(c.graded.unknown, m(&g, &[AREA_DYN])); + let area_of_bit = m(&g, &[AREA_OF]); + assert_ne!(c.graded.confirmed, area_of_bit); + assert!(c + .graded + .confirmed + .iter() + .zip(&area_of_bit) + .all(|(a, b)| a & b == 0)); +} + +/// Fails if Rect::area (never called directly) gains a confirmed caller. +#[test] +fn rev1_callers_of_rect_area() { + let mut g = open("rev1"); + let c = g.callers(p(&g, RECT)).unwrap(); + assert_eq!(c.graded.confirmed, m(&g, &[])); + assert_eq!(c.graded.possible, m(&g, &[AREA_OF])); + assert_eq!(c.graded.unknown, m(&g, &[AREA_DYN])); +} + +/// Fails if `run`'s four resolved callees or the 4 call sites are wrong. +#[test] +fn rev1_callees_of_run() { + let mut g = open("rev1"); + let c = g.callees(p(&g, RUN)).unwrap(); + assert_eq!(c.graded.confirmed, m(&g, &[AREA_OF, AREA_DYN, SQ, DOUBLE])); + assert_eq!(c.sites, 4); +} + +/// Fails if `area_of`'s dispatch targets are confirmed or not both possible. +#[test] +fn rev1_callees_of_area_of() { + let mut g = open("rev1"); + let c = g.callees(p(&g, AREA_OF)).unwrap(); + assert_eq!(c.graded.confirmed, m(&g, &[])); + assert_eq!(c.graded.possible, m(&g, &[SQ, RECT])); +} + +/// Fails if `checked`'s callees (including the external assert_failed) differ. +#[test] +fn rev1_callees_of_checked() { + let mut g = open("rev1"); + let c = g.callees(p(&g, CHECKED)).unwrap(); + assert_eq!(c.graded.confirmed, m(&g, &[TRIPLE, ASSERT_FAILED])); +} + +/// Fails if the reference sites to `double` are not 2 confirmed, 0 possible. +#[test] +fn rev1_references_of_double() { + let g = open("rev1"); + let s = g.references(p(&g, DOUBLE)); + assert_eq!(s.confirmed, 2); + assert_eq!(s.possible, 0); + assert_eq!(pop(&s.rows), 2); +} + +/// Fails if the transitive impact of `double` is mis-graded. +#[test] +fn rev1_impact_of_double() { + let g = open("rev1"); + let i = g.impact(p(&g, DOUBLE)).unwrap(); + assert_eq!(i.confirmed, m(&g, &[SQ, RUN])); + assert_eq!(i.possible, m(&g, &[AREA_OF])); + assert_eq!(i.unknown, m(&g, &[AREA_DYN])); +} + +/// Fails if area_of is called once instead of twice (sites 4, popcount 3). +#[test] +fn rev2_callees_of_run_counts_sites_not_positions() { + let mut g = open("rev2"); + let c = g.callees(p(&g, RUN)).unwrap(); + assert_eq!(pop(&c.graded.confirmed), 3); + assert_eq!(c.graded.confirmed, m(&g, &[AREA_OF, SQ, DOUBLE])); + assert_eq!(c.sites, 4); +} + +/// Fails if the two-hop chain checked -> twice -> double is not followed. +#[test] +fn rev2_impact_of_double_is_transitive() { + let g = open("rev2"); + let i = g.impact(p(&g, DOUBLE)).unwrap(); + assert_eq!(i.confirmed, m(&g, &[SQ, RUN, TWICE, CHECKED])); + assert_eq!(i.possible, m(&g, &[AREA_OF])); + assert_eq!(i.unknown, m(&g, &[])); +} + +/// Fails if dead code is not exactly `private_unused`, or the run is not +/// Complete, or the mask is vacuous (nearly everything flagged). +#[test] +fn rev1_dead_function() { + let g = open("rev1"); + let f = &g.diagnostics(CrateKind::Lib)[0]; + assert_eq!(f.kind, Kind::DeadFunction); + assert_eq!(f.positions, m(&g, &["cg_lib::util::private_unused"])); + assert_eq!(f.run, Run::Complete); + assert!(pop(&f.positions) * 3 < pop(g.fns())); +} + +/// Fails if the dynamic caller `area_dyn` is not the one UnresolvedCall row. +#[test] +fn rev1_unresolved_call() { + let g = open("rev1"); + let f = &g.diagnostics(CrateKind::Lib)[1]; + assert_eq!(f.kind, Kind::UnresolvedCall); + assert_eq!(f.positions, m(&g, &[AREA_DYN])); + assert_eq!(pop(&f.rows), 1); +} + +/// Fails if MAY_DISPATCH is mistaken for an unresolved call in rev2; the +/// anti-vacuity check proves a MAY_DISPATCH row exists. +#[test] +fn rev2_no_unresolved_call() { + let g = open("rev2"); + assert!((0..g.rows()).any(|r| g.row_predicate(r) == "MAY_DISPATCH")); + let f = &g.diagnostics(CrateKind::Lib)[1]; + assert_eq!(pop(&f.rows), 0); + assert_eq!(pop(&f.positions), 0); +} + +/// Fails if a callee that no stored crate defines (assert_failed, from +/// `core`) is reported dangling in a complete store. +#[test] +fn rev1_rev2_no_dangling_call() { + for store in ["rev1", "rev2"] { + let g = open(store); + let callees_external = g.pos(ASSERT_FAILED).is_some(); + assert!(callees_external, "{store}: external callee must exist"); + let f = &g.diagnostics(CrateKind::Lib)[2]; + assert_eq!(f.kind, Kind::DanglingCall); + assert_eq!(pop(&f.rows), 0, "{store}"); + } +} + +/// Fails if the stale cross-crate call run -> area_dyn (area_dyn gone from +/// the lib) is not reported as exactly one Stale row. +#[test] +fn mixed_dangling_call_is_stale() { + let g = open("mixed"); + let f = &g.diagnostics(CrateKind::Lib)[2]; + assert_eq!(f.kind, Kind::DanglingCall); + assert_eq!(pop(&f.rows), 1); + let row = f + .rows + .iter() + .enumerate() + .find_map(|(w, &bits)| (bits != 0).then(|| w * 64 + bits.trailing_zeros() as usize)) + .unwrap(); + assert_eq!(g.row_subject(row), RUN); + assert_eq!(g.row_object(row), AREA_DYN); + assert_eq!(f.status, FindingStatus::Stale); +} + +/// Fails if UnusedImport claims to have run when no import facts exist. +#[test] +fn rev1_unused_import_not_run() { + let g = open("rev1"); + let f = &g.diagnostics(CrateKind::Lib)[3]; + assert_eq!(f.kind, Kind::UnusedImport); + assert_eq!( + f.run, + Run::NotRun { + reason: "NO_IMPORT_FACTS" + } + ); +} + +/// Fails if a function used only as a function pointer (`helper`) is dead, +/// or `unused_private` is missed. +#[test] +fn fnref_dead_function() { + let g = open("fnref"); + let f = &g.diagnostics(CrateKind::Lib)[0]; + assert_eq!(f.positions, m(&g, &["fnref::unused_private"])); +} + +/// Fails if a binary crate (entry points unknown) is allowed to report dead code. +#[test] +fn rev1_bin_dead_function_refused() { + let g = open("rev1"); + let f = &g.diagnostics(CrateKind::Bin)[0]; + assert_eq!(f.kind, Kind::DeadFunction); + assert_eq!( + f.run, + Run::Refused { + reason: "NO_ENTRY_FACTS" + } + ); +} + +/// Fails if any query or diagnostic materialises cube rows instead of folding. +#[test] +fn no_rows_materialized() { + let mut g = open("rev1"); + let double = p(&g, DOUBLE); + let run = p(&g, RUN); + g.callers(double).unwrap(); + g.callees(run).unwrap(); + g.references(double); + g.impact(double).unwrap(); + g.diagnostics(CrateKind::Lib); + g.diagnostics(CrateKind::Bin); + assert_eq!(g.cube().stats().rows_materialized, 0); +}