diff --git a/.claude/board/entries/2026-11-11-cg-beh-1.md b/.claude/board/entries/2026-11-11-cg-beh-1.md new file mode 100644 index 000000000..2f92943e8 --- /dev/null +++ b/.claude/board/entries/2026-11-11-cg-beh-1.md @@ -0,0 +1,23 @@ +# 2026-11-11 — CG-BEH-1: a changed dispatch arm traced to the registered methods that use its word + +**Status:** MEASURED (read-only probe). `.claude/probes/2026-11-11-cg-beh-1/`. Plan: +`.claude/plans/codegraph-behaviour-v1.md`. Owner of the area: EXO-155. Board: `P-EXO-18`, EXO-232. + +The polyfill lens `0xFFFF_F000` has 17 words, bound by position in `POLYFILL` +(`lance-graph-r2il/src/polyfill.rs`). One function, `FoldDialect::call`, dispatches all 17 opcodes +in one `match`. The ordered uses of a method are readable from a bind store through `method_ref` +and `admit`, with no preparation or execution (two r2il integration tests). + +**Finding:** the trace code → opcode → word → method works for one lens and two existing test +bindings, at arm granularity: a change at `lib.rs:980` reaches word 5 (`IntSLess`) and both methods +that use it. The first two hops are source structure; the third is DT64 registration. With the +dispatch mapping of one opcode removed, its word is UNKNOWN and the other rows are unchanged; a +witness taken on another revision of the source gives no answer. + +**Limits:** `realizes` is an arm reading, not a compiler fact and not a behavioural witness. At +symbol level one function realizes every word. The native lowering path is not mapped. No +production crate registers a binding with DO effects; Foundry's `can_cook_method` is ON + WHEN only. + +OPEN: PR #2001 and PR #2002 both add `Codegraph::impact_report` with different types, and the name +`UnresolvedDispatch` means trait dispatch in one and dynamic calls in the other. The plan (§4) +proposes one shape; neither PR was changed. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 6a6e823bb..f3f0fb669 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,11 +25,12 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -597 entries, 2026-08-06 .. 2026-11-11. +598 entries, 2026-08-06 .. 2026-11-11. | date | entry id | finding | file | |---|---|---|---| | 2026-11-11 | `codegraph-recon-1` | | [2026-11-11-codegraph-recon-1.md](2026-11-11-codegraph-recon-1.md) | +| 2026-11-11 | `cg-beh-1` | | [2026-11-11-cg-beh-1.md](2026-11-11-cg-beh-1.md) | | 2026-11-10 | `lab-resolve-borrow-all` | | [2026-11-10-lab-resolve-borrow-all.md](2026-11-10-lab-resolve-borrow-all.md) | | 2026-11-10 | `int8-direct-packed-b` | | [2026-11-10-int8-direct-packed-b.md](2026-11-10-int8-direct-packed-b.md) | | 2026-11-10 | `D-FOUNDRY-PARITY-1` | | [2026-11-10-foundry-parity-1-lab.md](2026-11-10-foundry-parity-1-lab.md) | diff --git a/.claude/plans/codegraph-behaviour-v1.md b/.claude/plans/codegraph-behaviour-v1.md new file mode 100644 index 000000000..a80a8eb09 --- /dev/null +++ b/.claude/plans/codegraph-behaviour-v1.md @@ -0,0 +1,227 @@ +# D-CODEGRAPH-BEHAVIOR-1: from changed code to registered behaviour (plan v1) + +> **2026-11-11 · PLAN.** Ruling: D-CODEGRAPH-BEHAVIOR-RULING-1 (operator, 2026-11-11). +> Coordination: Linear project **Codegraph · Collaboration Board** (`P-EXO-18`), channel EXO-232. +> GitHub is canonical. This plan adds no slice to that board and no owner: it maps proposed stages +> onto existing owners and defines one read-only experiment, CG-BEH-1. +> It extends `codegraph-expansion-v1.md` (PR #2001, slices CG-1 to CG-8) and does not replace it. + +## 1. Question + +A function changes. Which **registered behaviours** may change with it, on what evidence, and +what is not known? + +Today the Codegraph answers "which functions call it". DT64 knows the lenses, their words and the +methods that use them. Nothing joins the two. + +## 2. Ownership (ruled; no competing owner) + +| concern | owner | issue | +|---|---|---| +| Codegraph → behaviour-lens declarations → `MethodBinding` | D-CGMM-0 | EXO-155 | +| `ClassView` contract, including value-schema presets | D-QR-CAP | EXO-121 | +| effects reader (`effects.tsv`), target kind, cfg rows | CG-8 | EXO-241 | +| extraction targets and dependents | CG-2 | EXO-234 | +| store provenance format | CG-3 | EXO-235 | +| body semantics through egglog (bounded, read-only, go-ahead given) | CG-5 | EXO-237 | +| review report follow-ups; crai-rs owns review semantics | CG-6 | EXO-238 | +| report-rs #33 transfer | CG-7 | EXO-239, EXO-185 | +| Rails / Odoo / SAP business parity | D-XGP | EXO-95 | + +## 3. Crosswalk of the nine proposed stages + +The stages N-1 to N-9 were proposed on 2026-11-11 and are **not** filed as issues. +Each is a stage of an existing owner. + +| stage | what | lands under | note | +|---|---|---|---| +| N-1 | SPO triples as graded store rows | EXO-155, stage CGMM-1 (source facts) | name-matched rows are `SOURCE_STRUCTURE + POSSIBLE` | +| N-2 | Python import rows from `ruff_graph` | EXO-155, CGMM-1 | Odoo only; file-level | +| N-3 | corpus conventions and residual rows (ore / config / slag) | EXO-155, CGMM-1 and CGMM-4 | §6 below | +| N-4 | more frontends (plain C, PHP, Go, TypeScript) | parked | after N-1 is proven on one corpus | +| N-5 | code → algebra word | **CG-BEH-1**, under EXO-155 (CGMM-1 / CGMM-8) | §5 below | +| N-6 | method ON × WHEN × DO rows from registered bindings | **CG-BEH-1**, under EXO-155 (CGMM-6 reads) | DO only in v0; see §5.4 | +| N-7 | impact through behaviour | **CG-BEH-1**; consumer: CG-6 review report | | +| N-8 | harvested methods into candidate ON / WHEN / DO | EXO-155, CGMM-3 to CGMM-5; EXO-95 | §7 below; precedent EXO-210 | +| N-9 | value-schema preset per harvested class | EXO-121 | a representation choice, never a behaviour claim | + +A new Linear issue for CG-BEH-1 is proposed only if EXO-155's stages cannot carry its gate +(§5.6) as an independently reviewed deliverable. + +## 4. Wave 0: one coverage-aware impact API (PR #2001 and PR #2002) + +Both PRs are open against the same base and both add `Codegraph::impact_report` in +`crates/lance-graph-codegraph/src/impact_report.rs`, with different types. Neither may be merged +over the other. This section is a proposal for the two owning sessions; nothing here was pushed +to either branch. + +### 4.1 What each has that the other lacks + +| | PR #2001 (`26bf53e`) | PR #2002 (`e436b0c`) | +|---|---|---| +| input | `ExtractionScope<'a> { targets: Targets, dependents: Dependents<'a>, sources: Option<(&Path, &Path)> }` | `Scope { store, source_root, targets: Vec<(String, Targets)>, consumers_closed: bool, excluded: Vec<(String, Vec)> }` | +| targets | one value for the whole store: `LibraryOnly` / `LibraryAndTests` | per crate: `{ lib, tests, examples, benches }` | +| consumers | `Dependents::NotDeclared` / `Declared(&[&str])`; a declared crate missing from the store is named (`ConsumersNotInStore`) | `consumers_closed: bool`; when closed, every stored crate's coverage is checked | +| result | `ImpactAnswer::Found(ImpactReport)` / `SymbolNotInStore { symbol, observed }` | `ImpactReport { graded, symbol: SymbolState, gaps }`, so a missing symbol still carries its crate's gaps | +| only here | trait-dispatch gap (`possible`) separate from dynamic calls; `MissingSource` separate from `StaleSource`; `InvalidFileList`; a crate without a CRATE row; a symbol whose rows a refresh removed; `observed` crate list with run status | `SourceFilesUnread`; `RegionsExcluded`; `TargetsUndeclared`; a private symbol behind a public caller; a missing symbol's target and harvest gaps; `owner_stored` | +| tests | 12 | 14 | +| consumers of the output | `crai_report.py`, `build_q8_evidence.py`, `cg_query impact-report` | the report-rs #33 probe, `cg_query report` | + +**One name, two meanings.** `UnresolvedDispatch` means trait dispatch the compiler did not resolve +(`possible`) in #2001, and dynamic calls (`UNRESOLVED_CALL`) in #2002. #2001 calls the second +`DynamicCalls`. A merged enum must not keep the shared name for either. + +### 4.2 Proposed single shape + +- **Result:** #2002's struct (`graded`, `symbol`, `gaps`), plus #2001's `observed: Vec<(String, RunStatus)>`. + A missing symbol then carries gaps, and the caller still sees which crates were read. +- **Targets:** #2002's per-crate struct. `LibraryOnly` is `Targets::LIB_ONLY`. +- **Consumers:** `Consumers::NotDeclared | Declared(Vec)`. Declared and all present in the + store is #2002's "closed" case and runs its per-crate check. A declared crate that is absent is + #2001's `ConsumersNotInStore`, by name. +- **Excluded regions and unread files:** from #2002, unchanged. +- **Gaps:** the union, with the clash renamed: `TraitDispatchUnresolved { possible }` and + `DynamicCalls { unknown }`. `StaleSource` and `MissingSource` stay separate. The harvest gap keeps + #2002's one variant with its three causes, or #2001's three variants; pick one and map the other's tests. +- **Claims:** `no_known_gap()` (no gap) and `closed_in_scope()` (found and no gap) both stay, both + documented as local to the declared scope. +- **`cg_query`:** one subcommand, `impact-report`, with the union of flags + (`--deps`, `--targets CRATE=…`, `--exclude CRATE=REGION`, `--no-freshness`). + +### 4.3 Gate for Wave 0 + +- Every one of the 26 existing tests passes against the one API, or is merged with its twin and the + merge is listed. The seven RECON-2 cases exist in both suites. +- Each guard still has a disable run that goes red, including #2002's two directions for unread files. +- `crai_report.py` and the report-rs #33 probe both run on the unified output and regenerate their + committed examples byte-identical, or the diff is explained line by line. +- Procedure: a third branch from `main`. Neither PR is rebased onto the other first. + +## 5. CG-BEH-1: symbol → algebra word → ordered method uses → classid + +Read-only. No new word, no new classid, no new binding, no R2IL preparation, no execution. + +### 5.1 What exists (read 2026-11-11 at `317c4ffc0`) + +- **The lens.** `POLYFILL_LENS = 0xFFFF_F000` (`lance-graph-r2il/src/polyfill.rs`). Its words are + `POLYFILL: [FrozenWord; 17]`; word `p` is the entry at index `p`. Each entry binds an R2IL opcode, + an arity and the aliases a view may use (R2IL, SQL and Quack names). The opcode, arity and R2IL + name are checked against OGAR's vocabulary by test. +- **The dispatch.** One function, `FoldDialect::call` (`lance-graph-r2il/src/lib.rs`), matches on + the opcode. `binding::EXECUTABLE` lists the same 17 opcodes in the same order. +- **A method's uses.** `MethodBinding::effects` is an ordered list; each step names one + `AlgebraRef { lens, word }`, a target field and ordered arguments. `BindStore::admit` returns the + stored binding; `words_used` and `users_of` index the same facts. +- **What does not exist.** No production crate registers a binding with DO effects. Foundry's + `can_cook_method` has `effects: Vec::new()`: it is ON + WHEN only. The bindings with ordered + effects are in the r2il and dt64 integration tests. The Rails scope bindings of EXO-210 live in + another repository. + +### 5.2 Two relations, three kinds of evidence + +| relation | read from | evidence | +|---|---|---| +| `symbol → realizes → opcode` | the match arms of `FoldDialect::call` | source structure. Not a compiler call fact, and not a behavioural test | +| `opcode → word` | the `POLYFILL` table, by position | source structure, cross-checked against `EXECUTABLE` | +| `method → uses[ordinal] → word` | `BindStore::admit` in a test run | DT64 registration | + +The report prints the line where source evidence ends and registration evidence begins. + +**Why `realizes` is graded this way.** A match arm shows that the function contains the code that +runs for that opcode. It does not show that the code computes what the word declares. That needs a +behavioural witness per word. The r2il integration tests compare whole method answers with a +scalar oracle; a mapping from those tests to single words is not built here. A function whose name +matches a word is never an edge. + +**Granularity.** One symbol dispatches every opcode, so at symbol level a change to +`FoldDialect::call` is a candidate change to all 17 words. Only the arm's line span narrows it. +A compiler call graph cannot narrow it at all: Charon sees one function. + +### 5.3 The fixture + +- `crates/lance-graph-r2il/tests/integration/polyfill.rs`: two tests that read the ordered uses of + two existing test bindings through `method_ref` and `admit`, cross-check `words_used` and + `users_of`, and export the rows. +- `.claude/probes/2026-11-11-cg-beh-1/beh.py`: `words`, `dispatch` and `impact` over the source + tree and the exported rows; `--self-test` and `disable_runs.py` for the negative controls. + +### 5.4 What v0 leaves out, on purpose + +- **ON and WHEN rows.** The binding carries `on` and the guard witness; v0 reads DO only, because + the algebra join needs only DO. ON and WHEN stay separate relations when they are added. +- **The native path.** `binding_native::prepare_native` lowers steps to mask-risc programs. That + second realization is not mapped; the report lists it as a residual. +- **mask-risc and `ndarray::simd`.** The words' implementations below the dialect are not followed. +- **Compiler facts.** No Charon store of `lance-graph-r2il` is available in this environment, and + #2001 records that crate's extraction as PARTIAL. The arm reading is source structure. + +### 5.5 Capacity + +`AlgebraRef.word` is a `u8`: a lens holds at most 256 words. The probe refuses a table larger than +that. This is independent of how many positions the Codegraph can address. + +### 5.6 Gate + +- For one existing binding, the query "what may change if this arm of `FoldDialect::call` changes" + returns its using methods and their classid, each row with its evidence. +- **Missing dispatch:** with one opcode's dispatch mapping removed, the words that depend on it are + UNKNOWN, and every other mapping's rows are unchanged. +- **Stale witness:** a dispatch witness taken on another revision of the source gives no current answer. +- A name match produces no edge. A word with no user is "no registered user in this store", never + "unaffected". A symbol that dispatches nothing has "no behaviour edge", never "no impact". +- Each guard has a disable run that goes red. + +## 6. Ore, config, slag (the Ruff track) + +Three kinds of rows, never merged: + +| kind | holds | may contain | +|---|---|---| +| ore | declarations, operations and ordered events observed in source | only what a harvester emitted, with its grade | +| config | corpus conventions and interpretation rules | explicit rows; each derivation names the rows it used | +| slag | residuals and unmet admission obligations | the obligation and the config row or witness that is missing | + +Removing one config row must invalidate exactly the derivations that name it. No source edge is +invented to fill a gap. + +## 7. ON, WHEN and DO stay separate for legacy code + +| source construct | candidate target | admitted only when | +|---|---|---| +| field-change trigger (`@api.depends`) | DT64 ON | the field basis is attested by the source | +| lifecycle callback (`before_save`, `after_commit`) | an event occurrence | not representable as an ON mask by convention | +| predicate or guard | DT64 WHEN | it lowers to a supported, equivalent `SetExpr` | +| ordered pure operation | DT64 DO | every step is an admitted word with admitted arguments | +| database writes, transactions, callbacks | effect residual or host boundary | not admitted to R2IL in general | + +EXO-210 is the precedent: Rails scopes were admitted for a narrow, differentially tested subset; +NULL-able comparisons were refused with a counterexample; every callback stayed outside. + +A value-schema preset (`ValueSchema::Bootstrap` and the others) is a representation choice. It +establishes nothing about inheritance, callback order or permissions. + +## 8. Dependency order + +``` +0 reconcile PR #2001 + PR #2002 (§4) two owning sessions +1 CG-2 extraction driver, CG-8 effects reader, EXO-234, EXO-241, EXO-235 + CG-3 provenance decision +2 code → algebra word CG-BEH-1 (this plan), then on compiler facts after 1 +3 algebra → method ON × WHEN × DO EXO-155 +4 behaviour impact, consumed by the report EXO-155, CG-6 +5 Ruff ore / config / slag, candidate EXO-155, EXO-95 (runs beside 2 to 4) + ON / WHEN / DO +6 cold admission and the crai-rs report EXO-155, EXO-238 (where 4 and 5 meet) +E egglog probe, read-only EXO-237 (after the effects reader) +``` + +CG-BEH-1 v0 does not wait for step 0 or step 1: it touches neither `impact_report` nor the store. +Its second version, on compiler facts, does. + +## 9. Constraints + +- No new F### word, classid, DT64 binding or execution path. No production admission. +- No second graph, parser or review reporter. No blocking CI check. +- A name-derived edge is POSSIBLE at most, and in §5 it is no edge at all. +- egglog stays a cold, read-only probe (EXO-237). DialEgg and MLIR are comparative reading only. +- The diagrams in `.mermaid/03`, `/05` and `/06` are compared against, never used as an oracle. diff --git a/.claude/probes/2026-11-11-cg-beh-1/REPORT.md b/.claude/probes/2026-11-11-cg-beh-1/REPORT.md new file mode 100644 index 000000000..8c2b2c3d7 --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/REPORT.md @@ -0,0 +1,94 @@ +# CG-BEH-1: from a changed dispatch arm to the registered methods that use its word + +**Status:** MEASURED (read-only probe), 2026-11-11, at `317c4ffc0`. +Plan: `.claude/plans/codegraph-behaviour-v1.md`. Owner of the area: EXO-155 (D-CGMM-0). +Board: `P-EXO-18`, channel EXO-232. + +No word, classid or binding is added. Nothing is prepared or executed. + +## Question + +Can a change in Rust code be traced to the registered methods that depend on it, with each hop +showing what kind of evidence it rests on? + +## Answer + +Yes for one lens and two existing test bindings, at the granularity of a `match` arm. +The trace has three hops and two kinds of evidence: + +| hop | read from | evidence | +|---|---|---| +| code → opcode | the `match f` arms of `FoldDialect::call` (`lance-graph-r2il/src/lib.rs`) | source structure | +| opcode → word | `POLYFILL[p]` (`lance-graph-r2il/src/polyfill.rs`), cross-checked against `binding::EXECUTABLE` | source structure | +| word → method, in order | `BindStore::method_ref` + `admit`, in a test run | DT64 registration | + +Source evidence ends at the `POLYFILL` table. Everything after it is what a bind store holds. + +## Measured + +- **Lens:** `0xFFFF_F000`, 17 words. All 17 opcodes have a dispatch arm. + `FoldDialect::call` has 25 opcode arms and one catch-all; 8 arm opcodes are not words of the lens + (`NUMBER`, `LOAD`, `VIA`, `INT_LESS`, `INT_LESS_EQUAL`, `GROUP_SUM`, `RANGE`, `KEEP`). +- **Uses** (`uses.tsv`, exported by the r2il integration test): two methods of class `0x7F0B_0001`. + Method 1 uses word 5 (`IntSLess`) then word 11 (`SUM`). Method 2 uses word 5 then word 10 (`PopCount`). +- **Arm query** (`examples/impact-arm-int_s_less.tsv`): a change at `lib.rs:980` is inside the + `INT_S_LESS` arm (lines 976 to 1018), word 5, used at ordinal 0 by methods 1 and 2. +- **Symbol query** (`examples/impact-symbol.tsv`): at symbol level the same function realizes all + 17 words. 3 are used in this store; 14 print "no registered user in this uses file", never "unaffected". + +## Negative controls + +| control | result | file | +|---|---|---| +| dispatch mapping of `INT_S_LESS` removed | word 5 is `UNKNOWN`; its two `uses` rows are withdrawn; every other row is unchanged | `examples/impact-missing-dispatch.tsv` | +| source changed after the witness was taken (one line appended to a copy of `lib.rs`) | `STALE`, exit 4, no impact row | `examples/impact-stale-witness.tsv` | +| the witness matches the current source | exit 0 | | +| a symbol that dispatches nothing (`polyfill::position_of`) | `behaviour NONE … (not: no impact)` | | +| uses file missing | `ARTIFACT_MISSING`, exit 3 | | + +`beh.py --self-test` has 13 tests on synthetic trees. `disable_runs.py` removes one guard at a time +from a copy of `beh.py`; all 12 cases go red. The Rust test's ordering assertion goes red when the +effects are read in reverse. + +## What this does not show + +- **`realizes` is not a behavioural witness.** An arm contains the code that runs for an opcode. It + does not show that the code computes what the word declares. No per-word behavioural test is mapped. +- **It is not a compiler fact.** The arms are parsed from source. A compiler call graph sees one + function for all 17 words and cannot narrow further. +- **The native path is not mapped.** `binding_native::prepare_native` lowers steps to mask-risc + programs; that realization and everything below it (mask-risc, `ndarray::simd`) is a residual. +- **The uses cover one test store.** No production crate registers a binding with DO effects. + Foundry's `can_cook_method` has no effects (`effects: Vec::new()`): it is ON + WHEN only. +- **ON and WHEN are not read.** Only DO is joined. +- **Parser limits.** `beh.py` finds the first `match f {` after `pub fn call(` in `impl FoldDialect`, + and assumes block arms. Raw strings are not handled. + +## Compared with `.mermaid/05-methods.md` (not used as an oracle) + +- Agrees: DT64 stores the binding and executes nothing; the DO path is `binding::prepare` → + `prepare_native` → `run_native`; `admit` is not a permit. +- The diagram does not show the word table or the dispatch arms; this probe adds that layer. +- One stale statement in the diagram: it places the G6 method lab on an open PR. + `crates/foundry-parity-lab/src/method.rs` is on `main` (commit `fcee68c90`). +- `.mermaid/03` and `/06` were not read for this slice. + +## Reproduce + +```sh +cd crates/lance-graph-r2il +CG_BEH_USES_OUT=$PWD/../../.claude/probes/2026-11-11-cg-beh-1/uses.tsv \ + cargo test -p lance-graph-r2il --test integration cg_beh +cd ../../.claude/probes/2026-11-11-cg-beh-1 +python3 -I beh.py words --src ../../.. > words.tsv +python3 -I beh.py dispatch --src ../../.. > dispatch.tsv +python3 -I beh.py impact --src ../../.. --uses uses.tsv \ + --symbol lance_graph_r2il::FoldDialect::call --line 980 +python3 -I beh.py --self-test && python3 -I disable_runs.py +``` + +## Next smallest step + +Replace the source-structure reading of the arms with compiler facts once the extraction driver +(CG-2, EXO-234) and the effects reader (CG-8, EXO-241) exist, and add one behavioural witness per +word so `realizes` can be graded above source structure. diff --git a/.claude/probes/2026-11-11-cg-beh-1/beh.py b/.claude/probes/2026-11-11-cg-beh-1/beh.py new file mode 100644 index 000000000..9dd8e63f7 --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/beh.py @@ -0,0 +1,682 @@ +#!/usr/bin/env python3 +"""CG-BEH-1: trace code symbol -> registered algebra word -> ordered method uses -> owning classid, +and state for every hop what kind of evidence it rests on. + +The probe adds no word, classid or binding and executes no method. It reads source text and a +uses file; neither is written. Commands: + + words --src SRC the POLYFILL table, one row per position + dispatch --src SRC the FoldDialect::call match arms per opcode + impact --src SRC --uses USES.tsv --symbol SYM [--line N] [--witness-fnv HEX] [--drop-dispatch OP] + --self-test the CG-BEH-1 negatives + +Evidence vocabulary: SOURCE_STRUCTURE (read from source text: the POLYFILL array position, a +match arm of `FoldDialect::call`) and REGISTRATION (a row of the uses file, read from a BindStore +through admit() in a test run). Source evidence ends at the POLYFILL table; every `uses` row is +registration evidence. A symbol that dispatches no opcode has no behaviour edge (NONE, not "no +impact"); a word with no user in the uses file is store-scoped, not "unaffected"; a used word +whose opcode has no dispatch arm is UNKNOWN. + +A missing or invalid artifact is an error (ARTIFACT_MISSING / ARTIFACT_INVALID, exit 3), never an +empty answer. A dispatch witness fingerprint that differs from the current lib.rs gives no answer +(STALE, exit 4). +""" + +import argparse +import re +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +POLY_PATH = "crates/lance-graph-r2il/src/polyfill.rs" +BIND_PATH = "crates/lance-graph-r2il/src/binding.rs" +LIB_PATH = "crates/lance-graph-r2il/src/lib.rs" +LENS = 0xFFFFF000 +LENS_TEXT = "0x%08X" % LENS +DISPATCH_SYMBOL = "lance_graph_r2il::FoldDialect::call" +MAX_WORDS = 256 # AlgebraRef.word is a u8 +GRADE = "SOURCE_STRUCTURE" +KEYWORDS = {"if", "while", "match", "for", "return", "let", "else", "loop", "in", "as", "fn", + "move", "ref", "mut", "unsafe", "where", "impl", "dyn"} +RESIDUALS = ["native lowering path (binding_native::prepare_native) not mapped", + "uses file covers one test store only", + "realizes is arm-level source structure, not a behavioural witness"] +USES_COLS = ["class", "method", "sig_rev", "ordinal", "lens", "word", "opcode", "r2il_name", + "arity", "target", "args"] +NONE_ROW = "behaviour\tNONE\tno behaviour edge for this symbol (not: no impact)" + + +class ArtifactError(Exception): + def __init__(self, kind, msg): + self.kind = kind + super().__init__(f"{kind}\t{msg}") + + +def fnv64(data): + h = 0xCBF29CE484222325 + for b in data: + h ^= b + h = (h * 0x100000001B3) & 0xFFFFFFFFFFFFFFFF + return f"{h:016x}" + + +def invalid(msg): + return ArtifactError("ARTIFACT_INVALID", msg) + + +def read_bytes(path): + p = Path(path) + if not p.is_file(): + raise ArtifactError("ARTIFACT_MISSING", str(p)) + return p.read_bytes() + + +def scan(text, keep_strings): + """Rust source with comments (and, unless keep_strings, string and char contents) replaced by + spaces. Length and newlines are preserved, so offsets and line numbers still apply.""" + out = list(text) + n, i = len(text), 0 + char_re = re.compile(r"'(?:\\[^']*|[^\\'])'") + while i < n: + c = text[i] + if text.startswith("//", i): + j = text.find("\n", i) + j = n if j < 0 else j + for k in range(i, j): + out[k] = " " + i = j + elif text.startswith("/*", i): + depth, j = 1, i + 2 + while j < n and depth: + if text.startswith("/*", j): + depth, j = depth + 1, j + 2 + elif text.startswith("*/", j): + depth, j = depth - 1, j + 2 + else: + j += 1 + for k in range(i, j): + if text[k] != "\n": + out[k] = " " + i = j + elif c == '"': + j = i + 1 + while j < n and text[j] != '"': + j += 2 if text[j] == "\\" else 1 + if not keep_strings: + for k in range(i + 1, min(j, n)): + if text[k] != "\n": + out[k] = " " + i = j + 1 + elif c == "'": + m = char_re.match(text, i) + if m: + if not keep_strings: + for k in range(i + 1, m.end() - 1): + out[k] = " " + i = m.end() + else: + i += 1 + else: + i += 1 + return "".join(out) + + +def match_close(text, i): + """Index of the bracket closing the one at text[i].""" + depth = 0 + for j in range(i, len(text)): + if text[j] in "([{": + depth += 1 + elif text[j] in ")]}": + depth -= 1 + if depth == 0: + return j + raise invalid(f"unbalanced bracket at offset {i}") + + +def split_top(text, a, b, sep=","): + """(start, end) spans of text[a:b] split at `sep` outside any bracket.""" + spans, depth, s = [], 0, a + for j in range(a, b): + ch = text[j] + if ch in "([{": + depth += 1 + elif ch in ")]}": + depth -= 1 + elif ch == sep and depth == 0: + spans.append((s, j)) + s = j + 1 + spans.append((s, b)) + return [(x, y) for x, y in spans if text[x:y].strip()] + + +def line_of(text, idx): + return text.count("\n", 0, idx) + 1 + + +def parse_executable(src): + raw = read_bytes(Path(src) / BIND_PATH).decode() + t = scan(raw, False) + m = re.search(r"pub const EXECUTABLE:\s*\[FnIndex;\s*(\d+)\]\s*=\s*\[", t) + if not m: + raise invalid(f"{BIND_PATH}: no `pub const EXECUTABLE: [FnIndex; N]`") + close = match_close(t, m.end() - 1) + names = [t[a:b].strip() for a, b in split_top(t, m.end(), close)] + if len(names) != int(m.group(1)): + raise invalid(f"{BIND_PATH}: EXECUTABLE declares {m.group(1)} but lists {len(names)}") + return names + + +def parse_words(src): + path = Path(src) / POLY_PATH + raw_bytes = read_bytes(path) + raw = raw_bytes.decode() + nostr, withstr = scan(raw, False), scan(raw, True) + m = re.search(r"pub const POLYFILL:\s*\[FrozenWord;\s*(\d+)\]\s*=\s*\[", nostr) + if not m: + raise invalid(f"{POLY_PATH}: no `pub const POLYFILL: [FrozenWord; N]`") + declared = int(m.group(1)) + close = match_close(nostr, m.end() - 1) + entry_re = re.compile(r"\bw\s*\(") + entries, pos = [], m.end() + while True: + mm = entry_re.search(nostr, pos, close) + if not mm: + break + o = mm.end() - 1 + c = match_close(nostr, o) + args = split_top(nostr, o + 1, c) + if len(args) < 3: + raise invalid(f"{POLY_PATH}:{line_of(raw, mm.start())}: entry has {len(args)} arguments") + opcode = nostr[args[0][0]:args[0][1]].strip() + if not re.fullmatch(r"[A-Z][A-Z0-9_]*", opcode): + raise invalid(f"{POLY_PATH}:{line_of(raw, mm.start())}: opcode {opcode!r} is not a constant name") + try: + arity = int(nostr[args[1][0]:args[1][1]].strip()) + except ValueError: + raise invalid(f"{POLY_PATH}:{line_of(raw, mm.start())}: arity is not an integer") + aliases = re.findall(r'"((?:[^"\\]|\\.)*)"', withstr[args[2][0]:args[2][1]]) + if not aliases: + raise invalid(f"{POLY_PATH}:{line_of(raw, mm.start())}: entry has no aliases") + entries.append({"opcode": opcode, "arity": arity, "r2il_name": aliases[0], + "aliases": aliases, "line": line_of(raw, mm.start())}) + pos = c + 1 + if len(entries) != declared: + raise invalid(f"{POLY_PATH}: array type says {declared} entries, parsed {len(entries)}") + if len(entries) > MAX_WORDS: + raise invalid(f"{POLY_PATH}: {len(entries)} entries exceed {MAX_WORDS} (word is a u8)") + exe = parse_executable(src) + if exe != [e["opcode"] for e in entries]: + raise invalid(f"{BIND_PATH}: EXECUTABLE order differs from POLYFILL opcode order") + digest = fnv64(raw_bytes) + for p, e in enumerate(entries): + e.update({"word": p, "file": POLY_PATH, "fnv": digest}) + return entries + + +CONST_RE = re.compile(r"(?:[A-Za-z_][A-Za-z0-9_]*::)*([A-Z][A-Z0-9_]+)") +BIND_RE = re.compile(r"[a-z_][A-Za-z0-9_]*") + + +def arm_opcodes(pattern): + pattern = re.sub(r"\s+if\s+.*$", "", pattern.strip(), flags=re.S) + ops = [] + for a, b in split_top(pattern, 0, len(pattern), "|"): + alt = pattern[a:b].strip() + m = CONST_RE.fullmatch(alt) + if m: + op = m.group(1) + elif alt == "_" or BIND_RE.fullmatch(alt): + op = "*" + else: + continue + if op not in ops: + ops.append(op) + return ops + + +def arm_callees(body): + found = set() + for m in re.finditer(r"\bself\.([a-z_]\w*)\s*\(", body): + found.add(m.group(1)) + for m in re.finditer(r"(?]*>)?\s+FoldDialect\b[^{;]*\{", t) + if not m: + raise invalid(f"{LIB_PATH}: no inherent `impl FoldDialect`") + f = t.find("pub fn call(", m.end()) + if f < 0: + raise invalid(f"{LIB_PATH}: no `pub fn call(` in `impl FoldDialect`") + sig_close = match_close(t, f + len("pub fn call")) + body_open = t.find("{", sig_close) + mm = re.compile(r"\bmatch\s+f\s*\{").search(t, body_open) + if body_open < 0 or not mm: + raise invalid(f"{LIB_PATH}: no `match f {{` in `FoldDialect::call`") + open_i = mm.end() - 1 + close_i = match_close(t, open_i) + rows, i = [], open_i + 1 + while True: + while i < close_i and (t[i].isspace() or t[i] == ","): + i += 1 + if i >= close_i: + break + pat_start, depth, j = i, 0, i + while j < close_i: + ch = t[j] + if ch in "([{": + depth += 1 + elif ch in ")]}": + depth -= 1 + elif depth == 0 and t.startswith("=>", j): + break + j += 1 + else: + raise invalid(f"{LIB_PATH}:{line_of(raw, pat_start)}: match arm without `=>`") + pattern = t[pat_start:j] + k = j + 2 + while t[k].isspace(): + k += 1 + if t[k] == "{": + end = match_close(t, k) + k = end + 1 + else: + d = 0 + while k < close_i: + ch = t[k] + if ch in "([{": + d += 1 + elif ch in ")]}": + d -= 1 + elif ch == "," and d == 0: + break + k += 1 + end = k - 1 + while t[end].isspace(): + end -= 1 + body = t[j + 2:end + 1] + arm_line, arm_end = line_of(raw, pat_start), line_of(raw, end) + callees = "|".join(arm_callees(body)) + for op in arm_opcodes(pattern): + rows.append({"opcode": op, "symbol": DISPATCH_SYMBOL, "file": LIB_PATH, + "arm_line": arm_line, "arm_end": arm_end, "callees": callees, + "fnv": fnv64(raw_bytes), "grade": GRADE, + "note": "match arm; one symbol dispatches every opcode"}) + while k < close_i and t[k].isspace(): + k += 1 + if k < close_i and t[k] == ",": + k += 1 + i = k + return rows + + +def load_uses(path, nwords): + p = Path(path) + if not p.is_file(): + raise ArtifactError("ARTIFACT_MISSING", str(p)) + cols, rows = None, [] + for n, line in enumerate(p.read_text().splitlines(), 1): + if line.startswith("#schema "): + cols = line[len("#schema "):].split("\t") + elif line and not line.startswith("#"): + if cols is None or any(c not in cols for c in USES_COLS): + raise invalid(f"{p}:{n}: uses file has no #schema with columns {USES_COLS}") + r = dict(zip(cols, line.split("\t"))) + try: + wi, ordinal = int(r["word"]), int(r["ordinal"]) + lens = int(r["lens"], 0) + except (KeyError, ValueError): + raise invalid(f"{p}:{n}: word, ordinal or lens is not an integer") + if lens != LENS: + raise invalid(f"{p}:{n}: lens {r['lens']} is not the polyfill lens {LENS_TEXT}") + if wi >= nwords: + raise invalid(f"{p}:{n}: word {wi} is outside the {nwords} words of lens {LENS_TEXT}") + r["word"], r["ord_int"] = wi, ordinal + rows.append(r) + return rows + + +def tsv(*cols): + return "\t".join(str(c) for c in cols) + + +def cmd_words(src): + words = parse_words(src) + lines = ["#cg-beh v1", + "#schema lens\tword\topcode\tarity\tr2il_name\taliases\tfile\tline\tfile_fnv64\tgrade"] + for w in words: + lines.append(tsv(LENS_TEXT, w["word"], w["opcode"], w["arity"], w["r2il_name"], + "|".join(w["aliases"]), w["file"], w["line"], w["fnv"], GRADE)) + return "\n".join(lines) + "\n" + + +def cmd_dispatch(src): + lines = ["#cg-beh v1", + "#schema opcode\tsymbol\tfile\tarm_line\tarm_end\tcallees\tfile_fnv64\tgrade\tnote"] + for d in parse_dispatch(src): + lines.append(tsv(d["opcode"], d["symbol"], d["file"], d["arm_line"], d["arm_end"], + d["callees"], d["fnv"], d["grade"], d["note"])) + return "\n".join(lines) + "\n" + + +def impact(src, uses_path, symbol, line=None, witness_fnv=None, drop_dispatch=None): + """Pure: (exit_code, text) for one symbol. Nothing is read beyond SRC and the uses file.""" + try: + words = parse_words(src) + disp = parse_dispatch(src) + uses = load_uses(uses_path, len(words)) + except ArtifactError as e: + return 3, str(e) + "\n" + current = disp[0]["fnv"] if disp else fnv64(read_bytes(Path(src) / LIB_PATH)) + if witness_fnv is not None and witness_fnv != current: + return 4, "STALE\tdispatch witness is for another revision of lib.rs; no current answer\n" + if drop_dispatch: + disp = [d for d in disp if d["opcode"] != drop_dispatch] + arms = [d for d in disp if d["symbol"] == symbol] + if line is not None: + arms = [d for d in arms if d["arm_line"] <= line <= d["arm_end"]] + if not arms: + return 0, NONE_ROW + "\n" + by_op = {w["opcode"]: w for w in words} + dispatched = {d["opcode"] for d in disp} + out = ["#cg-beh v1", tsv("query", symbol, "-" if line is None else line)] + seen, hit = set(), [] + for d in arms: + out.append(tsv("realizes", d["symbol"], f"{d['arm_line']}-{d['arm_end']}", d["opcode"], GRADE, + "dispatch match arm (read from source; not a compiler call fact, not a behavioural test)")) + op = d["opcode"] + if op in seen: + continue + seen.add(op) + w = by_op.get(op) + if w is None: + out.append(tsv("opcode", op, f"not a word of lens {LENS_TEXT}")) + continue + p = w["word"] + out.append(tsv("word", LENS_TEXT, p, w["r2il_name"], GRADE, "position in the POLYFILL table")) + us = sorted([u for u in uses if u["word"] == p], + key=lambda u: (u["class"], u["method"], u["ord_int"])) + if not us: + out.append(tsv("word", p, w["r2il_name"], + "no registered user in this uses file (store-scoped, not: unaffected)")) + for u in us: + out.append(tsv("uses", u["class"], u["method"], u["sig_rev"], u["ordinal"], p, "REGISTRATION", + "read from a BindStore through admit() in a test run")) + hit.append(u) + out.append("boundary\tsource evidence ends at the POLYFILL table; everything in 'uses' rows is DT64 registration evidence") + methods = {(u["class"], u["method"], u["sig_rev"]) for u in hit} + classes = {u["class"] for u in hit} + out.append(tsv("affected_methods", len(methods))) + out.append("affected_classes\t" + ",".join(sorted(classes))) + out.append("coverage\tPARTIAL\t" + "; ".join(RESIDUALS)) + used_words = {u["word"] for u in uses} + for w in words: + if w["word"] in used_words and w["opcode"] not in dispatched: + out.append(tsv("UNKNOWN", f"word {w['word']} {w['r2il_name']}", + "no dispatch witness; its users cannot be confirmed or excluded")) + return 0, "\n".join(out) + "\n" + + +# ---------------------------------------------------------------- self-test + +FAKE_POLY = '''pub const POLYFILL_LENS: u32 = 0; +pub const POLYFILL: [FrozenWord; 3] = [ + w(INT_ADD, 2, &["IntAdd", "+"], Some(1), None), + w( + INT_S_LESS, + 2, + &["IntSLess", "<", "Cmp::LtI32"], + Some(1), + Some(2), + ), + w(POP_COUNT, 1, &["PopCount", "COUNT"], Some(1), None), +]; +''' + +FAKE_BIND = '''/// Executable subset. +pub const EXECUTABLE: [FnIndex; 3] = [ + INT_ADD, INT_S_LESS, POP_COUNT, +]; +''' + +FAKE_LIB = '''use x; + +pub struct FoldDialect<'a> { + cell: &'a u8, +} + +impl Dialect for FoldDialect<'_> { + fn call(&mut self, f: FnIndex, v: [u8; 3], stack: &mut Vec) -> Result<(), FoldError> { + FoldDialect::call(self, f, v, stack) + } +} + +fn int_s_less(a: i64, b: i64) -> bool { + a < b +} + +impl FoldDialect<'_> { + pub fn call(&mut self, f: FnIndex, v: [u8; 3], stack: &mut Vec) -> Result<(), FoldError> { + match f { + FnIndex::NUMBER => { + stack.push(Val::Scalar(i64::from(v[0]))); + Ok(()) + } + INT_ADD | INT_S_LESS => { + // INT_NOT => decoy in a comment + let b = stack.pop().ok_or(FoldError::Underflow(f))?; + self.emit_pred(b, stack); + helper(b); + Ok(()) + } + POP_COUNT => self.count(stack), + other => Err(FoldError::Unimplemented(other)), + } + } +} +''' + +UCOLS = "#schema " + "\t".join(USES_COLS) + "\n" + + +def urow(cls, method, ordinal, word, lens=LENS_TEXT, opcode="X", name="N"): + return "\t".join([cls, method, "r1", str(ordinal), lens, str(word), opcode, name, "2", "t", "a"]) + "\n" + + +def mk_tree(td, poly=FAKE_POLY, bind=FAKE_BIND, lib=FAKE_LIB): + root = Path(td) / "src" + for rel, body in ((POLY_PATH, poly), (BIND_PATH, bind), (LIB_PATH, lib)): + p = root / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(body) + return root + + +def mk_uses(td, rows, name="uses.tsv"): + p = Path(td) / name + p.write_text("#cg-beh-uses v1\n" + UCOLS + "".join(rows)) + return p + + +BASE_USES = [urow("ClsA", "m1", 0, 0, opcode="INT_ADD", name="IntAdd"), + urow("ClsB", "m2", 1, 2, opcode="POP_COUNT", name="PopCount")] + + +class Negatives(unittest.TestCase): + def setUp(self): + self.td = tempfile.TemporaryDirectory() + self.src = mk_tree(self.td.name) + self.uses = mk_uses(self.td.name, BASE_USES) + + def tearDown(self): + self.td.cleanup() + + def run_impact(self, symbol=DISPATCH_SYMBOL, **kw): + return impact(self.src, self.uses, symbol, **kw) + + def test_01_missing_dispatch_mapping_is_unknown_not_unaffected(self): + """A used word with no dispatch arm must be UNKNOWN, and other words' rows must not move.""" + _, base = self.run_impact() + code, dropped = self.run_impact(drop_dispatch="POP_COUNT") + self.assertEqual(code, 0) + self.assertIn("UNKNOWN\tword 2 PopCount\tno dispatch witness", dropped) + self.assertNotIn("UNKNOWN", base) + + def stable(text): + return [l for l in text.splitlines() + if l.split("\t")[0] in ("realizes", "word", "uses") + and "POP_COUNT" not in l and "PopCount" not in l and "\t2\t" not in l + and "\tm2\t" not in l] + self.assertEqual(stable(base), stable(dropped)) + self.assertNotIn("\tm2\t", dropped) + + def test_02_stale_witness_gives_no_answer(self): + """A witness for another lib.rs revision must exit 4 and print no realizes or uses row.""" + code, text = self.run_impact(witness_fnv="0000000000000000") + self.assertEqual(code, 4) + self.assertTrue(text.startswith("STALE\t")) + self.assertNotIn("realizes", text) + self.assertNotIn("uses\t", text) + cur = fnv64((self.src / LIB_PATH).read_bytes()) + self.assertEqual(self.run_impact(witness_fnv=cur)[0], 0) + + def test_03_a_matching_name_is_not_a_behaviour_edge(self): + """A function named like a word but outside the dispatch match must give NONE.""" + for sym in ("int_s_less", "lance_graph_r2il::int_s_less"): + code, text = self.run_impact(sym) + self.assertEqual(code, 0) + self.assertIn("behaviour\tNONE", text) + self.assertNotIn("realizes", text) + + def test_04_a_word_without_user_is_stated_not_omitted(self): + """A dispatched word with no registered user must be reported as store-scoped, not unaffected.""" + _, text = self.run_impact() + self.assertIn("no registered user in this uses file (store-scoped, not: unaffected)", text) + self.assertIn("word\t1\tIntSLess\tno registered user", text) + + def test_05_a_non_dispatching_symbol_has_no_behaviour_edge(self): + """A symbol that dispatches nothing must print `behaviour NONE ... (not: no impact)`.""" + code, text = self.run_impact("lance_graph_r2il::nothing") + self.assertEqual(code, 0) + self.assertEqual(text, NONE_ROW + "\n") + + def test_06_polyfill_order_must_match_executable(self): + """POLYFILL opcode order differing from EXECUTABLE must be ARTIFACT_INVALID.""" + src = mk_tree(Path(self.td.name) / "b", bind=FAKE_BIND.replace("INT_ADD, INT_S_LESS,", "INT_S_LESS, INT_ADD,")) + code, text = impact(src, self.uses, DISPATCH_SYMBOL) + self.assertEqual(code, 3) + self.assertIn("ARTIFACT_INVALID", text) + with self.assertRaises(ArtifactError): + parse_words(src) + + def test_07_a_word_outside_the_lens_is_invalid(self): + """A uses row whose word is >= the word count must be ARTIFACT_INVALID.""" + uses = mk_uses(self.td.name, [urow("ClsA", "m1", 0, 3)], "u7.tsv") + code, text = impact(self.src, uses, DISPATCH_SYMBOL) + self.assertEqual(code, 3) + self.assertIn("ARTIFACT_INVALID", text) + + def test_08_another_lens_is_invalid(self): + """A uses row naming a lens other than 0xFFFFF000 must be ARTIFACT_INVALID.""" + uses = mk_uses(self.td.name, [urow("ClsA", "m1", 0, 0, lens="0xFFFFF100")], "u8.tsv") + code, text = impact(self.src, uses, DISPATCH_SYMBOL) + self.assertEqual(code, 3) + self.assertIn("ARTIFACT_INVALID", text) + + def test_09_same_input_gives_identical_output(self): + """Two runs on one input, in-process and across processes, must be byte-identical.""" + rows = [urow(c, "m", i, 0) for i, c in enumerate(("Z", "A", "M", "Q", "B"))] + uses = mk_uses(self.td.name, rows, "u9.tsv") + self.assertEqual(impact(self.src, uses, DISPATCH_SYMBOL), impact(self.src, uses, DISPATCH_SYMBOL)) + argv = [sys.executable, "-I", __file__, "impact", "--src", str(self.src), "--uses", str(uses), + "--symbol", DISPATCH_SYMBOL] + outs = {subprocess.run(argv, capture_output=True, check=True).stdout for _ in range(4)} + self.assertEqual(len(outs), 1) + self.assertIn(b"affected_classes\tA,B,M,Q,Z\n", outs.pop()) + + def test_10_line_selects_one_arm(self): + """--line must select only the arm containing it; a line in no arm must give NONE.""" + lines = FAKE_LIB.splitlines() + pop = next(i for i, l in enumerate(lines, 1) if l.startswith(" POP_COUNT =>")) + add = next(i for i, l in enumerate(lines, 1) if "INT_ADD | INT_S_LESS" in l) + 2 + _, text = self.run_impact(line=pop) + self.assertIn("\tPOP_COUNT\t", text) + self.assertNotIn("INT_ADD", text) + _, both = self.run_impact(line=add) + self.assertIn("\tINT_ADD\t", both) + self.assertIn("\tINT_S_LESS\t", both) + self.assertNotIn("POP_COUNT", both) + code, none = self.run_impact(line=1) + self.assertEqual((code, none), (0, NONE_ROW + "\n")) + + def test_11_uses_print_in_ordinal_order(self): + """Uses listed in reverse file order must print by ordinal, with the ordinal column kept.""" + rows = [urow("ClsA", "m1", o, 0) for o in (2, 0, 1)] + uses = mk_uses(self.td.name, rows, "u11.tsv") + _, text = impact(self.src, uses, DISPATCH_SYMBOL) + ords = [l.split("\t")[4] for l in text.splitlines() if l.startswith("uses\t")] + self.assertEqual(ords, ["0", "1", "2"]) + + def test_12_missing_uses_file_is_not_an_empty_impact(self): + """A missing uses file must be ARTIFACT_MISSING with exit 3.""" + code, text = impact(self.src, Path(self.td.name) / "absent.tsv", DISPATCH_SYMBOL) + self.assertEqual(code, 3) + self.assertTrue(text.startswith("ARTIFACT_MISSING")) + + def test_13_dispatch_parse_reads_arms_not_comments_or_other_receivers(self): + """The arm table must hold the constants of the real patterns, the catch-all, and self/bare callees only.""" + rows = parse_dispatch(self.src) + self.assertEqual([r["opcode"] for r in rows], ["NUMBER", "INT_ADD", "INT_S_LESS", "POP_COUNT", "*"]) + by = {r["opcode"]: r for r in rows} + self.assertEqual(by["INT_ADD"]["callees"], "emit_pred|helper") + self.assertEqual(by["POP_COUNT"]["callees"], "count") + self.assertEqual(by["NUMBER"]["callees"], "") + self.assertEqual((by["INT_ADD"]["arm_line"], by["INT_ADD"]["arm_end"]), (24, 30)) + + +def main(argv): + ap = argparse.ArgumentParser() + ap.add_argument("--self-test", action="store_true") + sub = ap.add_subparsers(dest="cmd") + for name in ("words", "dispatch", "impact"): + sp = sub.add_parser(name) + sp.add_argument("--src", required=True) + if name == "impact": + sp.add_argument("--uses", required=True) + sp.add_argument("--symbol", required=True) + sp.add_argument("--line", type=int) + sp.add_argument("--witness-fnv") + sp.add_argument("--drop-dispatch") + a = ap.parse_args(argv) + if a.self_test: + r = unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(Negatives)) + return 0 if r.wasSuccessful() else 1 + if a.cmd is None: + ap.print_help() + return 2 + try: + if a.cmd == "words": + sys.stdout.write(cmd_words(a.src)) + elif a.cmd == "dispatch": + sys.stdout.write(cmd_dispatch(a.src)) + else: + code, text = impact(a.src, a.uses, a.symbol, a.line, a.witness_fnv, a.drop_dispatch) + sys.stdout.write(text) + return code + except ArtifactError as e: + print(e) + return 3 + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/.claude/probes/2026-11-11-cg-beh-1/disable_runs.py b/.claude/probes/2026-11-11-cg-beh-1/disable_runs.py new file mode 100644 index 000000000..654145851 --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/disable_runs.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Disable runs for beh.py (CG-BEH-1): each case applies ONE textual mutation to a temporary copy, +runs the copy's `--self-test`, and requires the test that belongs to the mutated guard to fail. + +A case whose test stays green is reported GREEN (guard not load-bearing!) and the exit status is 1. +A mutation whose anchor does not occur exactly once aborts the run: a disable that does not apply +looks the same as a guard that does not bind. The unmutated copy must pass first. +""" + +import re +import subprocess +import sys +import tempfile +from pathlib import Path + +BEH = Path(__file__).with_name("beh.py") + +# (test, old, new): `old` occurs exactly once in beh.py. +CASES = [ + ("test_01", 'w["opcode"] not in dispatched:', 'w["opcode"] in dispatched:'), + ("test_02", "if witness_fnv is not None and witness_fnv != current:", "if False:"), + ("test_03", 'arms = [d for d in disp if d["symbol"] == symbol]', + 'arms = [d for d in disp if d["symbol"] == symbol or symbol.split("::")[-1].upper() == d["opcode"]]'), + ("test_04", " if not us:\n", " if False:\n"), + ("test_05", " if not arms:\n", " if False:\n"), + ("test_06", 'if exe != [e["opcode"] for e in entries]:', "if False:"), + ("test_07", "if wi >= nwords:", "if False:"), + ("test_08", "if lens != LENS:", "if False:"), + ("test_09", '",".join(sorted(classes))', '",".join(classes)'), + ("test_10", 'arms = [d for d in arms if d["arm_line"] <= line <= d["arm_end"]]', "arms = arms"), + ("test_11", 'u["ord_int"]))', "0))"), + ("test_12", " if not p.is_file():\n raise ArtifactError(\"ARTIFACT_MISSING\", str(p))\n cols, rows", + " cols, rows"), +] + + +def run_selftest(path): + p = subprocess.run([sys.executable, "-I", str(path), "--self-test"], capture_output=True, text=True) + failing = sorted(set(re.findall(r"^(?:FAIL|ERROR): (test_\w+)", p.stderr + p.stdout, flags=re.M))) + return p.returncode, failing + + +def main(): + src = BEH.read_text() + with tempfile.TemporaryDirectory() as td: + base = Path(td) / "base.py" + base.write_text(src) + code, failing = run_selftest(base) + if code != 0: + print(f"BASELINE\tself-test fails before any mutation\t{','.join(failing)}") + return 1 + bad = 0 + for i, (test, old, new) in enumerate(CASES): + assert src.count(old) == 1, f"anchor for {test} occurs {src.count(old)} times" + mut = Path(td) / f"mut_{i}.py" + mut.write_text(src.replace(old, new)) + _, failing = run_selftest(mut) + red = any(f.startswith(test) for f in failing) + bad += not red + print(f"{test}\t{'RED' if red else 'GREEN (guard not load-bearing!)'}\t{','.join(failing) or '-'}") + return 1 if bad else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.claude/probes/2026-11-11-cg-beh-1/dispatch.tsv b/.claude/probes/2026-11-11-cg-beh-1/dispatch.tsv new file mode 100644 index 000000000..e026c7944 --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/dispatch.tsv @@ -0,0 +1,28 @@ +#cg-beh v1 +#schema opcode symbol file arm_line arm_end callees file_fnv64 grade note +NUMBER lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 860 863 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +LOAD lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 864 886 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +VIA lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 887 903 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_EQUAL lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 904 974 const_i32|const_u32|emit_pred b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_LESS lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 975 975 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_LESS_EQUAL lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 975 975 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_S_LESS lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 976 1018 const_i32|emit_pred b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_AND lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1019 1082 and_peephole|fresh|fuse_binary|is_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_OR lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1086 1112 fresh|fuse_binary|is_mask|operand b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_XOR lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1114 1140 fresh|fuse_binary|is_mask|operand b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_ADD lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1145 1162 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_MULT lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1145 1162 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_S_DIV lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1145 1162 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_SUB lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1163 1175 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +POP_COUNT lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1176 1183 finalize_and_run_scalar|fold_mask|resolve_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +KEY_RUNS lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1195 1210 finalize_and_run_scalar|fold_mask|resolve_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +ANY lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1214 1226 finalize_and_run_scalar|fold_mask|resolve_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +ALL lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1214 1226 finalize_and_run_scalar|fold_mask|resolve_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +SUM lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1227 1246 finalize_and_run_scalar|fold_mask|i32_lane|resolve_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +MIN lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1247 1274 finalize_and_run|fold_mask|i32_lane|resolve_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +MAX lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1247 1274 finalize_and_run|fold_mask|i32_lane|resolve_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +GROUP_SUM lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1275 1314 finalize_and_run|fold_mask|resolve_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +INT_NOT lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1315 1343 expr_of|fresh|is_mask|push_expr b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +RANGE lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1348 1357 emit_pred b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +KEEP lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1360 1368 finalize_and_run|fold_mask|resolve_mask b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode +* lance_graph_r2il::FoldDialect::call crates/lance-graph-r2il/src/lib.rs 1369 1369 b60ac87550156f8b SOURCE_STRUCTURE match arm; one symbol dispatches every opcode diff --git a/.claude/probes/2026-11-11-cg-beh-1/examples/impact-arm-int_s_less.tsv b/.claude/probes/2026-11-11-cg-beh-1/examples/impact-arm-int_s_less.tsv new file mode 100644 index 000000000..849aacf6b --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/examples/impact-arm-int_s_less.tsv @@ -0,0 +1,10 @@ +#cg-beh v1 +query lance_graph_r2il::FoldDialect::call 980 +realizes lance_graph_r2il::FoldDialect::call 976-1018 INT_S_LESS SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 5 IntSLess SOURCE_STRUCTURE position in the POLYFILL table +uses 0x7F0B0001 1 1 0 5 REGISTRATION read from a BindStore through admit() in a test run +uses 0x7F0B0001 2 1 0 5 REGISTRATION read from a BindStore through admit() in a test run +boundary source evidence ends at the POLYFILL table; everything in 'uses' rows is DT64 registration evidence +affected_methods 2 +affected_classes 0x7F0B0001 +coverage PARTIAL native lowering path (binding_native::prepare_native) not mapped; uses file covers one test store only; realizes is arm-level source structure, not a behavioural witness diff --git a/.claude/probes/2026-11-11-cg-beh-1/examples/impact-missing-dispatch.tsv b/.claude/probes/2026-11-11-cg-beh-1/examples/impact-missing-dispatch.tsv new file mode 100644 index 000000000..9b85c15e9 --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/examples/impact-missing-dispatch.tsv @@ -0,0 +1,73 @@ +#cg-beh v1 +query lance_graph_r2il::FoldDialect::call - +realizes lance_graph_r2il::FoldDialect::call 860-863 NUMBER SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode NUMBER not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 864-886 LOAD SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode LOAD not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 887-903 VIA SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode VIA not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 904-974 INT_EQUAL SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 4 IntEqual SOURCE_STRUCTURE position in the POLYFILL table +word 4 IntEqual no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 975-975 INT_LESS SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode INT_LESS not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 975-975 INT_LESS_EQUAL SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode INT_LESS_EQUAL not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 1019-1082 INT_AND SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 6 IntAnd SOURCE_STRUCTURE position in the POLYFILL table +word 6 IntAnd no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1086-1112 INT_OR SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 7 IntOr SOURCE_STRUCTURE position in the POLYFILL table +word 7 IntOr no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1114-1140 INT_XOR SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 8 IntXor SOURCE_STRUCTURE position in the POLYFILL table +word 8 IntXor no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1145-1162 INT_ADD SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 0 IntAdd SOURCE_STRUCTURE position in the POLYFILL table +word 0 IntAdd no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1145-1162 INT_MULT SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 2 IntMult SOURCE_STRUCTURE position in the POLYFILL table +word 2 IntMult no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1145-1162 INT_S_DIV SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 3 IntSDiv SOURCE_STRUCTURE position in the POLYFILL table +word 3 IntSDiv no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1163-1175 INT_SUB SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 1 IntSub SOURCE_STRUCTURE position in the POLYFILL table +word 1 IntSub no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1176-1183 POP_COUNT SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 10 PopCount SOURCE_STRUCTURE position in the POLYFILL table +uses 0x7F0B0001 2 1 1 10 REGISTRATION read from a BindStore through admit() in a test run +realizes lance_graph_r2il::FoldDialect::call 1195-1210 KEY_RUNS SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 16 KEY_RUNS SOURCE_STRUCTURE position in the POLYFILL table +word 16 KEY_RUNS no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1214-1226 ANY SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 14 ANY SOURCE_STRUCTURE position in the POLYFILL table +word 14 ANY no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1214-1226 ALL SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 15 ALL SOURCE_STRUCTURE position in the POLYFILL table +word 15 ALL no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1227-1246 SUM SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 11 SUM SOURCE_STRUCTURE position in the POLYFILL table +uses 0x7F0B0001 1 1 1 11 REGISTRATION read from a BindStore through admit() in a test run +realizes lance_graph_r2il::FoldDialect::call 1247-1274 MIN SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 12 MIN SOURCE_STRUCTURE position in the POLYFILL table +word 12 MIN no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1247-1274 MAX SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 13 MAX SOURCE_STRUCTURE position in the POLYFILL table +word 13 MAX no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1275-1314 GROUP_SUM SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode GROUP_SUM not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 1315-1343 INT_NOT SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 9 IntNot SOURCE_STRUCTURE position in the POLYFILL table +word 9 IntNot no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1348-1357 RANGE SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode RANGE not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 1360-1368 KEEP SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode KEEP not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 1369-1369 * SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode * not a word of lens 0xFFFFF000 +boundary source evidence ends at the POLYFILL table; everything in 'uses' rows is DT64 registration evidence +affected_methods 2 +affected_classes 0x7F0B0001 +coverage PARTIAL native lowering path (binding_native::prepare_native) not mapped; uses file covers one test store only; realizes is arm-level source structure, not a behavioural witness +UNKNOWN word 5 IntSLess no dispatch witness; its users cannot be confirmed or excluded diff --git a/.claude/probes/2026-11-11-cg-beh-1/examples/impact-stale-witness.tsv b/.claude/probes/2026-11-11-cg-beh-1/examples/impact-stale-witness.tsv new file mode 100644 index 000000000..c118b9567 --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/examples/impact-stale-witness.tsv @@ -0,0 +1 @@ +STALE dispatch witness is for another revision of lib.rs; no current answer diff --git a/.claude/probes/2026-11-11-cg-beh-1/examples/impact-symbol.tsv b/.claude/probes/2026-11-11-cg-beh-1/examples/impact-symbol.tsv new file mode 100644 index 000000000..a81093a5f --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/examples/impact-symbol.tsv @@ -0,0 +1,76 @@ +#cg-beh v1 +query lance_graph_r2il::FoldDialect::call - +realizes lance_graph_r2il::FoldDialect::call 860-863 NUMBER SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode NUMBER not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 864-886 LOAD SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode LOAD not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 887-903 VIA SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode VIA not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 904-974 INT_EQUAL SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 4 IntEqual SOURCE_STRUCTURE position in the POLYFILL table +word 4 IntEqual no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 975-975 INT_LESS SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode INT_LESS not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 975-975 INT_LESS_EQUAL SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode INT_LESS_EQUAL not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 976-1018 INT_S_LESS SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 5 IntSLess SOURCE_STRUCTURE position in the POLYFILL table +uses 0x7F0B0001 1 1 0 5 REGISTRATION read from a BindStore through admit() in a test run +uses 0x7F0B0001 2 1 0 5 REGISTRATION read from a BindStore through admit() in a test run +realizes lance_graph_r2il::FoldDialect::call 1019-1082 INT_AND SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 6 IntAnd SOURCE_STRUCTURE position in the POLYFILL table +word 6 IntAnd no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1086-1112 INT_OR SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 7 IntOr SOURCE_STRUCTURE position in the POLYFILL table +word 7 IntOr no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1114-1140 INT_XOR SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 8 IntXor SOURCE_STRUCTURE position in the POLYFILL table +word 8 IntXor no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1145-1162 INT_ADD SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 0 IntAdd SOURCE_STRUCTURE position in the POLYFILL table +word 0 IntAdd no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1145-1162 INT_MULT SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 2 IntMult SOURCE_STRUCTURE position in the POLYFILL table +word 2 IntMult no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1145-1162 INT_S_DIV SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 3 IntSDiv SOURCE_STRUCTURE position in the POLYFILL table +word 3 IntSDiv no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1163-1175 INT_SUB SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 1 IntSub SOURCE_STRUCTURE position in the POLYFILL table +word 1 IntSub no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1176-1183 POP_COUNT SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 10 PopCount SOURCE_STRUCTURE position in the POLYFILL table +uses 0x7F0B0001 2 1 1 10 REGISTRATION read from a BindStore through admit() in a test run +realizes lance_graph_r2il::FoldDialect::call 1195-1210 KEY_RUNS SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 16 KEY_RUNS SOURCE_STRUCTURE position in the POLYFILL table +word 16 KEY_RUNS no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1214-1226 ANY SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 14 ANY SOURCE_STRUCTURE position in the POLYFILL table +word 14 ANY no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1214-1226 ALL SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 15 ALL SOURCE_STRUCTURE position in the POLYFILL table +word 15 ALL no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1227-1246 SUM SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 11 SUM SOURCE_STRUCTURE position in the POLYFILL table +uses 0x7F0B0001 1 1 1 11 REGISTRATION read from a BindStore through admit() in a test run +realizes lance_graph_r2il::FoldDialect::call 1247-1274 MIN SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 12 MIN SOURCE_STRUCTURE position in the POLYFILL table +word 12 MIN no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1247-1274 MAX SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 13 MAX SOURCE_STRUCTURE position in the POLYFILL table +word 13 MAX no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1275-1314 GROUP_SUM SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode GROUP_SUM not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 1315-1343 INT_NOT SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +word 0xFFFFF000 9 IntNot SOURCE_STRUCTURE position in the POLYFILL table +word 9 IntNot no registered user in this uses file (store-scoped, not: unaffected) +realizes lance_graph_r2il::FoldDialect::call 1348-1357 RANGE SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode RANGE not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 1360-1368 KEEP SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode KEEP not a word of lens 0xFFFFF000 +realizes lance_graph_r2il::FoldDialect::call 1369-1369 * SOURCE_STRUCTURE dispatch match arm (read from source; not a compiler call fact, not a behavioural test) +opcode * not a word of lens 0xFFFFF000 +boundary source evidence ends at the POLYFILL table; everything in 'uses' rows is DT64 registration evidence +affected_methods 2 +affected_classes 0x7F0B0001 +coverage PARTIAL native lowering path (binding_native::prepare_native) not mapped; uses file covers one test store only; realizes is arm-level source structure, not a behavioural witness diff --git a/.claude/probes/2026-11-11-cg-beh-1/uses.tsv b/.claude/probes/2026-11-11-cg-beh-1/uses.tsv new file mode 100644 index 000000000..dc8149cdb --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/uses.tsv @@ -0,0 +1,6 @@ +#cg-beh v1 +#schema class method sig_rev ordinal lens word opcode r2il_name arity target args +0x7F0B0001 1 1 0 0xFFFFF000 5 174 IntSLess 2 3 F0,C5 +0x7F0B0001 1 1 1 0xFFFFF000 11 228 SUM 2 4 F3,F1 +0x7F0B0001 2 1 0 0xFFFFF000 5 174 IntSLess 2 3 F0,C5 +0x7F0B0001 2 1 1 0xFFFFF000 10 185 PopCount 1 4 F3 diff --git a/.claude/probes/2026-11-11-cg-beh-1/words.tsv b/.claude/probes/2026-11-11-cg-beh-1/words.tsv new file mode 100644 index 000000000..7ba343216 --- /dev/null +++ b/.claude/probes/2026-11-11-cg-beh-1/words.tsv @@ -0,0 +1,19 @@ +#cg-beh v1 +#schema lens word opcode arity r2il_name aliases file line file_fnv64 grade +0xFFFFF000 0 INT_ADD 2 IntAdd IntAdd|+ crates/lance-graph-r2il/src/polyfill.rs 117 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 1 INT_SUB 2 IntSub IntSub|- crates/lance-graph-r2il/src/polyfill.rs 118 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 2 INT_MULT 2 IntMult IntMult|* crates/lance-graph-r2il/src/polyfill.rs 119 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 3 INT_S_DIV 2 IntSDiv IntSDiv|/ crates/lance-graph-r2il/src/polyfill.rs 120 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 4 INT_EQUAL 2 IntEqual IntEqual|=|Cmp::EqI32|Cmp::EqU32 crates/lance-graph-r2il/src/polyfill.rs 121 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 5 INT_S_LESS 2 IntSLess IntSLess|<|Cmp::LtI32 crates/lance-graph-r2il/src/polyfill.rs 128 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 6 INT_AND 2 IntAnd IntAnd|AND|Filter::and crates/lance-graph-r2il/src/polyfill.rs 135 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 7 INT_OR 2 IntOr IntOr|OR|Filter::or crates/lance-graph-r2il/src/polyfill.rs 142 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 8 INT_XOR 2 IntXor IntXor|XOR crates/lance-graph-r2il/src/polyfill.rs 143 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 9 INT_NOT 1 IntNot IntNot|NOT|Filter::negate crates/lance-graph-r2il/src/polyfill.rs 144 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 10 POP_COUNT 1 PopCount PopCount|COUNT|Agg::Count crates/lance-graph-r2il/src/polyfill.rs 151 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 11 SUM 2 SUM SUM|Agg::SumI32 crates/lance-graph-r2il/src/polyfill.rs 158 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 12 MIN 2 MIN MIN|Agg::MinI32 crates/lance-graph-r2il/src/polyfill.rs 159 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 13 MAX 2 MAX MAX|Agg::MaxI32 crates/lance-graph-r2il/src/polyfill.rs 160 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 14 ANY 1 ANY ANY|EXISTS|Agg::Any crates/lance-graph-r2il/src/polyfill.rs 161 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 15 ALL 1 ALL ALL|Agg::All crates/lance-graph-r2il/src/polyfill.rs 162 5c7f516673c654e3 SOURCE_STRUCTURE +0xFFFFF000 16 KEY_RUNS 2 KEY_RUNS KEY_RUNS|COUNT DISTINCT|Agg::CountDistinctOrderedU32 crates/lance-graph-r2il/src/polyfill.rs 163 5c7f516673c654e3 SOURCE_STRUCTURE diff --git a/crates/lance-graph-r2il/tests/integration/polyfill.rs b/crates/lance-graph-r2il/tests/integration/polyfill.rs index 39b333b64..d9f4f55e7 100644 --- a/crates/lance-graph-r2il/tests/integration/polyfill.rs +++ b/crates/lance-graph-r2il/tests/integration/polyfill.rs @@ -535,3 +535,135 @@ fn a_wrong_runtime_arity_for_the_lens_is_refused_by_the_table() { } ); } + +// ------------------------------------------- CG-BEH-1: method uses, read from the store + +/// The ordered algebra-word uses of method `id` of `class`, read through the +/// store's public API only: `(ordinal, word, target, args)` per effect. +fn method_uses(s: &BindStore, class: u32, id: u32) -> Vec<(usize, AlgebraRef, u8, Vec)> { + let r = s.method_ref(class, MethodId(id)).unwrap(); + let binding = s.admit(&r).unwrap(); + binding + .effects + .iter() + .enumerate() + .map(|(i, e)| (i, e.algebra, e.target, e.args.clone())) + .collect() +} + +/// One TAB-separated row per effect: class, method, sig_rev, ordinal, lens, +/// word, opcode, r2il_name, arity, target, args. +fn uses_rows(s: &BindStore, class: u32, ids: &[u32]) -> Vec { + let mut rows = Vec::new(); + for &id in ids { + let r = s.method_ref(class, MethodId(id)).unwrap(); + let sig_rev = s.admit(&r).unwrap().sig_rev.0; + for (ordinal, algebra, target, args) in method_uses(s, class, id) { + assert_eq!( + algebra.lens, POLYFILL_LENS, + "the test store declares only the polyfill lens" + ); + let w = &POLYFILL[usize::from(algebra.word)]; + let args = args + .iter() + .map(|a| match a { + Arg::Field(n) => format!("F{n}"), + Arg::Const(n) => format!("C{n}"), + }) + .collect::>() + .join(","); + rows.push(format!( + "0x{class:08X}\t{id}\t{sig_rev}\t{ordinal}\t0x{:08X}\t{}\t{}\t{}\t{}\t{target}\t{args}", + algebra.lens, algebra.word, w.opcode.0, w.aliases[0], w.arity + )); + } + } + rows +} + +/// The uses of a registered method are readable from the store, in order, +/// without preparing or running anything. A failure means the store's +/// bindings, its word index, or the table positions disagree. +#[test] +fn cg_beh_method_uses_are_read_from_the_store_in_order() { + let (s, _v) = store(vec![sum_below(), by_quack_names(2, 5, Agg::Count)]); + + let u1 = method_uses(&s, CLASS, SUM_BELOW); + assert_eq!(u1.len(), 2); + assert_eq!(u1[0].0, 0); + assert_eq!(u1[0].1.lens, POLYFILL_LENS); + assert_eq!(u1[0].1.word, pos("IntSLess")); + assert_eq!(u1[0].2, 3); + assert_eq!(u1[0].3, vec![Arg::Field(0), Arg::Const(5)]); + assert_eq!(u1[1].0, 1); + assert_eq!(u1[1].1.lens, POLYFILL_LENS); + assert_eq!(u1[1].1.word, pos("SUM")); + assert_eq!(u1[1].2, 4); + assert_eq!(u1[1].3, vec![Arg::Field(3), Arg::Field(1)]); + // Order is part of the answer. + assert_ne!(u1[0].1.word, u1[1].1.word); + + let u2 = method_uses(&s, CLASS, 2); + assert_eq!(u2.len(), 2); + assert_eq!(u2[0].1.word, pos("Cmp::LtI32")); + assert_eq!(u2[1].1.word, pos("Agg::Count")); + // The Quack names are aliases of the same positions. + assert_eq!(pos("Cmp::LtI32"), pos("IntSLess")); + assert_eq!(pos("Agg::Count"), pos("PopCount")); + + // The store's own indexes agree with the per-method reading. + let used = s.words_used(CLASS, POLYFILL_LENS).unwrap(); + assert_eq!( + used, + WideFieldMask::from_positions(&[pos("IntSLess"), pos("SUM"), pos("PopCount")]) + ); + let word = |n: &str| AlgebraRef { + lens: POLYFILL_LENS, + word: pos(n), + }; + assert_eq!(s.users_of(word("SUM")), vec![(CLASS, MethodId(SUM_BELOW))]); + // One class, methods in registration order. + assert_eq!( + s.users_of(word("IntSLess")), + vec![(CLASS, MethodId(1)), (CLASS, MethodId(2))] + ); + + // Silence twin: a word no method uses. + assert!(s.users_of(word("IntXor")).is_empty()); + assert!(!used.has(pos("IntXor"))); + assert!(used.has(pos("SUM"))); + + // A position outside the table has no arity. + assert_eq!( + s.arity(AlgebraRef { + lens: POLYFILL_LENS, + word: POLYFILL.len() as u8, + }), + None + ); + + let rows = uses_rows(&s, CLASS, &[SUM_BELOW, 2]); + assert_eq!(rows.len(), 4); + let cols: Vec<&str> = rows[0].split('\t').collect(); + assert_eq!(cols.len(), 11); + assert_eq!(cols[7], "IntSLess"); + assert_eq!(cols[10], "F0,C5"); +} + +/// Export for the CG-BEH-1 probe: with `CG_BEH_USES_OUT` set, writes the uses +/// of the two registered methods as TSV. It asserts nothing by itself. +#[test] +fn cg_beh_writes_uses_tsv_when_asked() { + let Ok(path) = std::env::var("CG_BEH_USES_OUT") else { + return; + }; + let (s, _v) = store(vec![sum_below(), by_quack_names(2, 5, Agg::Count)]); + let mut out = String::from( + "#cg-beh v1\n#schema class\tmethod\tsig_rev\tordinal\tlens\tword\topcode\tr2il_name\tarity\ttarget\targs\n", + ); + for row in uses_rows(&s, CLASS, &[SUM_BELOW, 2]) { + out.push_str(&row); + out.push('\n'); + } + std::fs::write(path, out).unwrap(); +}