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..a635cb45d --- /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`: 30 guards, each patched + out through an asserted anchor. Receipt beside it: 30 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..277b65818 --- /dev/null +++ b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.py @@ -0,0 +1,84 @@ +#!/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-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;"), + ("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..4c75aa85d --- /dev/null +++ b/.claude/probes/2026-11-11-codegraph-effects/disable_runs.receipt.tsv @@ -0,0 +1,31 @@ +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-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 30 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 3d9b0d427..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 434f8b1c7314d3b0 +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 10c90acb4..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: 434f8b1c7314d3b0 +digest: 8314ca9d65242c9c 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:234 CONFIRMED -- fn ImpactReport::closed_in_scope [pub] — crates/lance-graph-codegraph/src/impact_report.rs:249 CONFIRMED -- fn Codegraph::impact_report [pub] — crates/lance-graph-codegraph/src/impact_report.rs:429 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 7052a8aa9..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 434f8b1c7314d3b0 Cargo.toml+src (10 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/.codegraph/imports.tsv b/.codegraph/imports.tsv index 809ca5b47..2a7e543d4 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 @@ -4831,12 +4836,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 bd242fe13..eaea07cea 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..c8eb50982 --- /dev/null +++ b/crates/lance-graph-codegraph/src/effects.rs @@ -0,0 +1,493 @@ +//! 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; + 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) + .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 000000000..44f3f466c Binary files /dev/null and b/crates/lance-graph-codegraph/tests/data/effects/foundry_parity_lab.rows.bin differ diff --git a/crates/lance-graph-codegraph/tests/effects.rs b/crates/lance-graph-codegraph/tests/effects.rs new file mode 100644 index 000000000..424577cb7 --- /dev/null +++ b/crates/lance-graph-codegraph/tests/effects.rs @@ -0,0 +1,574 @@ +//! `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:?}" + ); +} + +/// 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. +}