From 266d05e8b1a2a62f84d724f17979a5697bdc55ce Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 09:34:34 +0000 Subject: [PATCH 1/3] feat(codegraph): read the effects rows and answer which effects a symbol reaches (CG-8) The Codegraph did not read .effects.tsv. effects.rs reads it read-only, keeps spans, and joins it with the call rows: Codegraph::effects_reached lists the effect rows in bodies a symbol is or reaches, graded Own / Confirmed / Possible / Unknown, with every known limit (a crate without an effects file, an incomplete harvest, callees in crates outside the store, a reachable dynamic call, an unplaced effect). A missing effects file gives a partial answer, never an empty clean one. EffectClass groups division, remainder, shift and write. The file's digest and row count identify what was read; the artifact carries no revision (CG-3). Nothing enters the call masks; impact and the ruff_charon format are unchanged. On the foundry_parity_lab store: max_quantities reaches the division at arm_b.rs:245 through cook_ops. 20 tests on real and fixture data; 29 guards disabled by a committed script, each red. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01XAqKMPJjVvasjwqhjJ6c9p --- .claude/board/STATUS_BOARD.md | 2 +- .../2026-11-11-codegraph-effects/REPORT.md | 82 +++ .../disable_runs.py | 83 +++ .../disable_runs.receipt.tsv | 30 + .codegraph/coverage.tsv | 2 +- .codegraph/crates.tsv | 2 +- .../crates/lance-graph-codegraph.index.md | 51 +- .codegraph/freshness.tsv | 2 +- .codegraph/imports.tsv | 17 +- .codegraph/symbols.tsv | 35 +- .../examples/cg_query.rs | 161 +++++- crates/lance-graph-codegraph/src/effects.rs | 481 ++++++++++++++++ crates/lance-graph-codegraph/src/lib.rs | 1 + .../tests/data/README.md | 1 + .../effects/foundry_parity_lab.effects.tsv | 215 +++++++ .../effects/foundry_parity_lab.residuals.tsv | 2 + .../data/effects/foundry_parity_lab.rows.bin | Bin 0 -> 90736 bytes crates/lance-graph-codegraph/tests/effects.rs | 545 ++++++++++++++++++ 18 files changed, 1673 insertions(+), 39 deletions(-) create mode 100644 .claude/probes/2026-11-11-codegraph-effects/REPORT.md create mode 100644 .claude/probes/2026-11-11-codegraph-effects/disable_runs.py create mode 100644 .claude/probes/2026-11-11-codegraph-effects/disable_runs.receipt.tsv create mode 100644 crates/lance-graph-codegraph/src/effects.rs create mode 100644 crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.effects.tsv create mode 100644 crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.residuals.tsv create mode 100644 crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.rows.bin create mode 100644 crates/lance-graph-codegraph/tests/effects.rs diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index be312e78a..713a6019e 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -375,7 +375,7 @@ Plan: `.claude/plans/codegraph-expansion-v1.md`. Coordination: Linear `P-EXO-18` | **CG-5** | co-research: body semantics through R2IL and egglog | Queued (needs go-ahead): EXO-237 | | **CG-6** | review report follow-ups (mask-risc extraction, re-grade, placement, advisory CI) | Queued: EXO-238 | | **CG-7** | report-rs #33 transfer case | Other session: EXO-239 / EXO-185 | -| **CG-8** | effects, target kind and cfg as Codegraph rows | Queued: EXO-241 | +| **CG-8** | effects, target kind and cfg as Codegraph rows | In PR (effects reader and reach; target kind and cfg still queued): EXO-241 | ## D-CODEGRAPH-0 — resident Codegraph: Charon → ruff → lance-graph + crai-rs (2026-11-02) diff --git a/.claude/probes/2026-11-11-codegraph-effects/REPORT.md b/.claude/probes/2026-11-11-codegraph-effects/REPORT.md new file mode 100644 index 000000000..56f1b42ca --- /dev/null +++ b/.claude/probes/2026-11-11-codegraph-effects/REPORT.md @@ -0,0 +1,82 @@ +# CG-8: the Codegraph reads the effects rows (EXO-241) + +Ruling: D-CODEGRAPH-WAVE0-ACCEPTANCE, priority 2. Stacked on PR 2004. + +## What it is + +`crates/lance-graph-codegraph/src/effects.rs`, read-only: + +- `Effects::open(dir)` reads every `.effects.tsv` (ruff_charon effects version 1, + `subject effect kind file start end`). A file that does not match the format is refused whole. +- `Effects::of(symbol)`: `Witnessed(rows)`, `NoneWitnessed` (not a purity verdict) or + `CrateNotRead`. +- `EffectClass::{Division, Remainder, Shift, Write}` groups the kinds a review asks about. +- `Codegraph::effects_reached(symbol, &effects, keep)`: the effect rows in bodies the symbol is + or reaches, each graded `Own`, `Confirmed`, `Possible` or `Unknown` from the call rows, plus + every known `ReachLimit`. +- `Effects::identity(krate)`: FNV-1a 64 of the file's bytes and its row count. + +Nothing is written into the call masks. `Codegraph::impact` and the ruff_charon format are +unchanged. + +## Acceptance + +"Which witnessed division, remainder, shift or write effects are reachable from a named symbol": + +``` +$ cg_query tests/data/effects effects \ + --reach "foundry_parity_lab::arm_b::{ItemQuery<'s>}::max_quantities" \ + --class division,remainder,shift,write +reach Confirmed effect ASSERT DivisionByZero …::cook_ops src/arm_b.rs 245 7745..7780 +reach Confirmed effect ASSERT Overflow:Div …::cook_ops src/arm_b.rs 245 7745..7780 +(each twice: the store has two rows for the site) +LIMIT CalleesOutsideStore { crates: ["alloc", "core", "lance_graph_mask_risc"] } +effects-file foundry_parity_lab fnv64=05ca96551de2c6a0 rows=213 +reached 4 PARTIAL (see LIMIT lines) +``` + +"A missing effects artifact must produce UNKNOWN/PARTIAL, never an empty clean answer": + +``` +$ cg_query tests/data/rev1 effects --reach cg_app::run --class division +LIMIT EffectsNotRead { krate: "cg_app" } +LIMIT EffectsNotRead { krate: "cg_lib" } +LIMIT DynamicCalls { store_dynamic_calls: 1 } +reached 0 UNKNOWN (partial: see LIMIT lines) +``` + +| asked to preserve | how | +|---|---| +| source spans | each row keeps `file start end`; the CLI adds the 1-based line | +| coverage | `EffectsNotRead` per stored crate without a file; `CalleesOutsideStore` names the crates reached but not stored; `DynamicCalls` when the symbol reaches a dynamic call; `Unplaced` for an effect whose subject has no position | +| residuals | `HarvestIncomplete` for a crate that is partial, has residuals, or has no residuals file | +| revision identity | the file's digest and row count. **The effects file carries no source revision**, so this identifies the bytes read, not the commit. Binding it to a revision is CG-3 (EXO-235). | + +## What it found on real data + +- All 62 effect subjects of the `foundry_parity_lab` store resolve to Codegraph positions. +- `--kind ASSERT:DivisionByZero` lists five rows at four sites: `method.rs:58`, `arm_a.rs:75`, + `arm_b.rs:245` (two rows) and `oracle.rs:62`. +- `method::execute_can_cook` reaches no division, although `can_cook_guard` (`method.rs:58`) is + registered from it through a closure. That is the closure-ownership gap of CG-1 (EXO-233): the + call rows have no edge into a closure body. The answer is `PARTIAL`, not clean, only because + of the other limits; nothing here reports the missing closure edge itself. +- Almost every function reaches `core` or `alloc`, so almost every answer is `PARTIAL`. That is + accurate: those bodies were not extracted. + +## Tests and controls + +- `tests/effects.rs`: 20 tests. Real fixture: `tests/data/effects/` (213 effect rows and the + matching call rows of `foundry_parity_lab`). The four reach grades use the `rev1` call rows + with one synthetic effect row. +- `python3 .claude/probes/2026-11-11-codegraph-effects/disable_runs.py`: 29 guards, each patched + out through an asserted anchor. Receipt beside it: 29 red, 0 not red. + +## Not done + +- Target-kind and cfg rows, the other half of CG-8's plan entry. They need a decision on the + cfg source and are a separate slice. +- No join with `impact_report`'s `Scope`: declared targets and excluded regions do not narrow a + reach answer yet. +- The reach is computed by one `impact` per candidate symbol (cold path). It is not timed here + and makes no latency claim. diff --git a/.claude/probes/2026-11-11-codegraph-effects/disable_runs.py b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.py new file mode 100644 index 000000000..605639540 --- /dev/null +++ b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Disable runs for crates/lance-graph-codegraph/src/effects.rs. + +Each guard is patched out (the anchor must occur exactly once, or the run is reported as +ANCHOR-MISSING and counts as a failure), `cargo test --release --test effects` is run, and +the file is restored. A guard is load-bearing when its run is RED. + +Run from the repository root on a clean tree: python3 +Exit 0 only when every guard is RED. Writes nothing but the temporary patch. +""" +import re, subprocess, sys +from pathlib import Path + +CRATE = Path(__file__).resolve().parents[3] / "crates/lance-graph-codegraph" +P = CRATE / "src/effects.rs" +TEST = "effects" +G = [ + ("version", " if found != 1 {", " if false {"), + ("schema", " if lines.next() != Some(SCHEMA) {", " if lines.next().is_none() {"), + ("arity", "let [subject, effect, kind, file, start, end] = f[..] else {", "let [subject, effect, kind, file, start, end, ..] = f[..] else {"), + ("assert-empty-kind", '("ASSERT", k) if !k.is_empty() =>', '("ASSERT", k) =>'), + ("site-order", 'if file != "-" && start <= end =>', 'if file != "-" =>'), + ("site-dash-file", 'if file != "-" && start <= end =>', 'if start <= end =>'), + ("site-partial-dash", '("-", _, _) if start == "-" && end == "-" => None,', '("-", _, _) => None,'), + ("crate-not-read", " if !self.crates.contains(krate) {\n return EffectAnswer::CrateNotRead;\n }\n", ""), + ("none-witnessed", " if hits.is_empty() {\n EffectAnswer::NoneWitnessed\n } else {\n EffectAnswer::Witnessed(hits)\n }", " EffectAnswer::Witnessed(hits)"), + ("subject-filter", ".filter(|e| e.subject == subject)", ".filter(|e| !e.subject.is_empty() || e.subject == subject)"), + ("kind-filter", ".filter(move |e| e.kind == *kind)", ".filter(move |e| e.kind == *kind || true)"), + ("class-division", '"DivisionByZero" | "Overflow:Div" => Some(EffectClass::Division),', '"DivisionByZero" => Some(EffectClass::Division),'), + ("class-remainder", '"RemainderByZero" | "Overflow:Rem" => Some(EffectClass::Remainder),', '"RemainderByZero" => Some(EffectClass::Remainder),'), + ("class-shift", '"Overflow:Shl" | "Overflow:Shr" => Some(EffectClass::Shift),', '"Overflow:Shl" => Some(EffectClass::Shift),'), + ("class-write", "EffectKind::DerefWriteArg | EffectKind::DerefWriteLocal => Some(EffectClass::Write),", "EffectKind::DerefWriteArg => Some(EffectClass::Write),\n EffectKind::DerefWriteLocal => None,"), + ("identity-digest", "fnv64: fnv1a(text.as_bytes()),", "fnv64: fnv1a(krate.as_bytes()),"), + ("identity-rows", "rows: (rows.len() - before) as u32,", "rows: rows.len() as u32 + 1,"), + ("reach-own", " Some(p) if p == me => Some(ReachGrade::Own),\n", ""), + ("reach-confirmed", " if has(&i.confirmed) {\n Some(ReachGrade::Confirmed)", " if false {\n Some(ReachGrade::Confirmed)"), + ("reach-possible", "} else if has(&i.possible) {\n Some(ReachGrade::Possible)", "} else if false {\n Some(ReachGrade::Possible)"), + ("reach-unknown", "} else if has(&i.unknown) {\n Some(ReachGrade::Unknown)", "} else if false {\n Some(ReachGrade::Unknown)"), + ("reach-keep", ".filter(|e| keep(&e.kind)) {", ".filter(|e| keep(&e.kind) || true) {"), + ("reach-no-position", " let Some(me) = self.pos(symbol) else {\n return Ok(None);\n };", " let me = self.pos(symbol).unwrap_or(0);"), + ("limit-effects-not-read", " if effects.identity(&krate).is_none() {", " if false {"), + ("limit-harvest", " if status == RunStatus::Partial || !clean_harvest {", " if status == RunStatus::Partial {"), + ("limit-outside", " if !outside.is_empty() {", " if false {"), + ("limit-dynamic", " if reaches_dynamic {", " if false {"), + ("limit-dynamic-per-symbol", " reaches_dynamic = true;", " reaches_dynamic = true;\n }\n if true {\n reaches_dynamic = true;"), + ("limit-unplaced", " unplaced.insert(subject);\n", " unplaced.insert(subject);\n unplaced.clear();\n"), +] + +def main(): + if subprocess.run(["git", "diff", "--quiet", "--", str(P)]).returncode != 0: + sys.exit(f"{P} has local changes; commit first (the restore would not be exact)") + orig = P.read_text() + bad = 0 + try: + for name, old, new in G: + if orig.count(old) != 1: + print(f"{name}\tANCHOR-MISSING({orig.count(old)})") + bad += 1 + continue + P.write_text(orig.replace(old, new)) + out = subprocess.run( + ["cargo", "test", "--release", "--test", TEST], + cwd=CRATE, capture_output=True, text=True, + ) + text = out.stdout + out.stderr + m = re.search(r"test result: (\w+)\. (\d+) passed; (\d+) failed", text) + failed = sorted(set(re.findall(r"^test (\S+) \.\.\. FAILED", text, re.M))) + if not m: + print(f"{name}\tBUILD-ERROR") + bad += 1 + elif m.group(1) == "FAILED": + print(f"{name}\tRED\t{','.join(f.split('::')[-1] for f in failed)}") + else: + print(f"{name}\tGREEN\tno test depends on this guard") + bad += 1 + finally: + P.write_text(orig) + print(f"guards\t{len(G)}\tnot-red\t{bad}") + sys.exit(1 if bad else 0) + + +if __name__ == "__main__": + main() diff --git a/.claude/probes/2026-11-11-codegraph-effects/disable_runs.receipt.tsv b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.receipt.tsv new file mode 100644 index 000000000..050ab72a4 --- /dev/null +++ b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.receipt.tsv @@ -0,0 +1,30 @@ +version RED a_wrong_version_is_refused +schema RED a_wrong_schema_is_refused +arity RED a_short_or_long_row_is_refused +assert-empty-kind RED an_unknown_effect_is_refused +site-order RED a_malformed_site_is_refused +site-dash-file RED a_malformed_site_is_refused +site-partial-dash RED a_malformed_site_is_refused +crate-not-read RED an_unread_crate_is_not_looked_at +none-witnessed RED no_row_is_not_a_purity_verdict +subject-filter RED a_function_with_rows_is_witnessed,no_row_is_not_a_purity_verdict +kind-filter RED division_sites_are_found_by_kind +class-division RED classes_select_division_remainder_shift_and_write +class-remainder RED classes_select_division_remainder_shift_and_write +class-shift RED classes_select_division_remainder_shift_and_write +class-write RED classes_select_division_remainder_shift_and_write +identity-digest RED the_effects_file_is_identified_by_its_bytes +identity-rows RED the_effects_file_is_identified_by_its_bytes +reach-own RED a_caller_reaches_its_callees_division,the_reach_grade_follows_the_call_rows +reach-confirmed RED a_caller_reaches_its_callees_division,classes_select_division_remainder_shift_and_write,the_reach_grade_follows_the_call_rows +reach-possible RED the_reach_grade_follows_the_call_rows +reach-unknown RED the_reach_grade_follows_the_call_rows +reach-keep RED a_caller_reaches_its_callees_division,classes_select_division_remainder_shift_and_write +reach-no-position RED a_caller_reaches_its_callees_division +limit-effects-not-read RED a_missing_effects_file_makes_the_answer_partial_never_clean +limit-harvest RED dynamic_calls_and_an_incomplete_harvest_are_limits +limit-outside RED callees_outside_the_store_are_a_limit +limit-dynamic RED dynamic_calls_and_an_incomplete_harvest_are_limits +limit-dynamic-per-symbol RED callees_outside_the_store_are_a_limit +limit-unplaced RED an_effect_subject_without_a_position_is_a_limit +guards 29 not-red 0 diff --git a/.codegraph/coverage.tsv b/.codegraph/coverage.tsv index a44ee53b2..e3f646a6e 100644 --- a/.codegraph/coverage.tsv +++ b/.codegraph/coverage.tsv @@ -36,7 +36,7 @@ lance-graph-benches 0 10 0 macro-generated items invisible;fn-local use not scan lance-graph-callcenter 35 6 3 macro-generated items invisible;fn-local use not scanned lance-graph-catalog 9 1 0 macro-generated items invisible;fn-local use not scanned lance-graph-codec-research 10 0 0 macro-generated items invisible;fn-local use not scanned -lance-graph-codegraph 9 12 0 macro-generated items invisible;fn-local use not scanned +lance-graph-codegraph 10 13 0 macro-generated items invisible;fn-local use not scanned lance-graph-cognitive 60 0 0 macro-generated items invisible;fn-local use not scanned lance-graph-consumer-conformance 2 0 0 macro-generated items invisible;fn-local use not scanned lance-graph-contract 175 35 2 macro-generated items invisible;fn-local use not scanned diff --git a/.codegraph/crates.tsv b/.codegraph/crates.tsv index f9167d582..38b0a0444 100644 --- a/.codegraph/crates.tsv +++ b/.codegraph/crates.tsv @@ -36,7 +36,7 @@ lance-graph-benches crates/lance-graph-benches member 2f2fb296a08260fb lance-graph-callcenter crates/lance-graph-callcenter member 41a65d38ac70bc87 lance-graph-catalog crates/lance-graph-catalog member 9d0c34f70c28a465 lance-graph-codec-research crates/lance-graph-codec-research excluded 8b8afbd9a6d0f359 -lance-graph-codegraph crates/lance-graph-codegraph excluded d7f19d8d862f76e3 +lance-graph-codegraph crates/lance-graph-codegraph excluded 03c06e701aa81d11 lance-graph-cognitive crates/lance-graph-cognitive excluded 93f956ac0d69ca66 lance-graph-consumer-conformance crates/lance-graph-consumer-conformance member 82e980465d770990 lance-graph-contract crates/lance-graph-contract member 6b4e13ff1d0afadb diff --git a/.codegraph/crates/lance-graph-codegraph.index.md b/.codegraph/crates/lance-graph-codegraph.index.md index f677bb4ef..65f9920fb 100644 --- a/.codegraph/crates/lance-graph-codegraph.index.md +++ b/.codegraph/crates/lance-graph-codegraph.index.md @@ -3,7 +3,7 @@ crate: lance-graph-codegraph path: crates/lance-graph-codegraph workspace: excluded -digest: d7f19d8d862f76e3 +digest: 03c06e701aa81d11 evidence legend: ../index.md#evidence-legend Generated; the code is the truth. See ../index.md. @@ -147,6 +147,29 @@ Generated; the code is the truth. See ../index.md. - struct Finding [pub] — crates/lance-graph-codegraph/src/diag.rs:107 CONFIRMED - fn Codegraph::coverage [pub] — crates/lance-graph-codegraph/src/diag.rs:137 CONFIRMED - fn Codegraph::diagnostics [pub] — crates/lance-graph-codegraph/src/diag.rs:164 CONFIRMED +- const EFFECTS_SUFFIX [pub] — crates/lance-graph-codegraph/src/effects.rs:26 CONFIRMED +- enum EffectKind [pub] — crates/lance-graph-codegraph/src/effects.rs:30 CONFIRMED +- fn EffectKind::columns [pub] — crates/lance-graph-codegraph/src/effects.rs:42 CONFIRMED +- enum EffectClass [pub] — crates/lance-graph-codegraph/src/effects.rs:63 CONFIRMED +- fn EffectKind::class [pub] — crates/lance-graph-codegraph/src/effects.rs:76 CONFIRMED +- struct EffectsIdentity [pub] — crates/lance-graph-codegraph/src/effects.rs:93 CONFIRMED +- struct EffectSite [pub] — crates/lance-graph-codegraph/src/effects.rs:102 CONFIRMED +- struct Effect [pub] — crates/lance-graph-codegraph/src/effects.rs:113 CONFIRMED +- enum EffectsRefusal [pub] — crates/lance-graph-codegraph/src/effects.rs:125 CONFIRMED +- enum EffectAnswer [pub] — crates/lance-graph-codegraph/src/effects.rs:165 CONFIRMED +- struct Effects [pub] — crates/lance-graph-codegraph/src/effects.rs:176 CONFIRMED +- fn Effects::open [pub] — crates/lance-graph-codegraph/src/effects.rs:230 CONFIRMED +- fn Effects::crates [pub] — crates/lance-graph-codegraph/src/effects.rs:262 CONFIRMED +- fn Effects::identity [pub] — crates/lance-graph-codegraph/src/effects.rs:267 CONFIRMED +- fn Effects::rows [pub] — crates/lance-graph-codegraph/src/effects.rs:275 CONFIRMED +- fn Effects::of [pub] — crates/lance-graph-codegraph/src/effects.rs:281 CONFIRMED +- fn Effects::with_kind [pub] — crates/lance-graph-codegraph/src/effects.rs:296 CONFIRMED +- enum ReachGrade [pub] — crates/lance-graph-codegraph/src/effects.rs:303 CONFIRMED +- struct ReachedEffect [pub] — crates/lance-graph-codegraph/src/effects.rs:316 CONFIRMED +- enum ReachLimit [pub] — crates/lance-graph-codegraph/src/effects.rs:325 CONFIRMED +- struct Reached [pub] — crates/lance-graph-codegraph/src/effects.rs:359 CONFIRMED +- fn Reached<'_>::partial [pub] — crates/lance-graph-codegraph/src/effects.rs:369 CONFIRMED +- fn crate::Codegraph::effects_reached [pub] — crates/lance-graph-codegraph/src/effects.rs:382 CONFIRMED - type Pos [pub] — crates/lance-graph-codegraph/src/graph.rs:20 CONFIRMED - enum IntakeRefusal [pub] — crates/lance-graph-codegraph/src/graph.rs:24 CONFIRMED - enum RunStatus [pub] — crates/lance-graph-codegraph/src/graph.rs:63 CONFIRMED @@ -201,34 +224,12 @@ Generated; the code is the truth. See ../index.md. - const Targets::LIB_ONLY [pub] — crates/lance-graph-codegraph/src/impact_report.rs:44 CONFIRMED - const Targets::ALL [pub] — crates/lance-graph-codegraph/src/impact_report.rs:52 CONFIRMED - enum Consumers [pub] — crates/lance-graph-codegraph/src/impact_report.rs:75 CONFIRMED -- struct Scope [pub] — crates/lance-graph-codegraph/src/impact_report.rs:89 CONFIRMED -- enum SymbolState [pub] — crates/lance-graph-codegraph/src/impact_report.rs:108 CONFIRMED -- enum Gap [pub] — crates/lance-graph-codegraph/src/impact_report.rs:123 CONFIRMED -- struct ImpactReport [pub] — crates/lance-graph-codegraph/src/impact_report.rs:226 CONFIRMED -- fn ImpactReport::closed_in_scope [pub] — crates/lance-graph-codegraph/src/impact_report.rs:241 CONFIRMED -- fn Codegraph::impact_report [pub] — crates/lance-graph-codegraph/src/impact_report.rs:421 CONFIRMED -- enum Fault [pub(crate)] cfg=test — crates/lance-graph-codegraph/src/lab_wal.rs:19 CONFIRMED -- struct LabWal [pub(crate)] cfg=test — crates/lance-graph-codegraph/src/lab_wal.rs:35 CONFIRMED -- fn LabWal::new [pub(crate)] cfg=test — crates/lance-graph-codegraph/src/lab_wal.rs:44 CONFIRMED -- fn LabWal::head [pub(crate)] cfg=test — crates/lance-graph-codegraph/src/lab_wal.rs:53 CONFIRMED -- fn LabWal::inject [pub(crate)] cfg=test — crates/lance-graph-codegraph/src/lab_wal.rs:58 CONFIRMED -- fn LabWal::reverse_physical_order [pub(crate)] cfg=test — crates/lance-graph-codegraph/src/lab_wal.rs:64 CONFIRMED -- fn LabWal::image [pub(crate)] cfg=test — crates/lance-graph-codegraph/src/lab_wal.rs:70 CONFIRMED -- mod artifact [pub] — crates/lance-graph-codegraph/src/lib.rs:6 CONFIRMED -- mod artifact_bin [pub] — crates/lance-graph-codegraph/src/lib.rs:7 CONFIRMED -- mod diag [pub] — crates/lance-graph-codegraph/src/lib.rs:8 CONFIRMED -- mod graph [pub] — crates/lance-graph-codegraph/src/lib.rs:9 CONFIRMED -- mod impact_report [pub] — crates/lance-graph-codegraph/src/lib.rs:10 CONFIRMED -- mod query [pub] — crates/lance-graph-codegraph/src/lib.rs:13 CONFIRMED -- struct Graded [pub] — crates/lance-graph-codegraph/src/query.rs:21 CONFIRMED -- struct Calls [pub] — crates/lance-graph-codegraph/src/query.rs:32 CONFIRMED -- struct Sites [pub] — crates/lance-graph-codegraph/src/query.rs:41 CONFIRMED -- enum QueryRefusal [pub] — crates/lance-graph-codegraph/src/query.rs:52 CONFIRMED -- truncated (204 total), see symbols.tsv +- truncated (228 total), see symbols.tsv ## Imports from in-scope crates ### lance-graph-contract +- use lance_graph_contract::hash::fnv1a — crates/lance-graph-codegraph/src/effects.rs:21 CONFIRMED - use lance_graph_contract::class_view::ClassId cfg=test — crates/lance-graph-codegraph/src/graph/publication.rs:13 CONFIRMED - use lance_graph_contract::class_view::ClassView cfg=test — crates/lance-graph-codegraph/src/graph/publication.rs:13 CONFIRMED - use lance_graph_contract::ontology::DisplayTemplate cfg=test — crates/lance-graph-codegraph/src/graph/publication.rs:14 CONFIRMED diff --git a/.codegraph/freshness.tsv b/.codegraph/freshness.tsv index 5a1ed7a06..c6370b3fa 100644 --- a/.codegraph/freshness.tsv +++ b/.codegraph/freshness.tsv @@ -36,7 +36,7 @@ lance-graph-benches 2f2fb296a08260fb Cargo.toml+src (1 files) lance-graph-callcenter 41a65d38ac70bc87 Cargo.toml+src (36 files) lance-graph-catalog 9d0c34f70c28a465 Cargo.toml+src (10 files) lance-graph-codec-research 8b8afbd9a6d0f359 Cargo.toml+src (11 files) -lance-graph-codegraph d7f19d8d862f76e3 Cargo.toml+src (10 files) +lance-graph-codegraph 03c06e701aa81d11 Cargo.toml+src (11 files) lance-graph-cognitive 93f956ac0d69ca66 Cargo.toml+src (61 files) lance-graph-consumer-conformance 82e980465d770990 Cargo.toml+src (3 files) lance-graph-contract 6b4e13ff1d0afadb Cargo.toml+src (176 files) diff --git a/.codegraph/imports.tsv b/.codegraph/imports.tsv index 3d87247e0..cd491a2f9 100644 --- a/.codegraph/imports.tsv +++ b/.codegraph/imports.tsv @@ -4731,6 +4731,11 @@ lance-graph-codegraph crates/lance-graph-codegraph/src/diag.rs 12 crate::artifac lance-graph-codegraph crates/lance-graph-codegraph/src/diag.rs 12 crate::artifact::ResidualCounts - use - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/diag.rs 13 crate::graph::Codegraph - use - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/diag.rs 9 ndarray::simd::mask_andnot - use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 17 std::collections::BTreeSet - use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 18 std::fs - use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 19 std::path::Path - use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 21 lance_graph_contract::hash::fnv1a - use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 23 crate::graph::RunStatus - use - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/graph.rs 10 lance_graph_xlog::cube::Cube - use - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/graph.rs 10 lance_graph_xlog::cube::CubeRefusal - use - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/graph.rs 10 lance_graph_xlog::cube::EdgeOp - use - CONFIRMED @@ -4830,12 +4835,12 @@ lance-graph-codegraph crates/lance-graph-codegraph/src/lab_wal.rs 14 lance_graph lance-graph-codegraph crates/lance-graph-codegraph/src/lab_wal.rs 170 super::* - use test POSSIBLE lance-graph-codegraph crates/lance-graph-codegraph/src/lab_wal.rs 171 lance_graph_planner::persist_sink::CycleFrame - use test CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/lab_wal.rs 9 std::collections::BTreeMap - use test CONFIRMED -lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 15 graph::Codegraph - pub-use - CONFIRMED -lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 15 graph::IntakeRefusal - pub-use - CONFIRMED -lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 15 graph::Pos - pub-use - CONFIRMED -lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 15 graph::RefreshRefusal - pub-use - CONFIRMED -lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 15 graph::RefreshStats - pub-use - CONFIRMED -lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 15 graph::RunStatus - pub-use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 16 graph::Codegraph - pub-use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 16 graph::IntakeRefusal - pub-use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 16 graph::Pos - pub-use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 16 graph::RefreshRefusal - pub-use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 16 graph::RefreshStats - pub-use - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 16 graph::RunStatus - pub-use - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/query.rs 10 lance_graph_xlog::cube::CubeRefusal - use - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/query.rs 10 lance_graph_xlog::cube::RelId - use - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/query.rs 11 lance_graph_xlog::wide::WideRows - use - CONFIRMED diff --git a/.codegraph/symbols.tsv b/.codegraph/symbols.tsv index b11358a19..e73d294cb 100644 --- a/.codegraph/symbols.tsv +++ b/.codegraph/symbols.tsv @@ -8683,6 +8683,34 @@ lance-graph-codegraph crates/lance-graph-codegraph/src/diag.rs 62 enum Qualifier lance-graph-codegraph crates/lance-graph-codegraph/src/diag.rs 69 enum FindingStatus pub - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/diag.rs 79 enum Run pub - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/diag.rs 98 enum CrateKind pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 102 struct EffectSite pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 113 struct Effect pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 125 enum EffectsRefusal pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 165 enum EffectAnswer pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 176 struct Effects pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 227 impl Effects - - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 230 fn Effects::open pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 26 const EFFECTS_SUFFIX pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 262 fn Effects::crates pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 267 fn Effects::identity pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 275 fn Effects::rows pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 281 fn Effects::of pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 296 fn Effects::with_kind pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 30 enum EffectKind pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 303 enum ReachGrade pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 316 struct ReachedEffect pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 325 enum ReachLimit pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 359 struct Reached pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 366 impl Reached<'_> - - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 369 fn Reached<'_>::partial pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 374 impl crate::Codegraph - - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 382 fn crate::Codegraph::effects_reached pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 40 impl EffectKind - - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 42 fn EffectKind::columns pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 63 enum EffectClass pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 74 impl EffectKind - - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 76 fn EffectKind::class pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/effects.rs 93 struct EffectsIdentity pub - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/graph.rs 1001 fn Codegraph::partial_crates pub(crate) - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/graph.rs 1009 fn Codegraph::any_partial pub(crate) - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/graph.rs 1014 fn Codegraph::words pub - CONFIRMED @@ -8763,12 +8791,13 @@ lance-graph-codegraph crates/lance-graph-codegraph/src/lab_wal.rs 58 fn LabWal:: lance-graph-codegraph crates/lance-graph-codegraph/src/lab_wal.rs 64 fn LabWal::reverse_physical_order pub(crate) test CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/lab_wal.rs 70 fn LabWal::image pub(crate) test CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/lab_wal.rs 78 impl WalSink for LabWal - test CONFIRMED -lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 10 mod impact_report pub - CONFIRMED -lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 13 mod query pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 10 mod graph pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 11 mod impact_report pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 14 mod query pub - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 6 mod artifact pub - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 7 mod artifact_bin pub - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 8 mod diag pub - CONFIRMED -lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 9 mod graph pub - CONFIRMED +lance-graph-codegraph crates/lance-graph-codegraph/src/lib.rs 9 mod effects pub - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/query.rs 125 fn Codegraph::callers pub - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/query.rs 157 fn Codegraph::callees pub - CONFIRMED lance-graph-codegraph crates/lance-graph-codegraph/src/query.rs 188 fn Codegraph::references pub - CONFIRMED diff --git a/crates/lance-graph-codegraph/examples/cg_query.rs b/crates/lance-graph-codegraph/examples/cg_query.rs index 1011aeaac..8be4e8274 100644 --- a/crates/lance-graph-codegraph/examples/cg_query.rs +++ b/crates/lance-graph-codegraph/examples/cg_query.rs @@ -4,11 +4,21 @@ //! ```text //! cargo run --release --example cg_query -- symbols [SUBSTRING] //! cargo run --release --example cg_query -- callers|callees|impact|refs SYMBOL +//! cargo run --release --example cg_query -- effects SYMBOL | --kind EFFECT:KIND | --reach SYMBOL (--kind EFFECT:KIND | --class LIST) //! cargo run --release --example cg_query -- impact-report SYMBOL \ //! [--deps a,b | --consumers-closed] [--targets CRATE=lib,tests,examples,benches]... \ //! [--exclude CRATE=REGION]... [--no-freshness] //! ``` //! +//! `effects SYMBOL` and `effects --kind EFFECT:KIND` (for example `ASSERT:DivisionByZero`, +//! `DEREF_WRITE:Arg`) print the rows of `.effects.tsv`: what a function body witnesses. +//! A function without a row is printed as `NONE-WITNESSED`, which is not a purity verdict, and a +//! crate without an effects file as `CRATE-NOT-READ`. `--reach SYMBOL` joins the effect rows with +//! the call rows: the effects in bodies SYMBOL is or reaches, each with its grade. `--class` takes +//! a list of `division,remainder,shift,write`. Every known reason the answer may be missing +//! effects is a `LIMIT` line (a crate without an effects file, an incomplete harvest, dynamic +//! calls), and an answer with a limit and no hit is printed as `UNKNOWN`, never as none. +//! //! `impact-report` (alias `report`) prints [`Codegraph::impact_report`]: whether the symbol has a //! position, the impact masks, the stored crates, and every coverage gap of the declared scope, //! one `GAP` line each. Nothing is assumed: a crate without `--targets` is reported as undeclared @@ -27,6 +37,7 @@ use std::path::{Path, PathBuf}; use std::process::ExitCode; +use lance_graph_codegraph::effects::{Effect, EffectAnswer, EffectClass, EffectKind, Effects}; use lance_graph_codegraph::impact_report::{Consumers, Scope, SymbolState, Targets}; use lance_graph_codegraph::Codegraph; @@ -72,6 +83,151 @@ fn observed_files(g: &Codegraph) -> Vec { files } +fn effect_line(src: &Path, e: &Effect) -> String { + let (effect, kind) = e.kind.columns(); + let site = match &e.site { + Some(s) => format!( + "{}\t{}\t{}..{}", + s.file, + line_of(src, &s.file, s.start), + s.start, + s.end + ), + None => "-\t-\t-".to_string(), + }; + format!("effect\t{effect}\t{kind}\t{}\t{site}", e.subject) +} + +fn parse_classes(list: &str) -> Option> { + list.split(',') + .map(|c| match c { + "division" => Some(EffectClass::Division), + "remainder" => Some(EffectClass::Remainder), + "shift" => Some(EffectClass::Shift), + "write" => Some(EffectClass::Write), + _ => None, + }) + .collect() +} + +fn parse_kind(spec: &str) -> Option { + match spec.split_once(':') { + Some(("ASSERT", k)) if !k.is_empty() => Some(EffectKind::Assert(k.to_string())), + Some(("DEREF_WRITE", "Arg")) => Some(EffectKind::DerefWriteArg), + Some(("DEREF_WRITE", "Local")) => Some(EffectKind::DerefWriteLocal), + _ => None, + } +} + +/// `effects SYMBOL`, `effects --kind EFFECT:KIND` or `effects --reach SYMBOL (--kind EFFECT:KIND | --class LIST)`. Exit 4 when the symbol's crate has no +/// effects file; 0 otherwise, also for a function without a row. +fn effects(store: &Path, src: &Path, args: &[String]) -> ExitCode { + let fx = match Effects::open(store) { + Ok(fx) => fx, + Err(e) => { + println!("REFUSED\teffects\t{e:?}"); + return ExitCode::from(3); + } + }; + let read: Vec<&str> = fx.crates().collect(); + match args { + [flag, spec] if flag == "--kind" => { + let Some(kind) = parse_kind(spec) else { + eprintln!("--kind needs ASSERT:, DEREF_WRITE:Arg or DEREF_WRITE:Local"); + return ExitCode::from(2); + }; + let mut n = 0u32; + for e in fx.with_kind(&kind) { + println!("{}", effect_line(src, e)); + n += 1; + } + println!("effects\t{n}\tcrates-read={}", read.join(",")); + ExitCode::SUCCESS + } + [flag, sym, how, spec] if flag == "--reach" && (how == "--kind" || how == "--class") => { + let one_kind = if how == "--kind" { + parse_kind(spec) + } else { + None + }; + let classes = if how == "--class" { + parse_classes(spec) + } else { + None + }; + if one_kind.is_none() && classes.is_none() { + eprintln!("--kind needs ASSERT:, DEREF_WRITE:Arg or DEREF_WRITE:Local; --class needs a list of division,remainder,shift,write"); + return ExitCode::from(2); + } + let keep = |k: &EffectKind| match (&one_kind, &classes) { + (Some(want), _) => k == want, + (_, Some(cs)) => k.class().is_some_and(|c| cs.contains(&c)), + _ => false, + }; + let g = match Codegraph::open(store) { + Ok(g) => g, + Err(e) => { + println!("REFUSED\topen\t{e:?}"); + return ExitCode::from(3); + } + }; + match g.effects_reached(sym, &fx, keep) { + Err(e) => { + println!("REFUSED\teffects\t{e:?}"); + ExitCode::from(3) + } + Ok(None) => { + println!("UNKNOWN\tsymbol-not-in-store\t{sym}"); + ExitCode::from(4) + } + Ok(Some(r)) => { + for h in &r.hits { + println!("reach\t{:?}\t{}", h.grade, effect_line(src, h.effect)); + } + for l in &r.limits { + println!("LIMIT\t{l:?}"); + } + for k in &read { + if let Some(i) = fx.identity(k) { + println!("effects-file\t{k}\tfnv64={:016x}\trows={}", i.fnv64, i.rows); + } + } + // A partial answer with no hit is "unknown", never "none". + let status = match (r.partial(), r.hits.is_empty()) { + (true, true) => "UNKNOWN (partial: see LIMIT lines)", + (true, false) => "PARTIAL (see LIMIT lines)", + (false, _) => "no known limit (not a proof of absence)", + }; + println!("reached\t{}\t{status}", r.hits.len()); + ExitCode::SUCCESS + } + } + } + [sym] if !sym.starts_with("--") => match fx.of(sym) { + EffectAnswer::Witnessed(rows) => { + for e in rows { + println!("{}", effect_line(src, e)); + } + ExitCode::SUCCESS + } + EffectAnswer::NoneWitnessed => { + println!("NONE-WITNESSED\t{sym}\tnot a purity verdict"); + ExitCode::SUCCESS + } + EffectAnswer::CrateNotRead => { + println!("CRATE-NOT-READ\t{sym}\tcrates-read={}", read.join(",")); + ExitCode::from(4) + } + }, + _ => { + eprintln!( + "effects needs a SYMBOL, --kind EFFECT:KIND, or --reach SYMBOL (--kind EFFECT:KIND | --class LIST)" + ); + ExitCode::from(2) + } + } +} + fn parse_targets(list: &str) -> Option { let mut t = Targets::default(); for k in list.split(',') { @@ -201,7 +357,7 @@ fn impact_report(g: &Codegraph, store: &Path, src: &Path, sym: &str, flags: &[St fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); if args.len() < 3 { - eprintln!("usage: cg_query symbols [SUBSTR] | sweep | callers|callees|impact|refs SYMBOL | impact-report SYMBOL [--deps a,b | --consumers-closed] [--targets CRATE=LIST]... [--exclude CRATE=REGION]... [--no-freshness]"); + eprintln!("usage: cg_query symbols [SUBSTR] | sweep | callers|callees|impact|refs SYMBOL | effects SYMBOL|--kind EFFECT:KIND | impact-report SYMBOL [--deps a,b | --consumers-closed] [--targets CRATE=LIST]... [--exclude CRATE=REGION]... [--no-freshness]"); return ExitCode::from(2); } let (store, src, cmd) = ( @@ -209,6 +365,9 @@ fn main() -> ExitCode { PathBuf::from(&args[1]), args[2].as_str(), ); + if cmd == "effects" { + return effects(&store, &src, &args[3..]); + } let mut g = match Codegraph::open(&store) { Ok(g) => g, Err(e) => { diff --git a/crates/lance-graph-codegraph/src/effects.rs b/crates/lance-graph-codegraph/src/effects.rs new file mode 100644 index 000000000..11c58feff --- /dev/null +++ b/crates/lance-graph-codegraph/src/effects.rs @@ -0,0 +1,481 @@ +//! A read-only view of `.effects.tsv`: the effects a function body witnesses. +//! +//! ruff_charon writes one row per occurrence (`ruff_charon::store`, version 1, columns +//! `subject effect kind file start end`): an `ASSERT` that can panic (`DivisionByZero`, +//! `BoundsCheck`, `Overflow:`, `User`, ...) or a `DEREF_WRITE` rooted in an argument or a +//! local. Before this module the Codegraph did not read the file at all. +//! +//! These rows are observations inside one body, kept apart from the call rows on purpose: +//! +//! - They are never folded into [`crate::Codegraph::impact`] or any mask. +//! - A function with no row is **not** proven free of that effect. Charon may fold a check into +//! a fallible operation, a callee's effects are not included, and an unstructured body records +//! nothing. [`EffectAnswer::NoneWitnessed`] says exactly that and no more. +//! - A function whose crate has no effects file was not looked at +//! ([`EffectAnswer::CrateNotRead`]). + +use std::collections::BTreeSet; +use std::fs; +use std::path::Path; + +use lance_graph_contract::hash::fnv1a; + +use crate::graph::RunStatus; + +/// File-name suffix of the effects file of a crate. +pub const EFFECTS_SUFFIX: &str = ".effects.tsv"; + +/// The kind of a witnessed effect: the `effect` and `kind` columns. +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum EffectKind { + /// An `Assert` that can panic, with Charon's tag (`DivisionByZero`, `Overflow:Mul`, `User`). + /// The tag set is Charon's and is kept as text. + Assert(String), + /// A write through a dereference rooted in an argument. + DerefWriteArg, + /// A write through a dereference rooted in a local. + DerefWriteLocal, +} + +impl EffectKind { + /// The `effect` and `kind` columns of this kind. + pub fn columns(&self) -> (&'static str, &str) { + match self { + EffectKind::Assert(k) => ("ASSERT", k), + EffectKind::DerefWriteArg => ("DEREF_WRITE", "Arg"), + EffectKind::DerefWriteLocal => ("DEREF_WRITE", "Local"), + } + } + + fn parse(effect: &str, kind: &str) -> Option { + match (effect, kind) { + ("ASSERT", k) if !k.is_empty() => Some(EffectKind::Assert(k.to_owned())), + ("DEREF_WRITE", "Arg") => Some(EffectKind::DerefWriteArg), + ("DEREF_WRITE", "Local") => Some(EffectKind::DerefWriteLocal), + _ => None, + } + } +} + +/// The effect classes a review asks about. Other asserts (bounds, add/sub/mul overflow, a source +/// `assert!`) have no class and are reached through [`EffectKind`] directly. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum EffectClass { + /// `DivisionByZero` and `Overflow:Div`: a `/` the compiler guards. + Division, + /// `RemainderByZero` and `Overflow:Rem`: a `%`. + Remainder, + /// `Overflow:Shl` and `Overflow:Shr`. + Shift, + /// A write through a dereference, rooted in an argument or a local. + Write, +} + +impl EffectKind { + /// The class of this kind, if it has one. + pub fn class(&self) -> Option { + match self { + EffectKind::DerefWriteArg | EffectKind::DerefWriteLocal => Some(EffectClass::Write), + EffectKind::Assert(k) => match k.as_str() { + "DivisionByZero" | "Overflow:Div" => Some(EffectClass::Division), + "RemainderByZero" | "Overflow:Rem" => Some(EffectClass::Remainder), + "Overflow:Shl" | "Overflow:Shr" => Some(EffectClass::Shift), + _ => None, + }, + } + } +} + +/// What identifies one effects file as read: its content digest and row count. The file carries +/// no source revision (ruff_charon effects version 1), so this says which bytes were read, not +/// which commit they were extracted from. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EffectsIdentity { + /// FNV-1a 64 of the file's bytes. + pub fnv64: u64, + /// Data rows read from it. + pub rows: u32, +} + +/// Where an effect sits: a file relative to the source root and a byte span. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EffectSite { + /// The recorded file path. + pub file: String, + /// Span start, in bytes. + pub start: u32, + /// Span end, in bytes. + pub end: u32, +} + +/// One witnessed effect. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Effect { + /// The function whose body holds the effect. + pub subject: String, + /// What was witnessed. + pub kind: EffectKind, + /// Its site; `None` when the row carries `-`. + pub site: Option, +} + +/// Why an effects file was refused. Nothing is guessed: a file that does not match the format +/// is refused whole. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EffectsRefusal { + /// The directory or a file could not be read. + Io(String), + /// The `#version` line is not 1 (0 when absent or unparsable). + Version { + /// The crate. + krate: String, + /// The version found. + found: u32, + }, + /// The `#schema` line is not `subject effect kind file start end`. + Schema { + /// The crate. + krate: String, + }, + /// A data line without exactly six fields; 1-based line. + Arity { + /// The crate. + krate: String, + /// The offending line. + line: usize, + }, + /// An `effect`/`kind` pair outside the format; 1-based line. + UnknownEffect { + /// The crate. + krate: String, + /// The offending line. + line: usize, + }, + /// A site that is neither `- - -` nor a file with `start <= end`; 1-based line. + Site { + /// The crate. + krate: String, + /// The offending line. + line: usize, + }, +} + +/// What the store says about one function's effects. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EffectAnswer<'a> { + /// Rows the body witnesses, in file order. + Witnessed(Vec<&'a Effect>), + /// The crate's effects file was read and holds no matching row. Not a purity verdict. + NoneWitnessed, + /// No effects file was read for the function's crate: nothing was looked at. + CrateNotRead, +} + +/// Every effects file of a store directory. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Effects { + rows: Vec, + crates: BTreeSet, + identities: Vec<(String, EffectsIdentity)>, +} + +const SCHEMA: &str = "#schema subject\teffect\tkind\tfile\tstart\tend"; + +fn parse(krate: &str, text: &str, rows: &mut Vec) -> Result<(), EffectsRefusal> { + let k = || krate.to_owned(); + 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 != 1 { + return Err(EffectsRefusal::Version { krate: k(), found }); + } + if lines.next() != Some(SCHEMA) { + return Err(EffectsRefusal::Schema { krate: k() }); + } + for (i, l) in lines.enumerate() { + let line = i + 3; + if l.is_empty() { + continue; + } + let f: Vec<&str> = l.split('\t').collect(); + let [subject, effect, kind, file, start, end] = f[..] else { + return Err(EffectsRefusal::Arity { krate: k(), line }); + }; + let kind = EffectKind::parse(effect, kind) + .ok_or(EffectsRefusal::UnknownEffect { krate: k(), line })?; + let site = match (file, start.parse::(), end.parse::()) { + ("-", _, _) if start == "-" && end == "-" => None, + (file, Ok(start), Ok(end)) if file != "-" && start <= end => Some(EffectSite { + file: file.to_owned(), + start, + end, + }), + _ => return Err(EffectsRefusal::Site { krate: k(), line }), + }; + rows.push(Effect { + subject: subject.to_owned(), + kind, + site, + }); + } + Ok(()) +} + +impl Effects { + /// Read every `.effects.tsv` of `dir`, sorted by crate name. A directory without any + /// is an empty view in which every crate is [`EffectAnswer::CrateNotRead`]. + pub fn open(dir: &Path) -> Result { + let io = |e: std::io::Error| EffectsRefusal::Io(e.to_string()); + let mut crates = BTreeSet::new(); + for entry in fs::read_dir(dir).map_err(io)? { + let name = entry.map_err(io)?.file_name(); + if let Some(stem) = name.to_str().and_then(|n| n.strip_suffix(EFFECTS_SUFFIX)) { + crates.insert(stem.to_owned()); + } + } + let mut rows = Vec::new(); + let mut identities = Vec::new(); + for krate in &crates { + let text = + fs::read_to_string(dir.join(format!("{krate}{EFFECTS_SUFFIX}"))).map_err(io)?; + let before = rows.len(); + parse(krate, &text, &mut rows)?; + identities.push(( + krate.clone(), + EffectsIdentity { + fnv64: fnv1a(text.as_bytes()), + rows: (rows.len() - before) as u32, + }, + )); + } + Ok(Effects { + rows, + crates, + identities, + }) + } + + /// The crates whose effects file was read, sorted. + pub fn crates(&self) -> impl Iterator { + self.crates.iter().map(String::as_str) + } + + /// The identity of `krate`'s effects file; `None` when none was read. + pub fn identity(&self, krate: &str) -> Option { + self.identities + .iter() + .find(|(k, _)| k == krate) + .map(|(_, i)| *i) + } + + /// Every row, in crate then file order. + pub fn rows(&self) -> &[Effect] { + &self.rows + } + + /// The effects witnessed in the body of `subject`. The subject's crate is its first path + /// segment. + pub fn of(&self, subject: &str) -> EffectAnswer<'_> { + let krate = subject.split("::").next().unwrap_or(subject); + if !self.crates.contains(krate) { + return EffectAnswer::CrateNotRead; + } + let hits: Vec<&Effect> = self.rows.iter().filter(|e| e.subject == subject).collect(); + if hits.is_empty() { + EffectAnswer::NoneWitnessed + } else { + EffectAnswer::Witnessed(hits) + } + } + + /// Every row of `kind`, in crate then file order. An empty answer covers only the crates in + /// [`Effects::crates`]. + pub fn with_kind<'a>(&'a self, kind: &'a EffectKind) -> impl Iterator { + self.rows.iter().filter(move |e| e.kind == *kind) + } +} + +/// How a function reaches an effect site. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ReachGrade { + /// The effect is in the function's own body. + Own, + /// Through resolved calls only. + Confirmed, + /// Through at least one trait dispatch the compiler did not resolve. + Possible, + /// Through a dynamic call or function pointer. + Unknown, +} + +/// One effect a function reaches, with how. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReachedEffect<'a> { + /// The effect row. + pub effect: &'a Effect, + /// How the queried function reaches the body that holds it. + pub grade: ReachGrade, +} + +/// A reason the reach answer may be missing effects. Each comes from a store fact. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ReachLimit { + /// A crate with call rows has no effects file: its bodies' effects were not read. + EffectsNotRead { + /// The crate. + krate: String, + }, + /// A crate's harvest did not finish, recorded residuals, or has no residuals file, so a call + /// or a body may be missing. + HarvestIncomplete { + /// The crate. + krate: String, + }, + /// The function reaches symbols of crates that are not in the store (`core`, a dependency + /// that was not extracted). Their bodies, and so their effects, were never read. + CalleesOutsideStore { + /// The crates reached, sorted. + crates: Vec, + }, + /// The function holds or reaches a dynamic call; an effect behind it has no call row to be + /// reached by. + DynamicCalls { + /// `UNRESOLVED_CALL` rows in the store. + store_dynamic_calls: u32, + }, + /// A matching effect's subject has no position in the call rows, so whether the function + /// reaches it is not known. + Unplaced { + /// The effect's subject. + subject: String, + }, +} + +/// The effects a function reaches through the call rows. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Reached<'a> { + /// Reached rows, in effects-file order. + pub hits: Vec>, + /// Why the answer may be missing effects; empty only when nothing known is missing. + pub limits: Vec, +} + +impl Reached<'_> { + /// `true` when a limit is known: the answer is then partial, and an empty `hits` is + /// "unknown", never "none". `false` is still not a proof of absence (see [`Effects`]). + pub fn partial(&self) -> bool { + !self.limits.is_empty() + } +} + +impl crate::Codegraph { + /// The effect rows `keep` selects whose body `symbol` is or reaches. + /// + /// This joins two observation lists and inherits the limits of both. What is known to be + /// missing is listed in [`Reached::limits`]: a crate without an effects file, an incomplete + /// harvest, callees in crates outside the store, dynamic calls, an effect that cannot be + /// placed. `None` when `symbol` has no + /// position. + pub fn effects_reached<'a>( + &self, + symbol: &str, + effects: &'a Effects, + keep: impl Fn(&EffectKind) -> bool, + ) -> Result>, crate::query::QueryRefusal> { + let Some(me) = self.pos(symbol) else { + return Ok(None); + }; + let has = |mask: &[u64]| { + mask.get(me as usize / 64) + .is_some_and(|w| w >> (me % 64) & 1 == 1) + }; + let mut limits = Vec::new(); + for (krate, status) in self.crate_list() { + if effects.identity(&krate).is_none() { + limits.push(ReachLimit::EffectsNotRead { + krate: krate.clone(), + }); + } + let clean_harvest = + matches!(self.crate_residuals(&krate), Some(Some(r)) if r.total() == 0); + if status == RunStatus::Partial || !clean_harvest { + limits.push(ReachLimit::HarvestIncomplete { krate }); + } + } + // Symbols outside every stored crate that the function reaches: who reaches each one is + // its impact, read in the same direction as the effect subjects below. + let stored_owner = self.stored_owner(); + let mut outside = BTreeSet::new(); + let mut reaches_dynamic = false; + for p in 0..(self.words() as u32 * 64) { + let stored = stored_owner + .get(p as usize / 64) + .is_some_and(|w| w >> (p % 64) & 1 == 1); + let Some(name) = self.symbol(p).filter(|_| !stored && p != me) else { + continue; + }; + let i = self.impact(p)?; + if has(&i.confirmed) || has(&i.possible) || has(&i.unknown) { + // `?dynamic` is the position every unresolved call points at, not a crate. + if name.starts_with('?') { + reaches_dynamic = true; + } else { + outside.insert(name.split("::").next().unwrap_or(name).to_owned()); + } + } + } + if !outside.is_empty() { + limits.push(ReachLimit::CalleesOutsideStore { + crates: outside.into_iter().collect(), + }); + } + if reaches_dynamic { + limits.push(ReachLimit::DynamicCalls { + store_dynamic_calls: self.coverage().dynamic_calls, + }); + } + let mut hits = Vec::new(); + let mut unplaced = BTreeSet::new(); + // One impact per distinct subject: who reaches the body that holds the effect. + let mut graded: Vec<(&str, Option)> = Vec::new(); + for e in effects.rows().iter().filter(|e| keep(&e.kind)) { + let subject = e.subject.as_str(); + let grade = match graded.iter().find(|(s, _)| *s == subject) { + Some((_, g)) => *g, + None => { + let g = match self.pos(subject) { + None => { + unplaced.insert(subject); + None + } + Some(p) if p == me => Some(ReachGrade::Own), + Some(p) => { + let i = self.impact(p)?; + if has(&i.confirmed) { + Some(ReachGrade::Confirmed) + } else if has(&i.possible) { + Some(ReachGrade::Possible) + } else if has(&i.unknown) { + Some(ReachGrade::Unknown) + } else { + None + } + } + }; + graded.push((subject, g)); + g + } + }; + if let Some(grade) = grade { + hits.push(ReachedEffect { effect: e, grade }); + } + } + limits.extend(unplaced.into_iter().map(|s| ReachLimit::Unplaced { + subject: s.to_owned(), + })); + Ok(Some(Reached { hits, limits })) + } +} diff --git a/crates/lance-graph-codegraph/src/lib.rs b/crates/lance-graph-codegraph/src/lib.rs index f81ba6eb2..ed0dcf42a 100644 --- a/crates/lance-graph-codegraph/src/lib.rs +++ b/crates/lance-graph-codegraph/src/lib.rs @@ -6,6 +6,7 @@ pub mod artifact; pub mod artifact_bin; pub mod diag; +pub mod effects; pub mod graph; pub mod impact_report; #[cfg(test)] diff --git a/crates/lance-graph-codegraph/tests/data/README.md b/crates/lance-graph-codegraph/tests/data/README.md index 22bd2f79f..52091008e 100644 --- a/crates/lance-graph-codegraph/tests/data/README.md +++ b/crates/lance-graph-codegraph/tests/data/README.md @@ -6,3 +6,4 @@ Store artifacts (rows v2) written by ruff `crates/ruff_charon` (`ruff_charon imp - `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). +- `effects/`: `foundry_parity_lab.{effects.tsv,rows.bin,residuals.tsv}` (213 effect rows; no `files.tsv`), written by `ruff_charon import` from a Charon `c69e231` extraction of the library target of `crates/foundry-parity-lab` at lance-graph `c7151a1`. Real data for the effects reader (`src/effects.rs`). diff --git a/crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.effects.tsv b/crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.effects.tsv new file mode 100644 index 000000000..0e3ab05fa --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.effects.tsv @@ -0,0 +1,215 @@ +#version 1 +#schema subject effect kind file start end +foundry_parity_lab::arm_b::demanded_fields ASSERT Overflow:Add src/arm_b.rs 1772 1782 +foundry_parity_lab::arm_b::demanded_fields ASSERT Overflow:Add src/arm_b.rs 1800 1810 +foundry_parity_lab::arm_b::demanded_fields ASSERT Overflow:Add src/arm_b.rs 1850 1859 +foundry_parity_lab::arm_b::demanded_fields ASSERT Overflow:Add src/arm_b.rs 1901 1917 +foundry_parity_lab::arm_b::demanded_fields ASSERT Overflow:Add src/arm_b.rs 1927 1947 +foundry_parity_lab::arm_b::demanded_fields ASSERT Overflow:Add src/arm_b.rs 1957 1969 +foundry_parity_lab::bind::field ASSERT Overflow:Sub src/bind.rs 2394 2406 +foundry_parity_lab::bind::field ASSERT Overflow:Sub src/bind.rs 2341 2351 +foundry_parity_lab::bind::field ASSERT Overflow:Sub src/bind.rs 2283 2293 +foundry_parity_lab::bind::field ASSERT Overflow:Sub src/bind.rs 2232 2241 +foundry_parity_lab::bind::field ASSERT Overflow:Sub src/bind.rs 2181 2191 +foundry_parity_lab::bind::field ASSERT Overflow:Sub src/bind.rs 2130 2140 +foundry_parity_lab::fixture::mix ASSERT Overflow:Shr src/fixture.rs 2483 2490 +foundry_parity_lab::fixture::mix ASSERT Overflow:Shr src/fixture.rs 2548 2555 +foundry_parity_lab::fixture::mix ASSERT Overflow:Shr src/fixture.rs 2612 2621 +foundry_parity_lab::fixture::h ASSERT Overflow:Shl src/fixture.rs 2677 2688 +foundry_parity_lab::fixture::h ASSERT Overflow:Shl src/fixture.rs 2691 2709 +foundry_parity_lab::fixture::recipe ASSERT RemainderByZero src/fixture.rs 3351 3364 +foundry_parity_lab::fixture::recipe ASSERT RemainderByZero src/fixture.rs 3381 3391 +foundry_parity_lab::fixture::recipe ASSERT RemainderByZero src/fixture.rs 3416 3430 +foundry_parity_lab::fixture::recipe ASSERT Overflow:Rem src/fixture.rs 3416 3430 +foundry_parity_lab::fixture::recipe ASSERT Overflow:Add src/fixture.rs 3412 3430 +foundry_parity_lab::fixture::recipe ASSERT RemainderByZero src/fixture.rs 3463 3477 +foundry_parity_lab::fixture::recipe ASSERT Overflow:Rem src/fixture.rs 3463 3477 +foundry_parity_lab::fixture::recipe ASSERT Overflow:Mul src/fixture.rs 3459 3477 +foundry_parity_lab::fixture::recipe ASSERT Overflow:Add src/fixture.rs 3454 3477 +foundry_parity_lab::fixture::recipe DEREF_WRITE Local src/fixture.rs 2849 2890 +foundry_parity_lab::fixture::stock ASSERT RemainderByZero src/fixture.rs 4197 4214 +foundry_parity_lab::fixture::stock ASSERT Overflow:Add src/fixture.rs 4192 4221 +foundry_parity_lab::fixture::unit_cost ASSERT RemainderByZero src/fixture.rs 4530 4548 +foundry_parity_lab::fixture::unit_cost ASSERT Overflow:Add src/fixture.rs 4525 4555 +foundry_parity_lab::fixture::staff ASSERT RemainderByZero src/fixture.rs 4940 4956 +foundry_parity_lab::fixture::delivery_slots ASSERT RemainderByZero src/fixture.rs 5267 5283 +foundry_parity_lab::fixture::delivery_minutes ASSERT RemainderByZero src/fixture.rs 5447 5464 +foundry_parity_lab::fixture::delivery_minutes ASSERT Overflow:Add src/fixture.rs 5442 5471 +foundry_parity_lab::fixture::store_id ASSERT Overflow:Add src/fixture.rs 5546 5570 +foundry_parity_lab::method::method_id ASSERT Overflow:Shl src/method.rs 2125 2142 +foundry_parity_lab::method::can_cook_guard ASSERT Overflow:Add src/method.rs 2348 2362 +foundry_parity_lab::method::can_cook_guard ASSERT Overflow:Add src/method.rs 2393 2415 +foundry_parity_lab::method::can_cook_guard DEREF_WRITE Local src/method.rs 2321 2430 +foundry_parity_lab::method::can_cook_guard ASSERT Overflow:Add src/method.rs 2493 2505 +foundry_parity_lab::method::can_cook_guard ASSERT Overflow:Add src/method.rs 2546 2558 +foundry_parity_lab::method::can_cook_guard ASSERT Overflow:Mul src/method.rs 2576 2581 +foundry_parity_lab::method::can_cook_guard ASSERT Overflow:Add src/method.rs 2700 2715 +foundry_parity_lab::method::can_cook_guard ASSERT Overflow:Sub src/method.rs 2699 2720 +foundry_parity_lab::method::can_cook_guard ASSERT DivisionByZero src/method.rs 2699 2734 +foundry_parity_lab::method::can_cook_guard ASSERT Overflow:Div src/method.rs 2699 2734 +foundry_parity_lab::method::can_cook_guard ASSERT Overflow:Add src/method.rs 2762 2780 +foundry_parity_lab::method::rename ASSERT BoundsCheck src/method.rs 5911 5929 +foundry_parity_lab::method::rename ASSERT BoundsCheck src/method.rs 6209 6230 +foundry_parity_lab::method::rename ASSERT BoundsCheck src/method.rs 6524 6545 +foundry_parity_lab::source::mask_from ASSERT Overflow:Shl src/source.rs 2225 2231 +foundry_parity_lab::source::mask_from DEREF_WRITE Local src/source.rs 2253 2260 +foundry_parity_lab::arm_a::{Objects}::can_cook ASSERT DivisionByZero src/arm_a.rs 2572 2577 +foundry_parity_lab::arm_a::{Objects}::can_cook ASSERT Overflow:Div src/arm_a.rs 2572 2577 +foundry_parity_lab::arm_a::{Objects}::can_cook ASSERT Overflow:Mul src/arm_a.rs 3096 3126 +foundry_parity_lab::arm_a::{Objects}::store_costs ASSERT BoundsCheck src/arm_a.rs 4270 4276 +foundry_parity_lab::arm_a::{Objects}::store_costs ASSERT BoundsCheck src/arm_a.rs 4270 4279 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::open ASSERT Overflow:Sub src/arm_b.rs 5030 5047 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::open ASSERT Overflow:Sub src/arm_b.rs 5306 5323 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::open ASSERT Overflow:Sub src/arm_b.rs 4871 4888 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::open ASSERT BoundsCheck src/arm_b.rs 4722 4747 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::mask ASSERT BoundsCheck src/arm_b.rs 5798 5811 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::lane ASSERT BoundsCheck src/arm_b.rs 6109 6122 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Mul src/arm_b.rs 6775 6793 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 6771 6793 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 6828 6848 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 6882 6902 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7706 7722 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7746 7761 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Sub src/arm_b.rs 7745 7766 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT DivisionByZero src/arm_b.rs 7745 7780 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Div src/arm_b.rs 7745 7780 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7298 7308 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7436 7446 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Mul src/arm_b.rs 7473 7478 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7436 7446 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Mul src/arm_b.rs 7473 7478 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7706 7722 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7746 7761 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Sub src/arm_b.rs 7745 7766 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT DivisionByZero src/arm_b.rs 7745 7780 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Div src/arm_b.rs 7745 7780 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7298 7308 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7436 7446 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Mul src/arm_b.rs 7473 7478 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Add src/arm_b.rs 7436 7446 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops ASSERT Overflow:Mul src/arm_b.rs 7473 7478 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::denied_mask DEREF_WRITE Local src/arm_b.rs 9472 9494 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::unknown_mask DEREF_WRITE Local src/arm_b.rs 9659 9681 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::unknown_mask ASSERT Overflow:Add src/arm_b.rs 9713 9745 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::unknown_mask ASSERT Overflow:Add src/arm_b.rs 10010 10020 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::store_at ASSERT BoundsCheck src/arm_b.rs 10345 10356 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::max_quantities ASSERT BoundsCheck src/arm_b.rs 11113 11124 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::max_quantities ASSERT Overflow:Sub src/arm_b.rs 11112 11141 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::max_quantities ASSERT BoundsCheck src/arm_b.rs 11108 11151 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::max_quantities ASSERT Overflow:Sub src/arm_b.rs 11181 11189 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 11907 11918 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT Overflow:Sub src/arm_b.rs 11906 11935 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT Overflow:Shr src/arm_b.rs 11989 12000 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT Overflow:Shr src/arm_b.rs 12083 12095 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 12222 12228 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 12260 12266 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 12222 12228 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 12260 12266 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT Overflow:Shr src/arm_b.rs 12083 12095 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 12222 12228 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 12260 12266 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 12222 12228 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 12260 12266 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::answers ASSERT BoundsCheck src/arm_b.rs 11958 11964 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::network_capacity ASSERT Overflow:Add src/arm_b.rs 12893 12910 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::network_cost ASSERT Overflow:Add src/arm_b.rs 13353 13362 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::network_cost ASSERT Overflow:Mul src/arm_b.rs 13647 13652 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::network_cost ASSERT Overflow:Mul src/arm_b.rs 13637 13657 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::network_cost ASSERT Overflow:Add src/arm_b.rs 13628 13657 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::fulfill_mask ASSERT Overflow:Sub src/arm_b.rs 14237 14272 +foundry_parity_lab::arm_c::cost_fields::{impl FnMut<(usize,)> for foundry_parity_lab::arm_c::cost_fields::{closure}}::call_mut ASSERT Overflow:Add src/arm_c.rs 2499 2510 +foundry_parity_lab::arm_c::{PreparedDemand}::prepare ASSERT Overflow:Mul src/arm_c.rs 3880 3897 +foundry_parity_lab::arm_c::{PreparedDemand}::prepare ASSERT Overflow:Mul src/arm_c.rs 3995 4003 +foundry_parity_lab::arm_c::{PreparedDemand}::prepare ASSERT Overflow:Add src/arm_c.rs 3995 4007 +foundry_parity_lab::arm_c::{PreparedDemand}::prepare DEREF_WRITE Local src/arm_c.rs 3993 4051 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open ASSERT Overflow:Mul src/arm_c.rs 5160 5179 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open ASSERT Overflow:Add src/arm_c.rs 6368 6379 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open DEREF_WRITE Local src/arm_c.rs 6622 6830 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open ASSERT Overflow:Mul src/arm_c.rs 7332 7345 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open ASSERT Overflow:Add src/arm_c.rs 5539 5557 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open ASSERT Overflow:Sub src/arm_c.rs 5605 5614 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open ASSERT BoundsCheck src/arm_c.rs 5636 5644 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open ASSERT Overflow:Mul src/arm_c.rs 5743 5759 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open ASSERT Overflow:Add src/arm_c.rs 5743 5763 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open ASSERT BoundsCheck src/arm_c.rs 5741 5764 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open DEREF_WRITE Local src/arm_c.rs 5741 5870 +foundry_parity_lab::arm_c::{CostQuery<'s>}::per_store ASSERT Overflow:Shr src/arm_c.rs 8366 8382 +foundry_parity_lab::arm_c::{CostQuery<'s>}::per_store ASSERT BoundsCheck src/arm_c.rs 8455 8466 +foundry_parity_lab::arm_c::{CostQuery<'s>}::per_store ASSERT Overflow:Sub src/arm_c.rs 8454 8483 +foundry_parity_lab::arm_c::{CostQuery<'s>}::per_store ASSERT Overflow:Mul src/arm_c.rs 8586 8594 +foundry_parity_lab::arm_c::{CostQuery<'s>}::per_store ASSERT Overflow:Add src/arm_c.rs 8586 8598 +foundry_parity_lab::arm_c::{CostQuery<'s>}::per_store ASSERT BoundsCheck src/arm_c.rs 8584 8599 +foundry_parity_lab::arm_c::{CostQuery<'s>}::per_store ASSERT Overflow:Mul src/arm_c.rs 8559 8600 +foundry_parity_lab::arm_c::{CostQuery<'s>}::per_store ASSERT BoundsCheck src/arm_c.rs 8542 8548 +foundry_parity_lab::arm_c::{CostQuery<'s>}::per_store ASSERT BoundsCheck src/arm_c.rs 8542 8551 +foundry_parity_lab::bind::KNOWN0 ASSERT Overflow:Add src/bind.rs 1294 1313 +foundry_parity_lab::bind::COST0 ASSERT Overflow:Add src/bind.rs 1340 1359 +foundry_parity_lab::bind::STAFF0 ASSERT Overflow:Add src/bind.rs 1387 1405 +foundry_parity_lab::bind::EQUIP0 ASSERT Overflow:Add src/bind.rs 1433 1448 +foundry_parity_lab::bind::OFFERED0 ASSERT Overflow:Add src/bind.rs 1478 1496 +foundry_parity_lab::bind::SLOTS ASSERT Overflow:Add src/bind.rs 1523 1539 +foundry_parity_lab::bind::MINUTES ASSERT Overflow:Add src/bind.rs 1568 1577 +foundry_parity_lab::bind::STORE_ID ASSERT Overflow:Add src/bind.rs 1607 1618 +foundry_parity_lab::bind::FIELD_COUNT ASSERT Overflow:Add src/bind.rs 1726 1738 +foundry_parity_lab::fixture::recipe::{impl FnMut<(usize,)> for foundry_parity_lab::fixture::recipe::{closure}<'_0>}::call_mut ASSERT Overflow:Mul src/fixture.rs 3111 3116 +foundry_parity_lab::fixture::recipe::{impl FnMut<(usize,)> for foundry_parity_lab::fixture::recipe::{closure}<'_0>}::call_mut ASSERT Overflow:Mul src/fixture.rs 3119 3125 +foundry_parity_lab::fixture::recipe::{impl FnMut<(usize,)> for foundry_parity_lab::fixture::recipe::{closure}<'_0>}::call_mut ASSERT Overflow:Add src/fixture.rs 3111 3125 +foundry_parity_lab::fixture::recipe::{impl FnMut<(usize,)> for foundry_parity_lab::fixture::recipe::{closure}<'_0>}::call_mut ASSERT Overflow:Add src/fixture.rs 3110 3130 +foundry_parity_lab::fixture::recipe::{impl FnMut<(usize,)> for foundry_parity_lab::fixture::recipe::{closure}<'_0>}::call_mut ASSERT RemainderByZero src/fixture.rs 3110 3143 +foundry_parity_lab::fixture::recipe::{impl FnMut<(usize,)> for foundry_parity_lab::fixture::recipe::{closure}<'_0>}::call_mut ASSERT Overflow:Add src/fixture.rs 3165 3186 +foundry_parity_lab::fixture::recipe::{impl FnMut<(usize,)> for foundry_parity_lab::fixture::recipe::{closure}<'_0>}::call_mut ASSERT RemainderByZero src/fixture.rs 3165 3190 +foundry_parity_lab::fixture::recipe::{impl FnMut<(usize,)> for foundry_parity_lab::fixture::recipe::{closure}<'_0>}::call_mut ASSERT Overflow:Rem src/fixture.rs 3165 3190 +foundry_parity_lab::fixture::recipe::{impl FnMut<(usize,)> for foundry_parity_lab::fixture::recipe::{closure}<'_0>}::call_mut ASSERT Overflow:Add src/fixture.rs 3161 3190 +foundry_parity_lab::fixture::DENIED_STORE ASSERT Overflow:Add src/fixture.rs 5677 5712 +foundry_parity_lab::method::{PreparedMethod<'s>}::prepare ASSERT BoundsCheck src/method.rs 9766 9781 +foundry_parity_lab::method::{PreparedMethod<'s>}::prepare DEREF_WRITE Local src/method.rs 9892 9943 +foundry_parity_lab::method::{PreparedMethod<'s>}::prepare ASSERT Overflow:Sub src/method.rs 9256 9273 +foundry_parity_lab::method::{PreparedMethod<'s>}::prepare ASSERT Overflow:Sub src/method.rs 9515 9532 +foundry_parity_lab::method::{PreparedMethod<'s>}::prepare ASSERT Overflow:Sub src/method.rs 9098 9115 +foundry_parity_lab::method::{PreparedMethod<'s>}::prepare ASSERT BoundsCheck src/method.rs 8965 8974 +foundry_parity_lab::method::{PreparedMethod<'s>}::run ASSERT BoundsCheck src/method.rs 11594 11601 +foundry_parity_lab::method::{PreparedMethod<'s>}::run ASSERT BoundsCheck src/method.rs 11746 11753 +foundry_parity_lab::method::{PreparedMethod<'s>}::run ASSERT BoundsCheck src/method.rs 11857 11870 +foundry_parity_lab::method::{PreparedMethod<'s>}::run ASSERT Overflow:Sub src/method.rs 11856 11887 +foundry_parity_lab::method::{PreparedMethod<'s>}::run ASSERT Overflow:Shl src/method.rs 11851 11887 +foundry_parity_lab::method::{PreparedMethod<'s>}::run ASSERT Overflow:Sub src/method.rs 11906 11911 +foundry_parity_lab::oracle::{Oracle}::can_cook ASSERT DivisionByZero src/oracle.rs 2063 2098 +foundry_parity_lab::oracle::{Oracle}::can_cook ASSERT Overflow:Div src/oracle.rs 2063 2098 +foundry_parity_lab::oracle::{Oracle}::can_cook ASSERT Overflow:Mul src/oracle.rs 2250 2285 +foundry_parity_lab::oracle::{Oracle}::can_fulfill ASSERT Overflow:Add src/oracle.rs 2876 2928 +foundry_parity_lab::source::{Owned}::bytes ASSERT Overflow:Mul src/source.rs 952 963 +foundry_parity_lab::source::{Owned}::bytes ASSERT Overflow:Mul src/source.rs 995 1006 +foundry_parity_lab::source::{Owned}::bytes ASSERT Overflow:Mul src/source.rs 910 921 +foundry_parity_lab::source::{Order}::row ASSERT Overflow:Sub src/source.rs 1425 1435 +foundry_parity_lab::source::{Order}::row ASSERT Overflow:Sub src/source.rs 1425 1439 +foundry_parity_lab::source::{Network}::rebuild DEREF_WRITE Arg src/source.rs 3012 3023 +foundry_parity_lab::source::{Network}::rebuild DEREF_WRITE Arg src/source.rs 3012 3023 +foundry_parity_lab::source::{Network}::restock ASSERT Overflow:Add src/source.rs 4320 4352 +foundry_parity_lab::source::{Network}::restock ASSERT BoundsCheck src/source.rs 4422 4429 +foundry_parity_lab::source::{Network}::restock DEREF_WRITE Local src/source.rs 4422 4480 +foundry_parity_lab::source::{Network}::restock DEREF_WRITE Arg src/source.rs 4500 4521 +foundry_parity_lab::source::{Network}::restock ASSERT Overflow:Add src/source.rs 4542 4576 +foundry_parity_lab::source::{Network}::set_unit_cost ASSERT Overflow:Add src/source.rs 5102 5124 +foundry_parity_lab::source::{Network}::set_unit_cost ASSERT BoundsCheck src/source.rs 5140 5147 +foundry_parity_lab::source::{Network}::set_unit_cost DEREF_WRITE Local src/source.rs 5140 5155 +foundry_parity_lab::source::{Network}::set_unit_cost ASSERT Overflow:Add src/source.rs 5186 5220 +foundry_parity_lab::source::impl_LaneSource_for_Network::borrow ASSERT Overflow:Add src/source.rs 6300 6322 +foundry_parity_lab::arm_a::{Objects}::bytes::{impl FnMut<(&'_0 StoreObject,)> for foundry_parity_lab::arm_a::{Objects}::bytes::{closure}}::call_mut ASSERT Overflow:Mul src/arm_a.rs 1917 1934 +foundry_parity_lab::arm_a::{Objects}::bytes::{impl FnMut<(&'_0 StoreObject,)> for foundry_parity_lab::arm_a::{Objects}::bytes::{closure}}::call_mut ASSERT Overflow:Mul src/arm_a.rs 1957 1973 +foundry_parity_lab::arm_a::{Objects}::bytes::{impl FnMut<(&'_0 StoreObject,)> for foundry_parity_lab::arm_a::{Objects}::bytes::{closure}}::call_mut ASSERT Overflow:Add src/arm_a.rs 1917 1973 +foundry_parity_lab::arm_a::{Objects}::bytes::{impl FnMut<(&'_0 StoreObject,)> for foundry_parity_lab::arm_a::{Objects}::bytes::{closure}}::call_mut ASSERT Overflow:Mul src/arm_a.rs 1996 2013 +foundry_parity_lab::arm_a::{Objects}::bytes::{impl FnMut<(&'_0 StoreObject,)> for foundry_parity_lab::arm_a::{Objects}::bytes::{closure}}::call_mut ASSERT Overflow:Add src/arm_a.rs 1917 2013 +foundry_parity_lab::arm_a::{Objects}::bytes::{impl FnMut<(&'_0 StoreObject,)> for foundry_parity_lab::arm_a::{Objects}::bytes::{closure}}::call_mut ASSERT Overflow:Add src/arm_a.rs 1917 2053 +foundry_parity_lab::arm_a::{Objects}::bytes::{impl FnMut<(&'_0 StoreObject,)> for foundry_parity_lab::arm_a::{Objects}::bytes::{closure}}::call_mut ASSERT Overflow:Add src/arm_a.rs 1917 2091 +foundry_parity_lab::arm_a::{Objects}::bytes::{impl FnMut<(&'_0 StoreObject,)> for foundry_parity_lab::arm_a::{Objects}::bytes::{closure}}::call_mut ASSERT Overflow:Add src/arm_a.rs 1917 2116 +foundry_parity_lab::arm_a::{Objects}::store_cost::{impl FnMut<(&'_2 (usize, i32),)> for foundry_parity_lab::arm_a::{Objects}::store_cost::{closure}<'_0, '_1>}::call_mut ASSERT Overflow:Mul src/arm_a.rs 3736 3741 +foundry_parity_lab::arm_a::{Objects}::store_cost::{impl FnMut<(&'_2 (usize, i32),)> for foundry_parity_lab::arm_a::{Objects}::store_cost::{closure}<'_0, '_1>}::call_mut ASSERT Overflow:Mul src/arm_a.rs 3726 3765 +foundry_parity_lab::arm_a::{Objects}::store_costs::{impl FnMut<(&'_2 (usize, i32),)> for foundry_parity_lab::arm_a::{Objects}::store_costs::{closure}<'_0, '_1>}::call_mut ASSERT Overflow:Mul src/arm_a.rs 4398 4403 +foundry_parity_lab::arm_a::{Objects}::store_costs::{impl FnMut<(&'_2 (usize, i32),)> for foundry_parity_lab::arm_a::{Objects}::store_costs::{closure}<'_0, '_1>}::call_mut ASSERT Overflow:Mul src/arm_a.rs 4388 4427 +foundry_parity_lab::arm_b::{ItemQuery<'s>}::keep::{impl FnOnce<(&'_1 [u64],)> for foundry_parity_lab::arm_b::{ItemQuery<'s>}::keep::{closure}<'s>}::call_once ASSERT BoundsCheck src/arm_b.rs 9002 9006 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open::{impl FnMut<(&'_2 usize,)> for foundry_parity_lab::arm_c::{CostQuery<'s>}::open::{closure#1}<'s, '_1, V>[TraitClause0, TraitClause1]}::call_mut ASSERT BoundsCheck src/arm_c.rs 6286 6294 +foundry_parity_lab::arm_c::{CostQuery<'s>}::open::{impl FnOnce<(&'_1 [u64],)> for foundry_parity_lab::arm_c::{CostQuery<'s>}::open::{closure#2}<'s, V>[TraitClause0, TraitClause1]}::call_once ASSERT BoundsCheck src/arm_c.rs 7135 7139 +foundry_parity_lab::oracle::{Oracle}::store_cost::{impl FnMut<(&'_2 (usize, i32),)> for foundry_parity_lab::oracle::{Oracle}::store_cost::{closure}<'_0, '_1>}::call_mut ASSERT Overflow:Mul src/oracle.rs 3760 3765 +foundry_parity_lab::oracle::{Oracle}::store_cost::{impl FnMut<(&'_2 (usize, i32),)> for foundry_parity_lab::oracle::{Oracle}::store_cost::{closure}<'_0, '_1>}::call_mut ASSERT Overflow:Mul src/oracle.rs 3750 3799 +foundry_parity_lab::oracle::{Oracle}::network_cost::{closure#1}::{impl FnMut<(&'_2 (usize, i32),)> for foundry_parity_lab::oracle::{Oracle}::network_cost::{closure#1}::{closure}<'_0, '_1>}::call_mut ASSERT Overflow:Mul src/oracle.rs 4237 4242 +foundry_parity_lab::oracle::{Oracle}::network_cost::{closure#1}::{impl FnMut<(&'_2 (usize, i32),)> for foundry_parity_lab::oracle::{Oracle}::network_cost::{closure#1}::{closure}<'_0, '_1>}::call_mut ASSERT Overflow:Mul src/oracle.rs 4227 4276 diff --git a/crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.residuals.tsv b/crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.residuals.tsv new file mode 100644 index 000000000..5ff2b1d84 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.residuals.tsv @@ -0,0 +1,2 @@ +#version 1 +#schema reason subject diff --git a/crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.rows.bin b/crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.rows.bin new file mode 100644 index 0000000000000000000000000000000000000000..44f3f466c67c26b9745e4c8b0db40e9430ec5e80 GIT binary patch literal 90736 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 Fcmaww8zTS! literal 0 HcmV?d00001 diff --git a/crates/lance-graph-codegraph/tests/effects.rs b/crates/lance-graph-codegraph/tests/effects.rs new file mode 100644 index 000000000..81191ea9e --- /dev/null +++ b/crates/lance-graph-codegraph/tests/effects.rs @@ -0,0 +1,545 @@ +//! `Effects`: the real foundry_parity_lab file and synthetic refusal cases. + +use std::ops::Deref; +use std::path::{Path, PathBuf}; + +use lance_graph_codegraph::effects::{ + EffectAnswer, EffectClass, EffectKind, EffectSite, Effects, EffectsRefusal, ReachGrade, + ReachLimit, +}; +use lance_graph_codegraph::Codegraph; + +const HEAD: &str = "#version 1\n#schema subject\teffect\tkind\tfile\tstart\tend\n"; + +fn real() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/effects") +} + +struct Tmp(PathBuf); + +impl Tmp { + fn new(tag: &str) -> Tmp { + let p = std::env::temp_dir().join(format!("cg_effects_{tag}_{}", std::process::id())); + if p.exists() { + std::fs::remove_dir_all(&p).unwrap(); + } + std::fs::create_dir_all(&p).unwrap(); + Tmp(p) + } +} + +impl Deref for Tmp { + type Target = Path; + fn deref(&self) -> &Path { + &self.0 + } +} + +impl Drop for Tmp { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +fn write(dir: &Path, krate: &str, body: &str) { + std::fs::write(dir.join(format!("{krate}.effects.tsv")), body).unwrap(); +} + +const GUARD: &str = "foundry_parity_lab::method::can_cook_guard"; + +#[test] +fn the_real_file_reads_every_row() { + let e = Effects::open(&real()).unwrap(); + assert_eq!(e.rows().len(), 213); + assert_eq!(e.crates().collect::>(), ["foundry_parity_lab"]); +} + +#[test] +fn division_sites_are_found_by_kind() { + let e = Effects::open(&real()).unwrap(); + let div = EffectKind::Assert("DivisionByZero".into()); + let got: Vec<(&str, &str, u32, u32)> = e + .with_kind(&div) + .map(|r| { + let s = r.site.as_ref().unwrap(); + (r.subject.as_str(), s.file.as_str(), s.start, s.end) + }) + .collect(); + assert_eq!( + got, + vec![ + (GUARD, "src/method.rs", 2699, 2734), + ( + "foundry_parity_lab::arm_a::{Objects}::can_cook", + "src/arm_a.rs", + 2572, + 2577 + ), + ( + "foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops", + "src/arm_b.rs", + 7745, + 7780 + ), + ( + "foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops", + "src/arm_b.rs", + 7745, + 7780 + ), + ( + "foundry_parity_lab::oracle::{Oracle}::can_cook", + "src/oracle.rs", + 2063, + 2098 + ), + ] + ); + assert_eq!( + e.with_kind(&EffectKind::Assert("NoSuchKind".into())) + .count(), + 0 + ); + assert_eq!(e.with_kind(&EffectKind::DerefWriteArg).count(), 3); +} + +#[test] +fn a_function_with_rows_is_witnessed() { + let e = Effects::open(&real()).unwrap(); + match e.of(GUARD) { + EffectAnswer::Witnessed(v) => { + // 11 rows in the fixture carry this subject (counted with awk on column 1). + assert_eq!(v.len(), 11); + assert!(v + .iter() + .any(|r| r.kind == EffectKind::Assert("DivisionByZero".into()))); + } + other => panic!("expected Witnessed, got {other:?}"), + } +} + +#[test] +fn no_row_is_not_a_purity_verdict() { + let e = Effects::open(&real()).unwrap(); + assert_eq!( + e.of("foundry_parity_lab::no_such_function"), + EffectAnswer::NoneWitnessed + ); +} + +#[test] +fn an_unread_crate_is_not_looked_at() { + let e = Effects::open(&real()).unwrap(); + assert_eq!(e.of("other_crate::f"), EffectAnswer::CrateNotRead); + + let tmp = Tmp::new("empty"); + let empty = Effects::open(&tmp).unwrap(); + assert_eq!(empty.rows().len(), 0); + assert_eq!(empty.of(GUARD), EffectAnswer::CrateNotRead); +} + +#[test] +fn two_crates_are_read_in_name_order() { + let tmp = Tmp::new("two"); + write( + &tmp, + "b", + &format!("{HEAD}b::f\tASSERT\tUser\tsrc/lib.rs\t1\t2\n"), + ); + write( + &tmp, + "a", + &format!("{HEAD}a::g\tDEREF_WRITE\tLocal\t-\t-\t-\n"), + ); + let e = Effects::open(&tmp).unwrap(); + assert_eq!(e.crates().collect::>(), ["a", "b"]); + assert_eq!(e.rows().len(), 2); + assert_eq!(e.rows()[0].subject, "a::g"); + assert_eq!(e.rows()[0].site, None); + assert_eq!(e.rows()[0].kind, EffectKind::DerefWriteLocal); + assert_eq!(e.rows()[1].kind, EffectKind::Assert("User".into())); + assert_eq!( + e.rows()[1].site, + Some(EffectSite { + file: "src/lib.rs".into(), + start: 1, + end: 2 + }) + ); +} + +fn refusal(tag: &str, body: &str) -> EffectsRefusal { + let tmp = Tmp::new(tag); + write(&tmp, "k", body); + Effects::open(&tmp).unwrap_err() +} + +#[test] +fn a_wrong_version_is_refused() { + assert_eq!( + refusal( + "ver2", + "#version 2\n#schema subject\teffect\tkind\tfile\tstart\tend\n" + ), + EffectsRefusal::Version { + krate: "k".into(), + found: 2 + } + ); + assert_eq!( + refusal("ver0", "#schema subject\teffect\tkind\tfile\tstart\tend\n"), + EffectsRefusal::Version { + krate: "k".into(), + found: 0 + } + ); +} + +#[test] +fn a_wrong_schema_is_refused() { + assert_eq!( + refusal("schema", "#version 1\n#schema subject\teffect\tkind\n"), + EffectsRefusal::Schema { krate: "k".into() } + ); +} + +#[test] +fn a_short_or_long_row_is_refused() { + assert_eq!( + refusal( + "short", + &format!("{HEAD}k::f\tASSERT\tUser\tsrc/lib.rs\t1\n") + ), + EffectsRefusal::Arity { + krate: "k".into(), + line: 3 + } + ); + assert_eq!( + refusal( + "long", + &format!("{HEAD}k::f\tASSERT\tUser\tsrc/lib.rs\t1\t2\textra\n") + ), + EffectsRefusal::Arity { + krate: "k".into(), + line: 3 + } + ); + assert_eq!( + refusal( + "later", + &format!( + "{HEAD}k::f\tASSERT\tUser\tsrc/lib.rs\t1\t2\nk::f\tASSERT\tUser\tsrc/lib.rs\t1\n" + ) + ), + EffectsRefusal::Arity { + krate: "k".into(), + line: 4 + } + ); +} + +#[test] +fn an_unknown_effect_is_refused() { + let want = EffectsRefusal::UnknownEffect { + krate: "k".into(), + line: 3, + }; + for (tag, row) in [ + ("e1", "k::f\tPANIC\tUser\t-\t-\t-\n"), + ("e2", "k::f\tDEREF_WRITE\tGlobal\t-\t-\t-\n"), + ("e3", "k::f\tASSERT\t\t-\t-\t-\n"), + ] { + assert_eq!(refusal(tag, &format!("{HEAD}{row}")), want, "{row:?}"); + } +} + +#[test] +fn a_malformed_site_is_refused() { + let want = EffectsRefusal::Site { + krate: "k".into(), + line: 3, + }; + for (i, site) in [ + "src/lib.rs\t5\t2", + "src/lib.rs\tx\t2", + "-\t1\t2", + "src/lib.rs\t-\t-", + "-\t-\t3", + ] + .iter() + .enumerate() + { + assert_eq!( + refusal( + &format!("site{i}"), + &format!("{HEAD}k::f\tASSERT\tUser\t{site}\n") + ), + want, + "{site:?}" + ); + } + + let tmp = Tmp::new("site_ok"); + write( + &tmp, + "k", + &format!("{HEAD}k::f\tASSERT\tUser\tsrc/lib.rs\t2\t2\n"), + ); + let e = Effects::open(&tmp).unwrap(); + assert_eq!( + e.rows()[0].site, + Some(EffectSite { + file: "src/lib.rs".into(), + start: 2, + end: 2 + }) + ); +} + +#[test] +fn duplicate_rows_are_kept() { + let tmp = Tmp::new("dup"); + let row = "k::f\tASSERT\tUser\tsrc/lib.rs\t1\t2\n"; + write(&tmp, "k", &format!("{HEAD}{row}{row}")); + let e = Effects::open(&tmp).unwrap(); + assert_eq!(e.rows().len(), 2); +} + +// --- effects joined with the call rows --- + +const DIV: &str = "DivisionByZero"; + +fn div(k: &EffectKind) -> bool { + *k == EffectKind::Assert(DIV.into()) +} + +fn reach(dir: &Path, symbol: &str) -> Option> { + let g = Codegraph::open(dir).unwrap(); + let fx = Effects::open(dir).unwrap(); + let r = g.effects_reached(symbol, &fx, div).unwrap()?; + Some( + r.hits + .iter() + .map(|h| (h.effect.subject.clone(), h.grade)) + .collect(), + ) +} + +#[test] +fn a_caller_reaches_its_callees_division() { + let cook_ops = "foundry_parity_lab::arm_b::{ItemQuery<'s>}::cook_ops"; + // max_quantities calls cook_ops; the division is in cook_ops (two rows, one site). + assert_eq!( + reach( + &real(), + "foundry_parity_lab::arm_b::{ItemQuery<'s>}::max_quantities" + ), + Some(vec![ + (cook_ops.to_string(), ReachGrade::Confirmed), + (cook_ops.to_string(), ReachGrade::Confirmed) + ]) + ); + assert_eq!( + reach(&real(), cook_ops), + Some(vec![ + (cook_ops.to_string(), ReachGrade::Own), + (cook_ops.to_string(), ReachGrade::Own) + ]) + ); + // A function that reaches no division body. + assert_eq!( + reach(&real(), "foundry_parity_lab::fixture::recipe"), + Some(vec![]) + ); + // A symbol without a position has no answer, which is not an empty one. + assert_eq!(reach(&real(), "foundry_parity_lab::no_such_function"), None); +} + +#[test] +fn classes_select_division_remainder_shift_and_write() { + let fx = Effects::open(&real()).unwrap(); + let count = |c: EffectClass| { + fx.rows() + .iter() + .filter(|e| e.kind.class() == Some(c)) + .count() + }; + // Counted from the fixture: DivisionByZero 5 + Overflow:Div 5; RemainderByZero 11 + + // Overflow:Rem 3; Overflow:Shr 7 + Overflow:Shl 5; DEREF_WRITE Local 11 + Arg 3. + assert_eq!(count(EffectClass::Division), 10); + assert_eq!(count(EffectClass::Remainder), 14); + assert_eq!(count(EffectClass::Shift), 12); + assert_eq!(count(EffectClass::Write), 14); + // Bounds checks and add/sub/mul overflow have no class: 213 - 50. + assert_eq!( + fx.rows() + .iter() + .filter(|e| e.kind.class().is_none()) + .count(), + 163 + ); + // Reach by class: max_quantities reaches cook_ops's division rows of both kinds. + let g = Codegraph::open(&real()).unwrap(); + let r = g + .effects_reached( + "foundry_parity_lab::arm_b::{ItemQuery<'s>}::max_quantities", + &fx, + |k| k.class() == Some(EffectClass::Division), + ) + .unwrap() + .unwrap(); + let kinds: Vec<&str> = r.hits.iter().map(|h| h.effect.kind.columns().1).collect(); + assert!( + kinds.contains(&"DivisionByZero") && kinds.contains(&"Overflow:Div"), + "{kinds:?}" + ); + assert!(r + .hits + .iter() + .all(|h| h.effect.kind.class() == Some(EffectClass::Division))); +} + +#[test] +fn the_effects_file_is_identified_by_its_bytes() { + let fx = Effects::open(&real()).unwrap(); + let bytes = std::fs::read(real().join("foundry_parity_lab.effects.tsv")).unwrap(); + let id = fx.identity("foundry_parity_lab").unwrap(); + assert_eq!(id.rows, 213); + assert_eq!(id.fnv64, lance_graph_contract::hash::fnv1a(&bytes)); + assert_eq!(fx.identity("other_crate"), None); + // The identity is of the bytes, not of the rows. + let dir = Tmp::new("identity"); + let mut edited = bytes.clone(); + // A trailing blank line: the same rows, other bytes. + edited.extend_from_slice(b"\n"); + std::fs::write(dir.join("foundry_parity_lab.effects.tsv"), &edited).unwrap(); + let other = Effects::open(&dir).unwrap(); + let other_id = other.identity("foundry_parity_lab").unwrap(); + assert_eq!(other_id.rows, 213); + assert_ne!(other_id.fnv64, id.fnv64); +} + +/// The rev1 call rows, with one division effect placed in `subject` when given. +fn rev1_with_effect(tag: &str, subject: Option<&str>) -> Tmp { + let dir = Tmp::new(tag); + let rev1 = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/rev1"); + for e in std::fs::read_dir(&rev1).unwrap() { + let e = e.unwrap(); + std::fs::copy(e.path(), dir.join(e.file_name())).unwrap(); + } + if let Some(subject) = subject { + write( + &dir, + "cg_lib", + &format!("{HEAD}{subject}\tASSERT\t{DIV}\tsrc/util.rs\t1\t2\n"), + ); + } + dir +} + +#[test] +fn the_reach_grade_follows_the_call_rows() { + let dir = rev1_with_effect("grades", Some("cg_lib::util::double")); + let one = |symbol: &str| reach(&dir, symbol).unwrap(); + let hit = |g| vec![("cg_lib::util::double".to_string(), g)]; + assert_eq!(one("cg_lib::util::double"), hit(ReachGrade::Own)); + // run -> Square::area -> double, all resolved. + assert_eq!(one("cg_app::run"), hit(ReachGrade::Confirmed)); + // area_of reaches Square::area only through trait dispatch. + assert_eq!(one("cg_lib::area_of"), hit(ReachGrade::Possible)); + // area_dyn holds the dynamic call. + assert_eq!(one("cg_lib::area_dyn"), hit(ReachGrade::Unknown)); + // triple does not reach double at all. + assert_eq!(one("cg_lib::util::triple"), vec![]); +} + +fn limits(dir: &Path, symbol: &str) -> Vec { + let g = Codegraph::open(dir).unwrap(); + let fx = Effects::open(dir).unwrap(); + g.effects_reached(symbol, &fx, div).unwrap().unwrap().limits +} + +#[test] +fn a_missing_effects_file_makes_the_answer_partial_never_clean() { + let not_read = |k: &str| ReachLimit::EffectsNotRead { krate: k.into() }; + // No effects file at all: nothing is reached, and both crates are named as unread. + let none = rev1_with_effect("no_effects", None); + let g = Codegraph::open(&none).unwrap(); + let fx = Effects::open(&none).unwrap(); + let r = g.effects_reached("cg_app::run", &fx, div).unwrap().unwrap(); + assert!(r.hits.is_empty()); + assert!(r.partial()); + assert!(r.limits.contains(¬_read("cg_lib")) && r.limits.contains(¬_read("cg_app"))); + // cg_lib's file present: only cg_app is still unread. + let one = rev1_with_effect("one_effects", Some("cg_lib::util::double")); + let l = limits(&one, "cg_app::run"); + assert!( + !l.contains(¬_read("cg_lib")) && l.contains(¬_read("cg_app")), + "{l:?}" + ); + // Both present: no crate is unread. + write(&one, "cg_app", HEAD); + let l = limits(&one, "cg_app::run"); + assert!( + !l.iter() + .any(|x| matches!(x, ReachLimit::EffectsNotRead { .. })), + "{l:?}" + ); +} + +#[test] +fn dynamic_calls_and_an_incomplete_harvest_are_limits() { + let dir = rev1_with_effect("limits", Some("cg_lib::util::double")); + write(&dir, "cg_app", HEAD); + // rev1 holds one dynamic call, in cg_lib::area_dyn, which cg_app::run calls. Both harvests + // are clean and both effects files are read, so that is the only limit. + assert_eq!( + limits(&dir, "cg_app::run"), + vec![ReachLimit::DynamicCalls { + store_dynamic_calls: 1 + }] + ); + // triple reaches no dynamic call and nothing outside the store: no limit is known. + assert_eq!(limits(&dir, "cg_lib::util::triple"), vec![]); + // Without its residuals file, what cg_lib's harvest missed is unknown. + std::fs::remove_file(dir.join("cg_lib.residuals.tsv")).unwrap(); + assert!( + limits(&dir, "cg_app::run").contains(&ReachLimit::HarvestIncomplete { + krate: "cg_lib".into() + }) + ); +} + +#[test] +fn an_effect_subject_without_a_position_is_a_limit() { + let dir = rev1_with_effect("unplaced", Some("cg_lib::not_in_the_rows")); + let g = Codegraph::open(&dir).unwrap(); + let fx = Effects::open(&dir).unwrap(); + let r = g.effects_reached("cg_app::run", &fx, div).unwrap().unwrap(); + assert!(r.hits.is_empty()); + assert!(r.limits.contains(&ReachLimit::Unplaced { + subject: "cg_lib::not_in_the_rows".into() + })); +} + +#[test] +fn callees_outside_the_store_are_a_limit() { + let outside = |crates: &[&str]| ReachLimit::CalleesOutsideStore { + crates: crates.iter().map(|c| c.to_string()).collect(), + }; + // recipe calls into core and alloc only; their bodies are not in the store. + assert_eq!( + limits(&real(), "foundry_parity_lab::fixture::recipe"), + vec![outside(&["alloc", "core"])] + ); + // max_quantities also reaches the mask-risc executor, which was not extracted. + let l = limits( + &real(), + "foundry_parity_lab::arm_b::{ItemQuery<'s>}::max_quantities", + ); + assert!( + l.contains(&outside(&["alloc", "core", "lance_graph_mask_risc"])), + "{l:?}" + ); +} From 6f9ff5422c8791a4c2373715f179d5d888f256f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 11:27:39 +0000 Subject: [PATCH 2/3] fix(codegraph): a queried symbol outside the store is a reach limit effects_reached skipped the queried position when it looked for callees outside the store, so asking about an external callee returned no hits and no limits although its body was never read. Found by the Codex review of PR 2006. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01XAqKMPJjVvasjwqhjJ6c9p --- .../2026-11-11-codegraph-effects/REPORT.md | 4 +-- .../disable_runs.py | 1 + .codegraph/crates.tsv | 2 +- .../crates/lance-graph-codegraph.index.md | 2 +- .codegraph/freshness.tsv | 2 +- crates/lance-graph-codegraph/src/effects.rs | 12 ++++++++ crates/lance-graph-codegraph/tests/effects.rs | 29 +++++++++++++++++++ 7 files changed, 47 insertions(+), 5 deletions(-) diff --git a/.claude/probes/2026-11-11-codegraph-effects/REPORT.md b/.claude/probes/2026-11-11-codegraph-effects/REPORT.md index 56f1b42ca..a635cb45d 100644 --- a/.claude/probes/2026-11-11-codegraph-effects/REPORT.md +++ b/.claude/probes/2026-11-11-codegraph-effects/REPORT.md @@ -69,8 +69,8 @@ reached 0 UNKNOWN (partial: see LIMIT lines) - `tests/effects.rs`: 20 tests. Real fixture: `tests/data/effects/` (213 effect rows and the matching call rows of `foundry_parity_lab`). The four reach grades use the `rev1` call rows with one synthetic effect row. -- `python3 .claude/probes/2026-11-11-codegraph-effects/disable_runs.py`: 29 guards, each patched - out through an asserted anchor. Receipt beside it: 29 red, 0 not red. +- `python3 .claude/probes/2026-11-11-codegraph-effects/disable_runs.py`: 30 guards, each patched + out through an asserted anchor. Receipt beside it: 30 red, 0 not red. ## Not done diff --git a/.claude/probes/2026-11-11-codegraph-effects/disable_runs.py b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.py index 605639540..277b65818 100644 --- a/.claude/probes/2026-11-11-codegraph-effects/disable_runs.py +++ b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.py @@ -40,6 +40,7 @@ ("reach-no-position", " let Some(me) = self.pos(symbol) else {\n return Ok(None);\n };", " let me = self.pos(symbol).unwrap_or(0);"), ("limit-effects-not-read", " if effects.identity(&krate).is_none() {", " if false {"), ("limit-harvest", " if status == RunStatus::Partial || !clean_harvest {", " if status == RunStatus::Partial {"), + ("limit-queried-symbol-outside", " if !me_stored {\n", " if false {\n"), ("limit-outside", " if !outside.is_empty() {", " if false {"), ("limit-dynamic", " if reaches_dynamic {", " if false {"), ("limit-dynamic-per-symbol", " reaches_dynamic = true;", " reaches_dynamic = true;\n }\n if true {\n reaches_dynamic = true;"), diff --git a/.codegraph/crates.tsv b/.codegraph/crates.tsv index 525880b4f..be245e50b 100644 --- a/.codegraph/crates.tsv +++ b/.codegraph/crates.tsv @@ -36,7 +36,7 @@ lance-graph-benches crates/lance-graph-benches member 2f2fb296a08260fb lance-graph-callcenter crates/lance-graph-callcenter member 41a65d38ac70bc87 lance-graph-catalog crates/lance-graph-catalog member 9d0c34f70c28a465 lance-graph-codec-research crates/lance-graph-codec-research excluded 8b8afbd9a6d0f359 -lance-graph-codegraph crates/lance-graph-codegraph excluded c32567bf367aa7bc +lance-graph-codegraph crates/lance-graph-codegraph excluded 8314ca9d65242c9c lance-graph-cognitive crates/lance-graph-cognitive excluded 93f956ac0d69ca66 lance-graph-consumer-conformance crates/lance-graph-consumer-conformance member 82e980465d770990 lance-graph-contract crates/lance-graph-contract member 6b4e13ff1d0afadb diff --git a/.codegraph/crates/lance-graph-codegraph.index.md b/.codegraph/crates/lance-graph-codegraph.index.md index 7bf5f4183..eb71e35e0 100644 --- a/.codegraph/crates/lance-graph-codegraph.index.md +++ b/.codegraph/crates/lance-graph-codegraph.index.md @@ -3,7 +3,7 @@ crate: lance-graph-codegraph path: crates/lance-graph-codegraph workspace: excluded -digest: c32567bf367aa7bc +digest: 8314ca9d65242c9c evidence legend: ../index.md#evidence-legend Generated; the code is the truth. See ../index.md. diff --git a/.codegraph/freshness.tsv b/.codegraph/freshness.tsv index f4a2dd01f..5e40fec3a 100644 --- a/.codegraph/freshness.tsv +++ b/.codegraph/freshness.tsv @@ -36,7 +36,7 @@ lance-graph-benches 2f2fb296a08260fb Cargo.toml+src (1 files) lance-graph-callcenter 41a65d38ac70bc87 Cargo.toml+src (36 files) lance-graph-catalog 9d0c34f70c28a465 Cargo.toml+src (10 files) lance-graph-codec-research 8b8afbd9a6d0f359 Cargo.toml+src (11 files) -lance-graph-codegraph c32567bf367aa7bc Cargo.toml+src (11 files) +lance-graph-codegraph 8314ca9d65242c9c Cargo.toml+src (11 files) lance-graph-cognitive 93f956ac0d69ca66 Cargo.toml+src (61 files) lance-graph-consumer-conformance 82e980465d770990 Cargo.toml+src (3 files) lance-graph-contract 6b4e13ff1d0afadb Cargo.toml+src (176 files) diff --git a/crates/lance-graph-codegraph/src/effects.rs b/crates/lance-graph-codegraph/src/effects.rs index 11c58feff..c8eb50982 100644 --- a/crates/lance-graph-codegraph/src/effects.rs +++ b/crates/lance-graph-codegraph/src/effects.rs @@ -410,6 +410,18 @@ impl crate::Codegraph { let stored_owner = self.stored_owner(); let mut outside = BTreeSet::new(); let mut reaches_dynamic = false; + let me_stored = stored_owner + .get(me as usize / 64) + .is_some_and(|w| w >> (me % 64) & 1 == 1); + // The queried symbol itself may be a callee from a crate that was never extracted: its + // body, and so its effects, were not read. + if !me_stored { + if symbol.starts_with('?') { + reaches_dynamic = true; + } else { + outside.insert(symbol.split("::").next().unwrap_or(symbol).to_owned()); + } + } for p in 0..(self.words() as u32 * 64) { let stored = stored_owner .get(p as usize / 64) diff --git a/crates/lance-graph-codegraph/tests/effects.rs b/crates/lance-graph-codegraph/tests/effects.rs index 81191ea9e..424577cb7 100644 --- a/crates/lance-graph-codegraph/tests/effects.rs +++ b/crates/lance-graph-codegraph/tests/effects.rs @@ -543,3 +543,32 @@ fn callees_outside_the_store_are_a_limit() { "{l:?}" ); } + +/// PR 2006 review: the queried symbol is itself a callee from a crate that was not extracted. +/// Its body was never read, so an empty answer must carry the limit. +#[test] +fn a_queried_symbol_outside_the_store_is_a_limit() { + let g = Codegraph::open(&real()).unwrap(); + let fx = Effects::open(&real()).unwrap(); + let external: Vec = (0..g.words() as u32 * 64) + .filter_map(|p| g.symbol(p)) + .filter(|n| n.starts_with("core::") || n.starts_with("alloc::")) + .map(str::to_owned) + .collect(); + assert!(external.len() > 10, "fixture has no external callees"); + for name in &external { + let r = g.effects_reached(name, &fx, |_| true).unwrap().unwrap(); + let krate = name.split("::").next().unwrap().to_string(); + assert!( + r.limits.iter().any(|l| matches!( + l, + ReachLimit::CalleesOutsideStore { crates } if crates.contains(&krate) + )), + "{name}: {:?}", + r.limits + ); + assert!(r.partial()); + } + // The silence twin is `cg_lib::util::triple` in the limits test above: a stored function + // that reaches nothing outside keeps an empty limit list. +} From 6dda9923b2154369d27e29558747c76d23fab2b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 11:32:45 +0000 Subject: [PATCH 3/3] probe(effects): disable receipt with the queried-symbol guard (30 guards red) Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_01XAqKMPJjVvasjwqhjJ6c9p --- .../2026-11-11-codegraph-effects/disable_runs.receipt.tsv | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.claude/probes/2026-11-11-codegraph-effects/disable_runs.receipt.tsv b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.receipt.tsv index 050ab72a4..4c75aa85d 100644 --- a/.claude/probes/2026-11-11-codegraph-effects/disable_runs.receipt.tsv +++ b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.receipt.tsv @@ -23,8 +23,9 @@ reach-keep RED a_caller_reaches_its_callees_division,classes_select_division_rem reach-no-position RED a_caller_reaches_its_callees_division limit-effects-not-read RED a_missing_effects_file_makes_the_answer_partial_never_clean limit-harvest RED dynamic_calls_and_an_incomplete_harvest_are_limits -limit-outside RED callees_outside_the_store_are_a_limit +limit-queried-symbol-outside RED a_queried_symbol_outside_the_store_is_a_limit +limit-outside RED a_queried_symbol_outside_the_store_is_a_limit,callees_outside_the_store_are_a_limit limit-dynamic RED dynamic_calls_and_an_incomplete_harvest_are_limits limit-dynamic-per-symbol RED callees_outside_the_store_are_a_limit limit-unplaced RED an_effect_subject_without_a_position_is_a_limit -guards 29 not-red 0 +guards 30 not-red 0