From 9cfdf2a34abbf1d3907c7f7f4ace27828b2f4e20 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 21:55:37 +0000 Subject: [PATCH] glove-parity: move the class-method layer to backup (D-CMR-RESET) The method layer built on 2026-10-12 was a second algebra beside the existing folds: MethodBody lowered per call through Filter::cmp over value lanes with fresh scratch, no Compiled, no lower_fused, no resident planes. billable_hours measured 430-480 us per call against 283 us for a scalar row loop. Its hops were foreign-key reads; the graph has none. Removed: method, call, decl, gremlin and awareness modules, their tests, fixtures and examples. Cargo.toml and glove-parity.yml return to their state before this session. basis.rs and the D-XGP tests stay (another session's work; green). Exact prior state: backup/session-011BMJ-2026-10-12 (and -chain2, -sagfix, -capstone for the open PRs). Inventory with keep/remove reasons: .claude/board/entries/2026-10-12-session-reset-inventory.md. STATUS_BOARD rows D-SAG-1-*, D-CMH-G1-* marked withdrawn; the two plans carry a withdrawal note. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_011BMJnDBAfzcuQh5HeT39Rh --- .claude/board/STATUS_BOARD.md | 32 +- .../2026-10-12-session-reset-inventory.md | 56 + .claude/board/entries/README.md | 3 +- .../plans/gremlin-composable-algebra-v1.md | 2 + .claude/plans/software-awareness-graph-v1.md | 2 + .github/workflows/glove-parity.yml | 13 +- crates/lance-graph-glove-parity/Cargo.toml | 5 +- .../examples/awareness_loop.rs | 237 --- .../examples/compose_cost.rs | 167 -- .../examples/method_cost.rs | 291 ---- .../examples/method_load.rs | 103 -- .../lance-graph-glove-parity/src/awareness.rs | 1372 --------------- crates/lance-graph-glove-parity/src/call.rs | 94 - crates/lance-graph-glove-parity/src/decl.rs | 826 --------- .../lance-graph-glove-parity/src/gremlin.rs | 342 ---- crates/lance-graph-glove-parity/src/lib.rs | 5 - crates/lance-graph-glove-parity/src/method.rs | 1527 ----------------- .../tests/awareness.rs | 1133 ------------ .../tests/common/mod.rs | 117 -- .../lance-graph-glove-parity/tests/compose.rs | 418 ----- .../tests/declared.rs | 423 ----- .../tests/distinct.rs | 585 ------- .../tests/fixtures/declared.methods | 7 - .../tests/fixtures/lego.methods | 6 - .../lance-graph-glove-parity/tests/gremlin.rs | 522 ------ .../lance-graph-glove-parity/tests/method.rs | 517 ------ .../lance-graph-glove-parity/tests/ordered.rs | 265 --- .../tests/polyfill.rs | 1004 ----------- .../tests/relation.rs | 722 -------- 29 files changed, 80 insertions(+), 10716 deletions(-) create mode 100644 .claude/board/entries/2026-10-12-session-reset-inventory.md delete mode 100644 crates/lance-graph-glove-parity/examples/awareness_loop.rs delete mode 100644 crates/lance-graph-glove-parity/examples/compose_cost.rs delete mode 100644 crates/lance-graph-glove-parity/examples/method_cost.rs delete mode 100644 crates/lance-graph-glove-parity/examples/method_load.rs delete mode 100644 crates/lance-graph-glove-parity/src/awareness.rs delete mode 100644 crates/lance-graph-glove-parity/src/call.rs delete mode 100644 crates/lance-graph-glove-parity/src/decl.rs delete mode 100644 crates/lance-graph-glove-parity/src/gremlin.rs delete mode 100644 crates/lance-graph-glove-parity/src/method.rs delete mode 100644 crates/lance-graph-glove-parity/tests/awareness.rs delete mode 100644 crates/lance-graph-glove-parity/tests/common/mod.rs delete mode 100644 crates/lance-graph-glove-parity/tests/compose.rs delete mode 100644 crates/lance-graph-glove-parity/tests/declared.rs delete mode 100644 crates/lance-graph-glove-parity/tests/distinct.rs delete mode 100644 crates/lance-graph-glove-parity/tests/fixtures/declared.methods delete mode 100644 crates/lance-graph-glove-parity/tests/fixtures/lego.methods delete mode 100644 crates/lance-graph-glove-parity/tests/gremlin.rs delete mode 100644 crates/lance-graph-glove-parity/tests/method.rs delete mode 100644 crates/lance-graph-glove-parity/tests/ordered.rs delete mode 100644 crates/lance-graph-glove-parity/tests/polyfill.rs delete mode 100644 crates/lance-graph-glove-parity/tests/relation.rs diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index cd01f268b..9ad24a08c 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -4,13 +4,13 @@ Plan: `.claude/plans/software-awareness-graph-v1.md`. No new opcode, Quack varia | D-id | scope | status | gate / falsifier | |---|---|---|---| -| D-SAG-1-0 | inventory: Bible work (6-lane fixture, English KJV; no 7-language graph in code), OGAR identities, dir-sim provenance, Quack/mask-risc | In PR | every row cites code or says documented-only | -| D-SAG-1-1 | typed schema + projection of prepared methods with tiers and provenance | In PR | `tests/awareness.rs` projection test; one identity, one kind | -| D-SAG-1-2 | queries A–D through Quack; compiler verdicts as edges | In PR | answers == the programs; falsifiers 1–10, each disable-verified | -| D-SAG-1-3 | H1 candidate + tier-labelled measurement; MDL measure with counterexample | In PR | `examples/awareness_loop.rs`; MDL test | -| D-SAG-1-4 | multi-hop dependency via relation carrier / factored `EqU32Via` | Queued (needs D-CMH-G1-4) | two-hop answer == closure computed from the programs | -| D-SAG-1-5 | G1 `CALLS` edges + method-reference refusals | In PR | `method_references_are_calls_edges_and_cycles_are_the_compilers`; callers via Quack; `MethodCycle` recorded, never re-derived | -| D-SAG-1-6 | persist as a Lance dataset; source revision from git; canonical body digest | Queued | reload equals projection; toolchain change keeps digests | +| D-SAG-1-0 | inventory: Bible work (6-lane fixture, English KJV; no 7-language graph in code), OGAR identities, dir-sim provenance, Quack/mask-risc | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | every row cites code or says documented-only | +| D-SAG-1-1 | typed schema + projection of prepared methods with tiers and provenance | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | `tests/awareness.rs` projection test; one identity, one kind | +| D-SAG-1-2 | queries A–D through Quack; compiler verdicts as edges | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | answers == the programs; falsifiers 1–10, each disable-verified | +| D-SAG-1-3 | H1 candidate + tier-labelled measurement; MDL measure with counterexample | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | `examples/awareness_loop.rs`; MDL test | +| D-SAG-1-4 | multi-hop dependency via relation carrier / factored `EqU32Via` | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | two-hop answer == closure computed from the programs | +| D-SAG-1-5 | G1 `CALLS` edges + method-reference refusals | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | `method_references_are_calls_edges_and_cycles_are_the_compilers`; callers via Quack; `MethodCycle` recorded, never re-derived | +| D-SAG-1-6 | persist as a Lance dataset; source revision from git; canonical body digest | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | reload equals projection; toolchain change keeps digests | ## D-CMH-G1 — Gremlin as a composable ClassId algebra (2026-10-12) @@ -18,15 +18,15 @@ Plan: `.claude/plans/gremlin-composable-algebra-v1.md`. | D-id | scope | status | gate / falsifier | |---|---|---|---| -| D-CMH-G1-0 | vocabulary → algebra matrix, TinkerPop 3.7.7 | Shipped (#1508) | every row cites a test, a refusal or the missing capability | -| D-CMH-G1-1 | predicate methods + method references | Shipped (#1508) | composed `programs()` == hand conjunction; cycles, contract, cross-class refused | -| D-CMH-G1-2 | typed Gremlin steps → method body | Shipped (#1508) | == direct Quack program; both frontends == row oracle | -| D-CMH-G1-3 | Gremlin-shaped runtime declarations | Shipped (#1508) | loads after the build; effects refused at load | -| D-CMH-G1-4a | functional relation carrier: `out(rel)`, `out(rel).has`, `count()` | Shipped (#1514; ConflictingUniverse fix follows) | row loop over the text at n ∈ {1,64,65,4097}; 6 disable runs red; R2IL refuses (no `GATHER`) | -| D-CMH-G1-4b | edge table as the anchor (`E()`, M:N one hop, `outE/inV`) + `Fold::Distinct` (`out(rel).dedup().count()`) | In PR | bag and set == row loop over the text at n ∈ {1,64,65,4097}; unordered key lane refused (`LaneNotOrdered`); 7 disable runs red | -| D-CMH-G1-4 | rest: a second hop (blocked on the chained-hop decision), `V(parent).in(rel)` spelling, `bothE` | Queued | — | -| D-CMH-G1-5 | FirstOfRun | Queued | insufficiency proof first; Quack + R2IL vs independent oracle | -| D-CMH-G1-6 | ordered `I32` conditions + `MIN`/`MAX` folds as method steps; R2IL `MIN`/`MAX` wired | In PR | both frontends == row loop incl. empty selection; 8 disable runs red | +| D-CMH-G1-0 | vocabulary → algebra matrix, TinkerPop 3.7.7 | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | every row cites a test, a refusal or the missing capability | +| D-CMH-G1-1 | predicate methods + method references | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | composed `programs()` == hand conjunction; cycles, contract, cross-class refused | +| D-CMH-G1-2 | typed Gremlin steps → method body | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | == direct Quack program; both frontends == row oracle | +| D-CMH-G1-3 | Gremlin-shaped runtime declarations | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | loads after the build; effects refused at load | +| D-CMH-G1-4a | functional relation carrier: `out(rel)`, `out(rel).has`, `count()` | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | row loop over the text at n ∈ {1,64,65,4097}; 6 disable runs red; R2IL refuses (no `GATHER`) | +| D-CMH-G1-4b | edge table as the anchor (`E()`, M:N one hop, `outE/inV`) + `Fold::Distinct` (`out(rel).dedup().count()`) | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | bag and set == row loop over the text at n ∈ {1,64,65,4097}; unordered key lane refused (`LaneNotOrdered`); 7 disable runs red | +| D-CMH-G1-4 | rest: a second hop (blocked on the chained-hop decision), `V(parent).in(rel)` spelling, `bothE` | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | — | +| D-CMH-G1-5 | FirstOfRun | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | insufficiency proof first; Quack + R2IL vs independent oracle | +| D-CMH-G1-6 | ordered `I32` conditions + `MIN`/`MAX` folds as method steps; R2IL `MIN`/`MAX` wired | Withdrawn 2026-10-12 (backup `backup/session-011BMJ-2026-10-12`; see entry session-reset-inventory) | both frontends == row loop incl. empty selection; 8 disable runs red | ## D-NALP — `NAL::narsese()` polyfill over R2IL procedures and mask-risc (2026-10-12) diff --git a/.claude/board/entries/2026-10-12-session-reset-inventory.md b/.claude/board/entries/2026-10-12-session-reset-inventory.md new file mode 100644 index 000000000..9a9a2e5ea --- /dev/null +++ b/.claude/board/entries/2026-10-12-session-reset-inventory.md @@ -0,0 +1,56 @@ +# 2026-10-12 — Session reset: method layer moved to backup (D-CMR-RESET) + +**Status:** DONE (removal); the rebuild is OPEN. + +**Finding:** the class-method layer built on 2026-10-12 was a second algebra +beside the existing folds, not a binding over them, and it was slower than a +scalar row loop: `billable_hours` measured 430–480 µs per call against 283 µs +for the row loop (1,048,576 rows, AVX-512, release). Its hops were +foreign-key reads; the graph has none. Everything here is moved to backup and +rebuilt. + +## Where the old state lives + +| repo | branch | what | +|---|---|---| +| lance-graph | `backup/session-011BMJ-2026-10-12` | `main` at `8ae5bce4`, with the whole method layer | +| lance-graph | `backup/session-011BMJ-2026-10-12-chain2` | #1522 (`GatherChain`, `Filter::Chain`, two-hop glove) | +| lance-graph | `backup/session-011BMJ-2026-10-12-sagfix` | #1519 (SAG reprojection fixes) | +| lance-graph | `backup/session-011BMJ-2026-10-12-capstone` | #1523 (SAG docs capstone) | +| ndarray | `backup/session-011BMJ-2026-10-12` | `master` at `e726f98`, with `mask_gather_chain_u32` (#358) | + +## Removed + +| item | where | why | +|---|---|---| +| `method.rs`, `call.rs`, `decl.rs`, `gremlin.rs`, `awareness.rs` | `crates/lance-graph-glove-parity/src` | own `MethodBody` algebra, lowered per call through `Filter::cmp` over value lanes with fresh scratch (`run_quack`), no `Compiled`, no `lower_fused`, no resident planes | +| `Relation { fk }`, `ForeignLane` hops, `Cond::Reaches/Via`, edge-table anchor, `Fold::Distinct` | `method.rs` | foreign-key model; a hop is a coordinate range, not a key read | +| SAG graph (`Vec`, `retain`, `HashSet`) | `awareness.rs` | O(edges) per operation; built on the removed method layer | +| 10 test files, 2 method fixtures, 4 examples | `crates/lance-graph-glove-parity/{tests,examples}` | test the removed code | +| mask-risc `GatherChain`, Quack `Filter::Chain` | #1522 (not merged) | per-row chained key reads | +| `mask_gather_chain_u32[_under]`, `ChainHop`, `ChainTest` | ndarray (#359 removes) | same | +| `lance-graph-r2il` dependency, OGAR checkout in `glove-parity.yml` | crate `Cargo.toml`, workflow | only the method layer needed them; both restored to their state before this session | + +## Kept, and why + +| item | why | +|---|---| +| `basis.rs`, `tests/authority.rs`, `tests/canonical_front.rs` | another session's work (D-XGP-1/2); untouched, tests green | +| r2il edits: every fold reads a resident plane as its mask (`8d5a36fb`); MIN/MAX and ordered-i32 procedures (`964a95c5`); declarative bodies (`0dbb6b52`, `b61f62fa`) | woven into r2il's own procedure surface; reviewed in the rebuild, not reverted blind | +| contract: compute DAG reads all 256 field positions (`6a83f99f`) | a correctness fix in `class_view.rs`, independent of the method layer | +| build: native target-cpu, debug 0, x86_64 pinned to v3 for wheels (`5cfc8453`, `12cdfde9`) | build configuration, not a primitive | +| NAL recipe audit, CE64 plateau, nal-polyfill plan, planner revision/receipt probes | research and oracle tests, no query path | + +## The rebuild (D-CMR) + +A class method is a binding `classid × WideFieldMask → an existing fold`: one +`Compiled` mask-risc program over resident planes and coordinate ranges +(`Lowering::Range`, ternlog fusion, the fold terminals, `CountKeyRunsU32`, +Quack `lower_fused`, `fold_ranges`, `prefix_*`). A call resolves the binding +and runs the compiled program; it does not lower, allocate or interpret. Chained +hops are coordinate-range intersections inside the same program. A fold that +does not exist is reported as a gap in the fold catalogue, not built in the +method layer. + +OPEN: #1523 anchors `ProposedMethod` on `awareness::Candidate`, which this +reset removes; the anchor moves to the rebuilt binding. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index a7624e066..cdc8433cd 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. -319 entries, 2026-08-06 .. 2026-10-12. +320 entries, 2026-08-06 .. 2026-10-12. | date | entry id | finding | file | |---|---|---|---| | 2026-10-12 | `D-SAG-1` | | [2026-10-12-software-awareness-graph.md](2026-10-12-software-awareness-graph.md) | +| 2026-10-12 | `D-CMR-RESET` | method layer was a second algebra beside the folds and slower than a row loop (430–480 µs vs 283 µs); moved to backup, rebuild as bindings over existing folds | [2026-10-12-session-reset-inventory.md](2026-10-12-session-reset-inventory.md) | | 2026-10-12 | `D-CMH-G1-6` | | [2026-10-12-ordered-conditions-and-extrema.md](2026-10-12-ordered-conditions-and-extrema.md) | | 2026-10-12 | `opencog-pln-entropy-research-map` | Source inventory and falsifiable PLN/ECAN/Shannon/Moore attention comparison: entropy target, evidence overlap, graph projection, resource fairness | [2026-10-12-opencog-pln-entropy-research-map.md](2026-10-12-opencog-pln-entropy-research-map.md) | | 2026-10-12 | `nars-self-evidence-literature` | NARS internal trace versus evidence, Jøsang dependent/independent fusion and fission, and the snapshot-stable fold assumption; source-grounded red tests for merged #1496 | [2026-10-12-nars-self-evidence-literature.md](2026-10-12-nars-self-evidence-literature.md) | diff --git a/.claude/plans/gremlin-composable-algebra-v1.md b/.claude/plans/gremlin-composable-algebra-v1.md index b46faf54f..cc80c5952 100644 --- a/.claude/plans/gremlin-composable-algebra-v1.md +++ b/.claude/plans/gremlin-composable-algebra-v1.md @@ -1,5 +1,7 @@ # gremlin-composable-algebra-v1 — Gremlin steps as typed building blocks of ClassId methods (D-CMH-G1) +> **⊘ WITHDRAWN 2026-10-12.** The code this plan describes was removed from main and kept on `backup/session-011BMJ-2026-10-12`. It built class methods as a second algebra beside the existing folds, lowered per call over value lanes, and modelled hops as foreign-key reads. See `.claude/board/entries/2026-10-12-session-reset-inventory.md`. + > **Status:** MEASURED for the slice in §2 (code: `crates/lance-graph-glove-parity/src/{method,gremlin,decl}.rs`, > tests `tests/{compose,gremlin}.rs`). The vocabulary matrix (§4) is ANALYSIS against code and tests, > one row per step family. Stacked on #1507 (runtime declarations); independent of #1506. diff --git a/.claude/plans/software-awareness-graph-v1.md b/.claude/plans/software-awareness-graph-v1.md index e5783aaae..a599840e5 100644 --- a/.claude/plans/software-awareness-graph-v1.md +++ b/.claude/plans/software-awareness-graph-v1.md @@ -1,5 +1,7 @@ # D-SAG-1 — Software-awareness graph: the methods, programs and evidence of lance-graph as graph rows +> **⊘ WITHDRAWN 2026-10-12.** The code this plan describes was removed from main and kept on `backup/session-011BMJ-2026-10-12`. It built class methods as a second algebra beside the existing folds, lowered per call over value lanes, and modelled hops as foreign-key reads. See `.claude/board/entries/2026-10-12-session-reset-inventory.md`. + **Status:** MEASURED for one concept's methods (#1504's `BillableWorkEntry` set). Code: `crates/lance-graph-glove-parity/src/awareness.rs`, tests `tests/awareness.rs` (19), example `examples/awareness_loop.rs`. diff --git a/.github/workflows/glove-parity.yml b/.github/workflows/glove-parity.yml index 450988465..33bf786b3 100644 --- a/.github/workflows/glove-parity.yml +++ b/.github/workflows/glove-parity.yml @@ -7,7 +7,6 @@ on: - 'crates/lance-graph-ontology/**' - 'crates/lance-graph-quack/**' - 'crates/lance-graph-mask-risc/**' - - 'crates/lance-graph-r2il/**' - 'crates/lance-graph-contract/**' - '.github/workflows/glove-parity.yml' workflow_dispatch: @@ -27,21 +26,11 @@ jobs: ref: master path: ndarray persist-credentials: false - # lance-graph-r2il (method calls through R2IL) path-deps ogar-loco and - # ogar-r2il from the OGAR sibling. - - uses: actions/checkout@v4 - with: - repository: AdaWorldAPI/OGAR - ref: main - path: OGAR - persist-credentials: false - uses: dtolnay/rust-toolchain@stable with: components: rustfmt, clippy - name: Record tested substrate - run: | - git -C ndarray rev-parse HEAD - git -C OGAR rev-parse HEAD + run: git -C ndarray rev-parse HEAD - name: Format run: cargo +stable fmt --manifest-path lance-graph/crates/lance-graph-glove-parity/Cargo.toml -- --check - name: Lint diff --git a/crates/lance-graph-glove-parity/Cargo.toml b/crates/lance-graph-glove-parity/Cargo.toml index 4d78dcac3..183a61cc4 100644 --- a/crates/lance-graph-glove-parity/Cargo.toml +++ b/crates/lance-graph-glove-parity/Cargo.toml @@ -17,12 +17,9 @@ lance-graph-quack = { path = "../lance-graph-quack" } # The capability authority (`OgarAuthority`) and OGAR's promoted classes and # `ClassView`, for the hot-plugs and the canonical field basis (`basis`). lance-graph-ogar = { path = "../lance-graph-ogar" } -# Executes a method through both frontends: Quack (above) and R2IL. R2IL -# path-deps the OGAR sibling (`ogar-loco`, `ogar-r2il`), so CI checks it out. -lance-graph-mask-risc = { path = "../lance-graph-mask-risc" } -lance-graph-r2il = { path = "../lance-graph-r2il" } [dev-dependencies] +lance-graph-mask-risc = { path = "../lance-graph-mask-risc" } lance-graph-sap = { path = "../lance-graph-sap" } # mask-risc names ndarray by its canonical git coordinate; point it at the diff --git a/crates/lance-graph-glove-parity/examples/awareness_loop.rs b/crates/lance-graph-glove-parity/examples/awareness_loop.rs deleted file mode 100644 index 69a435afa..000000000 --- a/crates/lance-graph-glove-parity/examples/awareness_loop.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! D-SAG-1, the loop run once: project prepared methods into the -//! software-awareness graph, ask it which programs are H1 fusion candidates, -//! time each candidate against a fused scalar reference on THIS machine, and -//! record the timings as tier-labelled evidence. -//! -//! ```text -//! cargo run --release --example awareness_loop [rows] [reps] -//! ``` -//! -//! The scalar loop is a reference for what one pass costs, not a fused -//! mask-risc terminal: no such terminal exists (`FusedFold` is `Count`/`Any`), -//! so the graph records the candidate's obligations open and the gap as a -//! measured ratio, never as an achieved speed-up. -use std::hint::black_box; -use std::time::Instant; - -use lance_graph_glove_parity::awareness::{CpuTier, Measurement, SoftwareGraph}; -use lance_graph_glove_parity::basis::{canonical_fields, FieldMap, Grade, SAP_FIELDS}; -use lance_graph_glove_parity::method::{ - Cond, EffectClass, Fold, Glove, Law, MethodBody, MethodKey, MethodSpec, MethodTable, ReadingId, - SchemaSeal, Universe, -}; -use lance_graph_mask_risc::{execute_into, Foreign, LaneRef, Out, Planes, Program, Scratch, Value}; -use lance_graph_ogar::ogar_vocab::class_ids::BILLABLE_WORK_ENTRY; -use lance_graph_sap::bind::{CatsBatch, BILLABLE, BILLING, HOURS}; -use lance_graph_sap::binder::{live_words, CatsBinder, TABLE}; -use lance_graph_sap::schema::{CatsSchema, FIELD_COUNT}; - -const INDICATORS: [&str; 4] = ["Billable", "Non-Billable", "Internal Cost", "Unknown"]; -const HOURS_TEXT: [&str; 3] = ["8.50", "0.125", "2"]; -const BILLABLE_TRUE: Cond = Cond::Eq { - field: "billable", - literal: "true", -}; - -fn batch(n: usize) -> CatsBatch { - let base: Vec> = include_str!("../../lance-graph-sap/fixtures/cats.txt") - .lines() - .map(|v| if v == "\\N" { None } else { Some(v) }) - .collect(); - let mut input: [Vec>; FIELD_COUNT] = std::array::from_fn(|i| vec![base[i]; n]); - for i in 0..n { - input[BILLING][i] = Some(INDICATORS[(i * 7 + i / 3) % 4]); - input[HOURS][i] = Some(HOURS_TEXT[i % 3]); - } - CatsBatch::bind( - CatsSchema::new(42, 0), - std::array::from_fn(|i| input[i].as_slice()), - ) - .unwrap() -} - -fn tier() -> CpuTier { - CpuTier { - target: format!( - "compiled avx512f={} avx2={}", - cfg!(target_feature = "avx512f"), - cfg!(target_feature = "avx2") - ), - #[cfg(target_arch = "x86_64")] - avx512f: std::is_x86_feature_detected!("avx512f"), - #[cfg(not(target_arch = "x86_64"))] - avx512f: false, - } -} - -fn main() { - let mut args = std::env::args().skip(1); - let n: usize = args.next().map_or(1 << 20, |s| s.parse().unwrap()); - let reps: usize = args.next().map_or(51, |s| s.parse().unwrap()); - let rev = std::env::var("SAG_REV").unwrap_or_else(|_| "worktree".into()); - - let batch = batch(n); - let cats = CatsBinder::new(&batch); - let basis = canonical_fields(); - // FIXTURE (as in `method_cost.rs`): SAP's real claim on `duration` is - // `Hypothesized`; under it `billable_hours` is refused, and the graph - // records that refusal instead of a program. - let claims: Vec = SAP_FIELDS - .iter() - .map(|m| FieldMap { - grade: if m.canonical == "duration" { - Grade::Exact - } else { - m.grade - }, - ..*m - }) - .collect(); - let spec = |key, folds: &'static [Fold<'static>]| MethodSpec { - key: MethodKey(key), - concept: BILLABLE_WORK_ENTRY, - schema: SchemaSeal::of(&basis), - reading: ReadingId(1), - effect: EffectClass::Pure, - body: MethodBody::Query { - filter: &[BILLABLE_TRUE], - folds, - law: Law::Values, - }, - }; - let specs = [ - ("count_billable", spec([0xB1; 16], &[Fold::Count])), - ("billable_hours", spec([0xB2; 16], &[Fold::Sum("duration")])), - ]; - let glove = Glove { - concept: BILLABLE_WORK_ENTRY, - basis: &basis, - claims: &claims, - binder: &cats, - table: TABLE, - reading: ReadingId(1), - universe: Universe(42), - relations: &[], - }; - let table = MethodTable::prepare(&glove, &specs); - let mut g = SoftwareGraph::new(); - g.project( - &table, - &specs, - &claims, - "crates/lance-graph-glove-parity/examples/awareness_loop.rs", - &rev, - ); - println!( - "graph: {} nodes, {} edges, rev {rev}", - g.nodes().len(), - g.edges().len() - ); - println!( - "methods using MaskedSumI32 (Quack over the edge table): {:?}", - g.methods_using("MaskedSumI32").unwrap() - ); - - let mut programs: Vec<(String, Program)> = Vec::new(); - for (_, s) in &specs { - if let Some(Ok(p)) = table.get(s.key) { - for (i, q) in p.programs().iter().enumerate() { - let hex: String = s.key.0.iter().map(|b| format!("{b:02x}")).collect(); - programs.push((format!("program:{hex}#{i}"), q.clone())); - } - } - } - let refs: Vec<(String, &Program)> = programs.iter().map(|(i, p)| (i.clone(), p)).collect(); - let candidates = g.fusion_candidates(&refs); - println!("H1 candidates: {}", candidates.len()); - - let live = live_words(n); - let lanes = batch.lanes(); - let planes = Planes { - n_rows: n, - masks: &[live.as_slice()], - lanes: &lanes, - }; - let (LaneRef::U32(bill), LaneRef::I32(hours)) = (lanes[BILLABLE], lanes[HOURS]) else { - panic!("lane kinds") - }; - let scalar = || -> i64 { - bill.iter() - .zip(hours) - .filter(|(b, _)| **b == 1) - .map(|(_, h)| i64::from(*h)) - .sum() - }; - let t = tier(); - for c in &candidates { - let lance_graph_glove_parity::awareness::CandidateKind::FusePredicateIntoSum { program } = - &c.kind - else { - continue; - }; - let p = &programs.iter().find(|(i, _)| i == program).unwrap().1; - let mut scratch = Scratch::for_program(p, n).unwrap(); - let mut run = - || match execute_into(p, &planes, &Foreign::NONE, &mut scratch, Out::None).unwrap() { - Value::SumI64(s) => s, - v => panic!("{v:?}"), - }; - // Both paths must give the same answer before either is timed. - assert_eq!(run(), scalar(), "the reference computes another answer"); - let (mut a, mut b) = (Vec::new(), Vec::new()); - for r in 0..reps { - for k in 0..2 { - let t0 = Instant::now(); - if (r + k) % 2 == 0 { - black_box(run()); - a.push(t0.elapsed().as_nanos() as f64); - } else { - black_box(scalar()); - b.push(t0.elapsed().as_nanos() as f64); - } - } - } - a.sort_by(f64::total_cmp); - b.sort_by(f64::total_cmp); - let (ma, mb) = (a[a.len() / 2], b[b.len() / 2]); - g.measure(Measurement { - program: program.clone(), - rows: n, - tier: t.clone(), - median_ns: ma, - }); - g.measure(Measurement { - program: format!("reference:scalar_fused:{program}"), - rows: n, - tier: t.clone(), - median_ns: mb, - }); - println!("candidate {program} of {:?}", c.methods); - for o in &c.obligations { - println!( - " obligation: {} — {}", - o.what, - o.discharged.as_deref().unwrap_or("OPEN") - ); - } - println!( - " tier {:?}; rows {n}; reps {reps}; median program {:.1} µs, scalar reference {:.1} µs, ratio {:.2}", - t, - ma / 1e3, - mb / 1e3, - ma / mb - ); - println!(" recorded cost on this tier: {:?}", g.cost(program, n, &t)); - let other = CpuTier { - target: "x86-64-v3".into(), - avx512f: t.avx512f, - }; - if other != t { - println!( - " asked for another tier ({}): {:?}", - other.target, - g.cost(program, n, &other) - ); - } - } -} diff --git a/crates/lance-graph-glove-parity/examples/compose_cost.rs b/crates/lance-graph-glove-parity/examples/compose_cost.rs deleted file mode 100644 index eb15682b3..000000000 --- a/crates/lance-graph-glove-parity/examples/compose_cost.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! D-CMH-G1 cost: what loading a composed method from text costs, cold, and -//! what calling it costs, warm, against the same query run directly. -//! -//! Every timed path is asserted to give the same answer before it is timed. -//! Run: `cargo run --release --example compose_cost -- [rows] [reps]` -use std::hint::black_box; -use std::time::Instant; - -use lance_graph_glove_parity::basis::{canonical_fields, FieldMap, Grade, SAP_FIELDS}; -use lance_graph_glove_parity::decl::parse_decls; -use lance_graph_glove_parity::method::{ - Answer, FoldValue, Frontend, Glove, MethodTable, ReadingId, Universe, -}; -use lance_graph_mask_risc::{execute_into, Foreign, LaneRef, Out, Planes, Scratch, Value}; -use lance_graph_ogar::ogar_vocab::class_ids::BILLABLE_WORK_ENTRY; -use lance_graph_sap::bind::{CatsBatch, BILLABLE, BILLING, HOURS}; -use lance_graph_sap::binder::{live_words, CatsBinder, TABLE}; -use lance_graph_sap::schema::{CatsSchema, FIELD_COUNT}; - -const INDICATORS: [&str; 4] = ["Billable", "Non-Billable", "Internal Cost", "Unknown"]; -const HOURS_TEXT: [&str; 3] = ["8.50", "0.125", "2"]; - -fn batch(n: usize) -> CatsBatch { - let base: Vec> = include_str!("../../lance-graph-sap/fixtures/cats.txt") - .lines() - .map(|v| if v == "\\N" { None } else { Some(v) }) - .collect(); - let mut input: [Vec>; FIELD_COUNT] = std::array::from_fn(|i| vec![base[i]; n]); - for i in 0..n { - input[BILLING][i] = Some(INDICATORS[(i * 7 + i / 3) % 4]); - input[HOURS][i] = Some(HOURS_TEXT[i % 3]); - } - CatsBatch::bind( - CatsSchema::new(42, 0), - std::array::from_fn(|i| input[i].as_slice()), - ) - .unwrap() -} - -fn median_min(mut v: Vec) -> (f64, f64) { - v.sort_unstable(); - (v[v.len() / 2] as f64 / 1e3, v[0] as f64 / 1e3) -} - -fn main() { - let mut args = std::env::args().skip(1); - let n: usize = args.next().map_or(1 << 20, |s| s.parse().unwrap()); - let reps: usize = args.next().map_or(101, |s| s.parse().unwrap()); - let text = include_str!("../tests/fixtures/lego.methods"); - - let batch = batch(n); - let cats = CatsBinder::new(&batch); - let basis = canonical_fields(); - // FIXTURE: proves SAP's hypothesized `duration` claim, as the tests do. - let claims: Vec = SAP_FIELDS - .iter() - .map(|m| FieldMap { - grade: if m.canonical == "duration" { - Grade::Exact - } else { - m.grade - }, - ..*m - }) - .collect(); - let glove = Glove { - concept: BILLABLE_WORK_ENTRY, - basis: &basis, - claims: &claims, - binder: &cats, - table: TABLE, - reading: ReadingId(1), - universe: Universe(42), - relations: &[], - }; - let live = live_words(n); - let lanes = batch.lanes(); - let planes = Planes { - n_rows: n, - masks: &[live.as_slice()], - lanes: &lanes, - }; - let (LaneRef::U32(bill), LaneRef::I32(hours)) = (lanes[BILLABLE], lanes[HOURS]) else { - panic!("lane kinds") - }; - let want: i64 = bill - .iter() - .zip(hours) - .filter(|(b, _)| **b == 1) - .map(|(_, h)| i64::from(*h)) - .sum(); - - // Cold: text -> declarations -> prepared table (3 methods, 2 references). - let mut parse = Vec::new(); - let mut prep = Vec::new(); - for _ in 0..reps { - let t0 = Instant::now(); - let d = black_box(parse_decls(text).unwrap()); - parse.push(t0.elapsed().as_nanos()); - let t1 = Instant::now(); - black_box(MethodTable::prepare_with(&glove, &[], &d)); - prep.push(t1.elapsed().as_nanos()); - } - let decls = parse_decls(text).unwrap(); - let t = MethodTable::prepare_with(&glove, &[], &decls); - let key = t.resolve("billable_hours").unwrap(); - let expect = Answer::Values(vec![FoldValue::Scalar(want)]); - for f in [Frontend::Quack, Frontend::R2il] { - assert_eq!(t.call(key, &planes, Universe(42), f).unwrap(), expect); - } - // Direct: the prepared method's own program, run without the method - // layer. The composition test pins that this is also the program a - // direct Quack query lowers to. - let program = t.get(key).unwrap().as_ref().unwrap().programs()[0].clone(); - let mut scratch = Scratch::for_program(&program, n).unwrap(); - let mut direct = || match execute_into( - &program, - &planes, - &Foreign::NONE, - &mut scratch, - Out::None, - ) - .unwrap() - { - Value::SumI64(s) => s, - v => panic!("{v:?}"), - }; - assert_eq!(direct(), want); - - let mut warm: [Vec; 3] = Default::default(); - for r in 0..reps { - for k in 0..3 { - let path = (r + k) % 3; - let t0 = Instant::now(); - match path { - 0 => { - black_box(t.call(key, &planes, Universe(42), Frontend::Quack).unwrap()); - } - 1 => { - black_box(t.call(key, &planes, Universe(42), Frontend::R2il).unwrap()); - } - _ => { - black_box(direct()); - } - } - warm[path].push(t0.elapsed().as_nanos()); - } - } - println!( - "tier: avx512f={} avx2={} (compiled target features)", - cfg!(target_feature = "avx512f"), - cfg!(target_feature = "avx2") - ); - println!("rows {n}, reps {reps}, answer {want}"); - let row = |name: &str, (med, min): (f64, f64)| { - println!("{name:<40} median {med:>10.2} µs min {min:>10.2} µs"); - }; - row("cold: parse 3 declarations", median_min(parse)); - row( - "cold: prepare_with (resolve+inline+lower)", - median_min(prep), - ); - let [q, r, d] = warm; - row("warm: composed billable_hours, Quack", median_min(q)); - row("warm: composed billable_hours, R2IL", median_min(r)); - row("warm: direct program (same Program)", median_min(d)); -} diff --git a/crates/lance-graph-glove-parity/examples/method_cost.rs b/crates/lance-graph-glove-parity/examples/method_cost.rs deleted file mode 100644 index ae2d89f51..000000000 --- a/crates/lance-graph-glove-parity/examples/method_cost.rs +++ /dev/null @@ -1,291 +0,0 @@ -//! Experiment E: what the declarative method interface costs. -//! -//! Measures, on one SAP CATS batch, separately: -//! -//! 1. cold: `MethodTable::prepare` (binding) and `resolve_call` (text → key); -//! 2. warm: `MethodTable::call`, Quack and R2IL frontends; -//! 3. direct Quack: the same query, lowered once, executed; -//! 4. scalar reference: a row loop over the same lanes. -//! -//! Every timed path is asserted to give the same answer before it is timed. -//! Allocations are counted by a wrapping global allocator. -//! -//! Run: `cargo run --release --example method_cost -- [rows] [reps]` -use std::alloc::{GlobalAlloc, Layout, System}; -use std::hint::black_box; -use std::sync::atomic::{AtomicU64, Ordering::Relaxed}; -use std::time::Instant; - -use lance_graph_glove_parity::basis::{canonical_fields, FieldMap, Grade, SAP_FIELDS}; -use lance_graph_glove_parity::call::resolve_call; -use lance_graph_glove_parity::method::{ - Answer, Cond, EffectClass, Fold, FoldValue, Frontend, Glove, Law, MethodBody, MethodKey, - MethodSpec, MethodTable, ReadingId, SchemaSeal, Universe, -}; -use lance_graph_mask_risc::{execute_into, Foreign, LaneRef, Out, Planes, Scratch, Value}; -use lance_graph_ogar::ogar_vocab::class_ids::BILLABLE_WORK_ENTRY; -use lance_graph_quack::bind::{table, Binder}; -use lance_graph_quack::{lower, Agg, Query}; -use lance_graph_sap::bind::{CatsBatch, BILLABLE, BILLING, HOURS}; -use lance_graph_sap::binder::{live_words, CatsBinder, TABLE}; -use lance_graph_sap::schema::{CatsSchema, FIELD_COUNT}; - -struct Counting; -static ALLOCS: AtomicU64 = AtomicU64::new(0); -static BYTES: AtomicU64 = AtomicU64::new(0); - -// SAFETY: forwards every call to `System` unchanged; only counts. -unsafe impl GlobalAlloc for Counting { - unsafe fn alloc(&self, l: Layout) -> *mut u8 { - ALLOCS.fetch_add(1, Relaxed); - BYTES.fetch_add(l.size() as u64, Relaxed); - // SAFETY: same contract as the caller's. - unsafe { System.alloc(l) } - } - unsafe fn dealloc(&self, p: *mut u8, l: Layout) { - // SAFETY: same contract as the caller's. - unsafe { System.dealloc(p, l) } - } -} - -#[global_allocator] -static A: Counting = Counting; - -const INDICATORS: [&str; 4] = ["Billable", "Non-Billable", "Internal Cost", "Unknown"]; -const HOURS_TEXT: [&str; 3] = ["8.50", "0.125", "2"]; - -fn batch(n: usize) -> CatsBatch { - let base: Vec> = include_str!("../../lance-graph-sap/fixtures/cats.txt") - .lines() - .map(|v| if v == "\\N" { None } else { Some(v) }) - .collect(); - let mut input: [Vec>; FIELD_COUNT] = std::array::from_fn(|i| vec![base[i]; n]); - for i in 0..n { - input[BILLING][i] = Some(INDICATORS[(i * 7 + i / 3) % 4]); - input[HOURS][i] = Some(HOURS_TEXT[i % 3]); - } - CatsBatch::bind( - CatsSchema::new(42, 0), - std::array::from_fn(|i| input[i].as_slice()), - ) - .unwrap() -} - -/// Median of `reps` timings of `f`, in nanoseconds, plus allocations per call. -fn time(reps: usize, mut f: impl FnMut() -> T) -> (u128, f64, f64) { - let mut v = Vec::with_capacity(reps); - let (a0, b0) = (ALLOCS.load(Relaxed), BYTES.load(Relaxed)); - for _ in 0..reps { - let t = Instant::now(); - black_box(f()); - v.push(t.elapsed().as_nanos()); - } - let (a1, b1) = (ALLOCS.load(Relaxed), BYTES.load(Relaxed)); - v.sort_unstable(); - ( - v[reps / 2], - (a1 - a0) as f64 / reps as f64, - (b1 - b0) as f64 / reps as f64, - ) -} - -fn main() { - let mut args = std::env::args().skip(1); - let n: usize = args.next().map_or(1 << 20, |s| s.parse().unwrap()); - let reps: usize = args.next().map_or(51, |s| s.parse().unwrap()); - - let batch = batch(n); - let cats = CatsBinder::new(&batch); - let basis = canonical_fields(); - let claims: Vec = SAP_FIELDS - .iter() - .map(|m| FieldMap { - grade: if m.canonical == "duration" { - Grade::Exact - } else { - m.grade - }, - ..*m - }) - .collect(); - let spec = MethodSpec { - key: MethodKey([0xB2; 16]), - concept: BILLABLE_WORK_ENTRY, - schema: SchemaSeal::of(&basis), - reading: ReadingId(1), - effect: EffectClass::Pure, - body: MethodBody::Query { - filter: &[Cond::Eq { - field: "billable", - literal: "true", - }], - folds: &[Fold::Sum("duration")], - law: Law::Values, - }, - }; - let glove = Glove { - concept: BILLABLE_WORK_ENTRY, - basis: &basis, - claims: &claims, - binder: &cats, - table: TABLE, - reading: ReadingId(1), - universe: Universe(42), - relations: &[], - }; - let specs = [("billable_hours", spec)]; - let sql = "SELECT billable_work_entry::billable_hours(cats) FROM cats"; - - let live = live_words(n); - let lanes = batch.lanes(); - let planes = Planes { - n_rows: n, - masks: &[live.as_slice()], - lanes: &lanes, - }; - - // The answer every path must give. - let (LaneRef::U32(bill), LaneRef::I32(hours)) = (lanes[BILLABLE], lanes[HOURS]) else { - panic!("lane kinds") - }; - let scalar = || -> i64 { - bill.iter() - .zip(hours) - .filter(|(b, _)| **b == 1) - .map(|(_, h)| i64::from(*h)) - .sum() - }; - let want = scalar(); - - let t = MethodTable::prepare(&glove, &specs); - let key = resolve_call(sql, &t).unwrap().method; - let expect = Answer::Values(vec![FoldValue::Scalar(want)]); - for f in [Frontend::Quack, Frontend::R2il] { - assert_eq!(t.call(key, &planes, Universe(42), f).unwrap(), expect); - } - - let tid = cats.table(TABLE).unwrap(); - let hours_col = cats.field(tid, "hours_logged").unwrap().col; - let direct = Query { - filter: table(TABLE) - .where_eq("billable", "true") - .count() - .bind(&cats) - .unwrap() - .filter, - agg: Agg::SumI32(hours_col), - }; - let program = lower(&direct).unwrap(); - // The method compiles to the very program a direct query does. - let prepared = t.get(key).unwrap().as_ref().unwrap(); - let method_q = prepared.queries(); - // A call executes the program lowered at preparation, and it is the - // very program the direct query lowers to. - assert_eq!( - prepared.programs(), - std::slice::from_ref(&program), - "same lowered program" - ); - let mut scratch = Scratch::for_program(&program, n).unwrap(); - let mut run_direct = || match execute_into( - &program, - &planes, - &Foreign::NONE, - &mut scratch, - Out::None, - ) - .unwrap() - { - Value::SumI64(s) => s, - v => panic!("{v:?}"), - }; - assert_eq!(run_direct(), want); - - println!( - "tier: avx512f={} avx2={} (compiled target features, not the host)", - cfg!(target_feature = "avx512f"), - cfg!(target_feature = "avx2") - ); - println!("rows {n}, reps {reps} (median), answer {want}"); - println!( - "lowered program: {} mask ops + terminal, {} scratch slots", - program.ops.len(), - program.scratch_slots - ); - let row = |name: &str, (ns, a, b): (u128, f64, f64)| { - println!( - "{name:<34} {:>12.3} µs {:>8.2} ns/row {:>7.1} allocs {:>11.0} B alloc", - ns as f64 / 1e3, - ns as f64 / n as f64, - a, - b - ); - }; - row( - "cold: prepare (bind 1 method)", - time(reps, || MethodTable::prepare(&glove, &specs)), - ); - row( - "cold: resolve_call (text → key)", - time(reps, || resolve_call(sql, &t).unwrap()), - ); - row( - "cold: lower (bound query → program)", - time(reps, || lower(&method_q[0]).unwrap()), - ); - // Hot paths interleaved: each rep runs all four in a rotating order, - // so machine drift lands on every path alike. Median and minimum. - let mut samples: [Vec; 4] = Default::default(); - for r in 0..reps { - for k in 0..4 { - let path = (r + k) % 4; - let t0 = Instant::now(); - match path { - 0 => { - black_box(t.call(key, &planes, Universe(42), Frontend::Quack).unwrap()); - } - 1 => { - black_box(t.call(key, &planes, Universe(42), Frontend::R2il).unwrap()); - } - 2 => { - black_box(run_direct()); - } - _ => { - black_box(scalar()); - } - } - samples[path].push(t0.elapsed().as_nanos()); - } - } - let names = [ - "warm: method call, Quack", - "warm: method call, R2IL", - "direct Quack (lowered once)", - "scalar reference (row loop)", - ]; - for (name, v) in names.iter().zip(samples.iter_mut()) { - v.sort_unstable(); - println!( - "{name:<34} median {:>8.1} µs min {:>8.1} µs {:>6.3} ns/row (min)", - v[v.len() / 2] as f64 / 1e3, - v[0] as f64 / 1e3, - v[0] as f64 / n as f64 - ); - } - let (_, a, b) = time(reps, || { - t.call(key, &planes, Universe(42), Frontend::Quack).unwrap() - }); - println!("allocs per warm Quack call: {a:.1} ({b:.0} B)"); - let (_, a, b) = time(reps, || { - t.call(key, &planes, Universe(42), Frontend::R2il).unwrap() - }); - println!("allocs per warm R2IL call: {a:.1} ({b:.0} B)"); - let (_, a, b) = time(reps, &mut run_direct); - println!("allocs per direct call: {a:.1} ({b:.0} B)"); - println!( - "lane bytes read per call: billable {} + hours {} + live {}", - n * 4, - n * 4, - live.len() * 8 - ); -} diff --git a/crates/lance-graph-glove-parity/examples/method_load.rs b/crates/lance-graph-glove-parity/examples/method_load.rs deleted file mode 100644 index f15969bc3..000000000 --- a/crates/lance-graph-glove-parity/examples/method_load.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Load method declarations from a file at run time and call them. -//! -//! Build once, then run against any declarations file; a new method needs a -//! new line of text, not a rebuild: -//! -//! ```text -//! cargo build --release --example method_load -//! target/release/examples/method_load tests/fixtures/declared.methods 4097 -//! ``` -//! -//! The glove is SAP CATS with SAP's real claims, so a method reading -//! `duration` (claimed `Hypothesized`) is refused, and the refusal is printed. -//! Each method runs on both frontends, which must agree; the independent -//! oracle lives in `tests/declared.rs`. -use std::time::Instant; - -use lance_graph_glove_parity::basis::{canonical_fields, SAP_FIELDS}; -use lance_graph_glove_parity::decl::parse_decls; -use lance_graph_glove_parity::method::{Frontend, Glove, MethodTable, ReadingId, Universe}; -use lance_graph_mask_risc::Planes; -use lance_graph_ogar::ogar_vocab::class_ids::BILLABLE_WORK_ENTRY; -use lance_graph_sap::bind::{CatsBatch, BILLING, HOURS}; -use lance_graph_sap::binder::{live_words, CatsBinder, TABLE}; -use lance_graph_sap::schema::{CatsSchema, FIELD_COUNT}; - -const INDICATORS: [&str; 4] = ["Billable", "Non-Billable", "Internal Cost", "Unknown"]; -const HOURS_TEXT: [&str; 3] = ["8.50", "0.125", "2"]; - -fn batch(n: usize) -> CatsBatch { - let base: Vec> = include_str!("../../lance-graph-sap/fixtures/cats.txt") - .lines() - .map(|v| if v == "\\N" { None } else { Some(v) }) - .collect(); - let mut input: [Vec>; FIELD_COUNT] = std::array::from_fn(|i| vec![base[i]; n]); - for i in 0..n { - input[BILLING][i] = Some(INDICATORS[(i * 7 + i / 3) % 4]); - input[HOURS][i] = Some(HOURS_TEXT[i % 3]); - } - CatsBatch::bind( - CatsSchema::new(42, 0), - std::array::from_fn(|i| input[i].as_slice()), - ) - .unwrap() -} - -fn main() { - let mut args = std::env::args().skip(1); - let path = args.next().expect("usage: method_load [rows]"); - let n: usize = args.next().map_or(4097, |s| s.parse().unwrap()); - - let t0 = Instant::now(); - let text = std::fs::read_to_string(&path).expect("read declarations"); - let decls = match parse_decls(&text) { - Ok(d) => d, - Err(e) => { - println!("refused: {e:?}"); - std::process::exit(2); - } - }; - let parsed = t0.elapsed(); - - let batch = batch(n); - let cats = CatsBinder::new(&batch); - let basis = canonical_fields(); - let glove = Glove { - concept: BILLABLE_WORK_ENTRY, - basis: &basis, - claims: SAP_FIELDS, - binder: &cats, - table: TABLE, - reading: ReadingId(1), - universe: Universe(42), - relations: &[], - }; - let t1 = Instant::now(); - let table = MethodTable::prepare_with(&glove, &[], &decls); - let prepared = t1.elapsed(); - // Each declaration's label and key, taken before the text and the - // declarations are dropped; calls go by key, so a duplicate label (which - // `resolve` leaves unnamed on purpose) is still reported, not a panic. - let methods: Vec<(String, _)> = decls.iter().map(|d| (d.label.clone(), d.key)).collect(); - drop((decls, text)); - - let live = live_words(n); - let lanes = batch.lanes(); - let planes = Planes { - n_rows: n, - masks: &[live.as_slice()], - lanes: &lanes, - }; - println!("{path}: parsed in {parsed:?}, prepared in {prepared:?}, rows {n}"); - for (label, key) in methods { - let q = table.call(key, &planes, Universe(42), Frontend::Quack); - let r = table.call(key, &planes, Universe(42), Frontend::R2il); - match (q, r) { - (Ok(q), Ok(r)) => { - assert_eq!(q, r, "{label}: frontends disagree"); - println!(" {label}: {q:?}"); - } - (Err(e), _) | (_, Err(e)) => println!(" {label}: refused {e:?}"), - } - } -} diff --git a/crates/lance-graph-glove-parity/src/awareness.rs b/crates/lance-graph-glove-parity/src/awareness.rs deleted file mode 100644 index 3189d130a..000000000 --- a/crates/lance-graph-glove-parity/src/awareness.rs +++ /dev/null @@ -1,1372 +0,0 @@ -//! D-SAG-1: a software-awareness graph — the methods of a concept, their -//! compiled programs and the evidence about them, as typed graph rows queried -//! through Quack. -//! -//! A [`SoftwareGraph`] is a PROJECTION. It is rebuilt from a [`MethodTable`] -//! and the specs it was prepared from; it is never an authority the compiler -//! reads. Every edge says how it is known ([`Tier`]) and where it came from -//! ([`Provenance`]): -//! -//! | tier | written by | example | -//! |---|---|---| -//! | [`Tier::Declared`] | the spec as written | `billable_hours READS_FIELD duration` | -//! | [`Tier::Derived`] | source read at projection (claims, compute edges) | `duration DEPENDS_ON_CLAIM hypothesized` | -//! | [`Tier::Compiled`] | `MethodTable::prepare` and `lower` | `program USES_PRIMITIVE MaskedSumI32` | -//! | [`Tier::Verified`] | an executed oracle, under a [`Certificate`] | `billable_hours VERIFIED_BY …` | -//! | [`Tier::Hypothesis`] | a person or an analysis, unproven | `iam_reconcile MAY_REQUIRE FirstOfRun` | -//! -//! Validation is the compiler's: a refusal is the [`PrepareError`] the table -//! holds, recorded as a [`Rel::RefusedFor`] edge. The graph adds no rule that -//! could disagree with it. -//! -//! The edge table is a Quack table: [`SoftwareGraph`] implements [`Binder`], -//! so a dependency question is a `Draft` that binds, lowers and runs on -//! mask-risc like any other query (see [`SoftwareGraph::methods_using`]). -//! Nothing here runs on a method's warm path. -//! -//! Plan: `.claude/plans/software-awareness-graph-v1.md`. -use std::fmt::Write as _; - -use lance_graph_contract::class_view::{compute_dag_is_acyclic, ComputeEdge}; -use lance_graph_mask_risc::{ - execute_into, materialize_rows, words_for, ExecError, Foreign, LaneRef, Lowering, MaskOp, - Operand, Planes, Pred, Program, Scratch, Terminal, -}; -use lance_graph_quack::bind::{BindError, Binder, BoundField, Draft, FieldKind, TableId}; -use lance_graph_quack::{lower, Agg, Col, LowerError, Mask, Query}; - -use crate::basis::{FieldMap, Grade}; -use crate::method::{ - Cond, Fold, Law, MethodBody, MethodKey, MethodRef, MethodSpec, MethodTable, Order, - PrepareError, ReadingId, SchemaSeal, -}; - -/// What a node is. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Kind { - Class, - Method, - /// A canonical field of one concept under one schema seal. - Field, - /// One condition of a body, under its reading. - Cond, - /// One fold of a body, under its reading. - Fold, - Law, - /// One lowered program of a prepared method. - Program, - /// A mask-risc op or terminal kind. - Primitive, - /// How mask-risc executes a program ([`Program::lowering`]). - Lowering, - /// A compiler refusal, by variant. - Refusal, - /// A claim grade on a field. - Claim, - /// A reusable operation, shipped or not. - Capability, - /// An executed check. - Test, -} - -/// What an edge says. Directions are fixed: `src REL dst`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Rel { - /// class → method. - HasMethod, - /// method → field. - ReadsField, - /// method → cond. - Filters, - /// method → fold. - Folds, - /// method → law. - FinishedBy, - /// method → method: a `Cond::Method` reference (G1). - Calls, - /// method → program. - LowersTo, - /// program → primitive (compiled), or method → primitive (a claim). - UsesPrimitive, - /// program → lowering. - LowersAs, - /// method or class → refusal. - RefusedFor, - /// field → claim. - DependsOnClaim, - /// field → field (`target` recomputed from `input`). - ComputesFrom, - /// method or refusal → capability: the capability would admit it. - BlockedBy, - /// method → capability, unproven. - MayRequire, - /// method → test, under a certificate. - VerifiedBy, -} - -/// How an edge is known. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum Tier { - Declared, - Derived, - Compiled, - Verified, - Hypothesis, -} - -/// Where a projection's facts came from. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Provenance { - /// The file the specs are declared in. - pub source: String, - /// The source revision (a commit, or `worktree`). - pub rev: String, - pub seal: SchemaSeal, - pub reading: ReadingId, -} - -/// One node: a kind and an identity string. Labels are display text and -/// never part of the identity. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Node { - pub kind: Kind, - pub ident: String, - pub label: Option, -} - -/// One edge row. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Edge { - pub src: u32, - pub rel: Rel, - pub dst: u32, - pub tier: Tier, - /// The method (or class) whose projection wrote the edge: the hop a - /// dependency query lands on. - pub ctx: u32, - pub prov: u32, -} - -/// Proof that a method's current body ran against an oracle. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Certificate { - pub method: MethodKey, - /// [`SoftwareGraph::digest`] of the body when the oracle ran. - pub digest: u64, - pub seal: SchemaSeal, - pub test: String, -} - -/// Why a certificate is not accepted. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CertError { - UnknownMethod(MethodKey), - /// The body changed since the oracle ran. - StaleBody { - method: MethodKey, - }, - /// The schema changed since the oracle ran. - StaleSeal { - method: MethodKey, - }, -} - -/// A dependency query that did not run. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum QueryError { - Bind(BindError), - Lower(LowerError), - Exec(ExecError), -} - -/// Whether a reusable operation exists, from the graph's evidence alone. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CapStatus { - /// A compiled program contains it. - Shipped, - /// Not shipped; a compiler refusal names it as the missing piece. - Demonstrated { - refusals: Vec, - }, - /// Not shipped; only declared or hypothesized needs. - Claimed { - by: Vec, - }, - Unknown, -} - -/// A program that names a primitive its claim edges do not match. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ClaimMismatch { - pub method: String, - pub primitive: String, -} - -/// A structural optimization candidate. Never a rewrite. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Candidate { - pub kind: CandidateKind, - /// The method nodes that would change. - pub methods: Vec, - /// What must hold for the rewrite to be legal, each open or discharged. - pub obligations: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CandidateKind { - /// The same conditions in several methods: factor one predicate. - SharedPredicate { conds: Vec }, - /// One method's programs evaluate the same selection more than once. - SharedSelection { programs: Vec }, - /// A tiled program whose selection feeds a masked sum: H1, fuse the - /// predicate into the fold. - FusePredicateIntoSum { program: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Obligation { - pub what: &'static str, - /// `Some(reason)` when the graph's evidence discharges it. - pub discharged: Option, -} - -/// The CPU a measurement ran on: the compiled target and the host feature. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CpuTier { - pub target: String, - pub avx512f: bool, -} - -/// One timing of one program on one dataset and tier. -#[derive(Debug, Clone, PartialEq)] -pub struct Measurement { - pub program: String, - pub rows: usize, - pub tier: CpuTier, - pub median_ns: f64, -} - -/// A measurement refused for the tier it was asked about. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CostError { - Unmeasured, - TierMismatch { measured: CpuTier }, -} - -/// One workload's before/after timing. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Workload { - pub before_ns: f64, - pub after_ns: f64, -} - -/// What a set of workloads says about a transformation. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Verdict { - Unmeasured, - Improves, - Regresses, - /// Faster on some, slower on others: not an improvement. - Mixed, -} - -/// Faster everywhere, slower everywhere, or mixed. A tie is not faster. -#[must_use] -pub fn verdict(w: &[Workload]) -> Verdict { - if w.is_empty() { - return Verdict::Unmeasured; - } - let up = w.iter().filter(|x| x.after_ns < x.before_ns).count(); - let down = w.iter().filter(|x| x.after_ns > x.before_ns).count(); - match (up, down) { - (_, 0) if up == w.len() => Verdict::Improves, - (0, _) if down == w.len() => Verdict::Regresses, - _ => Verdict::Mixed, - } -} - -/// The software-awareness graph. -#[derive(Debug, Default, Clone)] -pub struct SoftwareGraph { - nodes: Vec, - edges: Vec, - provs: Vec, - /// `(method, body digest, seal)` of every projected spec. - digests: Vec<(MethodKey, u64, SchemaSeal)>, - measurements: Vec, -} - -fn hex(bytes: &[u8]) -> String { - bytes.iter().fold(String::new(), |mut s, b| { - let _ = write!(s, "{b:02x}"); - s - }) -} - -/// A node's kind, from its identity's prefix: one identity, one kind. -#[must_use] -pub fn kind_of(ident: &str) -> Option { - const PREFIXES: [(&str, Kind); 13] = [ - ("class:", Kind::Class), - ("method:", Kind::Method), - ("field:", Kind::Field), - ("cond:", Kind::Cond), - ("fold:", Kind::Fold), - ("law:", Kind::Law), - ("program:", Kind::Program), - ("prim:", Kind::Primitive), - ("lowering:", Kind::Lowering), - ("refusal:", Kind::Refusal), - ("claim:", Kind::Claim), - ("cap:", Kind::Capability), - ("test:", Kind::Test), - ]; - PREFIXES - .iter() - .find(|(p, _)| ident.starts_with(p)) - .map(|(_, k)| *k) -} - -/// The identity of a method node. -#[must_use] -pub fn method_ident(key: MethodKey) -> String { - format!("method:{}", hex(&key.0)) -} - -/// The identity of a field node: concept, seal and canonical name. Two -/// concepts that share a field name have two field nodes. -#[must_use] -pub fn field_ident(concept: u16, seal: SchemaSeal, name: &str) -> String { - format!("field:{concept:#06x}/{:016x}/{name}", seal.0) -} - -/// The primitive kind of a mask op, by variant. -#[must_use] -pub fn op_name(op: &MaskOp) -> String { - match op { - MaskOp::Pred { pred, .. } => format!("Pred.{}", pred_name(pred)), - MaskOp::And { .. } => "And".into(), - MaskOp::Or { .. } => "Or".into(), - MaskOp::Xor { .. } => "Xor".into(), - MaskOp::AndNot { .. } => "AndNot".into(), - MaskOp::Not { .. } => "Not".into(), - MaskOp::Ternlog { .. } => "Ternlog".into(), - MaskOp::Gather { .. } => "Gather".into(), - } -} - -fn pred_name(p: &Pred) -> &'static str { - match p { - Pred::GtI32 { .. } => "GtI32", - Pred::LtI32 { .. } => "LtI32", - Pred::GeI32 { .. } => "GeI32", - Pred::LeI32 { .. } => "LeI32", - Pred::EqI32 { .. } => "EqI32", - Pred::NeI32 { .. } => "NeI32", - Pred::EqU32 { .. } => "EqU32", - Pred::NeU32 { .. } => "NeU32", - Pred::MatchU32 { .. } => "MatchU32", - Pred::MatchU64 { .. } => "MatchU64", - Pred::EqU32Via { .. } => "EqU32Via", - Pred::Range { .. } => "Range", - Pred::EqU32Strided { .. } => "EqU32Strided", - Pred::NeU32Strided { .. } => "NeU32Strided", - Pred::MatchFacetStrided { .. } => "MatchFacetStrided", - Pred::MatchFacet16Strided { .. } => "MatchFacet16Strided", - } -} - -/// The terminal kind, by variant. -#[must_use] -pub fn terminal_name(t: &Terminal) -> &'static str { - match t { - Terminal::Count { .. } => "Count", - Terminal::Any { .. } => "Any", - Terminal::All { .. } => "All", - Terminal::MaskedSumI32 { .. } => "MaskedSumI32", - Terminal::MaskedMinI32 { .. } => "MaskedMinI32", - Terminal::MaskedMaxI32 { .. } => "MaskedMaxI32", - Terminal::MaskedStridedGroupSum { .. } => "MaskedStridedGroupSum", - Terminal::BlendI32 { .. } => "BlendI32", - Terminal::Keep { .. } => "Keep", - Terminal::ScatterOrU32 { .. } => "ScatterOrU32", - Terminal::ScatterCountU32 { .. } => "ScatterCountU32", - Terminal::CountKeyRunsU32 { .. } => "CountKeyRunsU32", - Terminal::GroupSumI32 { .. } => "GroupSumI32", - Terminal::GroupSumViaI32 { .. } => "GroupSumViaI32", - Terminal::GroupReduce { .. } => "GroupReduce", - Terminal::GroupPowerSumsI32 { .. } => "GroupPowerSumsI32", - Terminal::GroupCrossPowerSumsI32 { .. } => "GroupCrossPowerSumsI32", - } -} - -fn lowering_name(l: Lowering) -> &'static str { - match l { - Lowering::Range(_) => "Range", - Lowering::Ternlog(_) => "Ternlog", - Lowering::TernlogKeep(_) => "TernlogKeep", - Lowering::Tern2(_) => "Tern2", - Lowering::Tern3(_) => "Tern3", - Lowering::Tiled => "Tiled", - } -} - -fn refusal_name(e: &PrepareError) -> String { - let d = format!("{e:?}"); - d.split(|c: char| !c.is_alphanumeric()) - .next() - .unwrap_or_default() - .to_owned() -} - -/// FNV-1a over a spec's rendering. Stable within one build: a toolchain -/// change makes every certificate stale (never falsely fresh). -fn digest(spec: &MethodSpec<'_>) -> u64 { - let mut h = 0xcbf2_9ce4_8422_2325u64; - for b in format!("{spec:?}").bytes() { - h = (h ^ u64::from(b)).wrapping_mul(0x0000_0100_0000_01b3); - } - h -} - -const EDGE_TABLE: &str = "edges"; -const SRC: u16 = 0; -const REL: u16 = 1; -const DST: u16 = 2; -const TIER: u16 = 3; -const CTX: u16 = 4; - -const RELS: [(Rel, &str); 15] = [ - (Rel::Calls, "calls"), - (Rel::HasMethod, "has_method"), - (Rel::ReadsField, "reads_field"), - (Rel::Filters, "filters"), - (Rel::Folds, "folds"), - (Rel::FinishedBy, "finished_by"), - (Rel::LowersTo, "lowers_to"), - (Rel::UsesPrimitive, "uses_primitive"), - (Rel::LowersAs, "lowers_as"), - (Rel::RefusedFor, "refused_for"), - (Rel::DependsOnClaim, "depends_on_claim"), - (Rel::ComputesFrom, "computes_from"), - (Rel::BlockedBy, "blocked_by"), - (Rel::MayRequire, "may_require"), - (Rel::VerifiedBy, "verified_by"), -]; - -const TIERS: [(Tier, &str); 5] = [ - (Tier::Declared, "declared"), - (Tier::Derived, "derived"), - (Tier::Compiled, "compiled"), - (Tier::Verified, "verified"), - (Tier::Hypothesis, "hypothesis"), -]; - -fn rel_code(r: Rel) -> u32 { - RELS.iter() - .position(|(x, _)| *x == r) - .expect("every rel is listed") as u32 -} - -fn tier_code(t: Tier) -> u32 { - TIERS - .iter() - .position(|(x, _)| *x == t) - .expect("every tier is listed") as u32 -} - -impl SoftwareGraph { - #[must_use] - pub fn new() -> Self { - Self::default() - } - - #[must_use] - pub fn nodes(&self) -> &[Node] { - &self.nodes - } - - #[must_use] - pub fn edges(&self) -> &[Edge] { - &self.edges - } - - #[must_use] - pub fn provenance(&self, edge: &Edge) -> &Provenance { - &self.provs[edge.prov as usize] - } - - /// The node with this identity. - #[must_use] - pub fn node(&self, ident: &str) -> Option { - self.nodes - .iter() - .position(|n| n.ident == ident) - .map(|i| i as u32) - } - - fn intern(&mut self, kind: Kind, ident: String) -> u32 { - debug_assert_eq!(kind_of(&ident), Some(kind), "{ident}"); - if let Some(i) = self.node(&ident) { - return i; - } - self.nodes.push(Node { - kind, - ident, - label: None, - }); - (self.nodes.len() - 1) as u32 - } - - /// Add an edge unless the same `(src, rel, dst, tier)` is already there. - pub fn link(&mut self, src: u32, rel: Rel, dst: u32, tier: Tier, ctx: u32, prov: u32) { - let dup = self - .edges - .iter() - .any(|e| (e.src, e.rel, e.dst, e.tier) == (src, rel, dst, tier)); - if !dup { - self.edges.push(Edge { - src, - rel, - dst, - tier, - ctx, - prov, - }); - } - } - - /// The edges `src REL ?`, any tier. - #[must_use] - pub fn out(&self, src: &str, rel: Rel) -> Vec<(&str, Tier)> { - let Some(s) = self.node(src) else { - return Vec::new(); - }; - self.edges - .iter() - .filter(|e| e.src == s && e.rel == rel) - .map(|e| (self.nodes[e.dst as usize].ident.as_str(), e.tier)) - .collect() - } - - /// The current body digest of a projected method. - #[must_use] - pub fn digest(&self, key: MethodKey) -> Option { - self.digests - .iter() - .rev() - .find(|(k, ..)| *k == key) - .map(|(_, d, _)| *d) - } - - /// Project a prepared table and the specs it was prepared from. - /// - /// `claims` are the glove's claims, read so that a field's grade is a - /// node (a method whose field is `Hypothesized` is refused, and the - /// graph says why). A spec's identity facts are `Declared`, the claims - /// `Derived`, the table's verdicts and programs `Compiled`. - pub fn project( - &mut self, - table: &MethodTable, - specs: &[(&str, MethodSpec<'_>)], - claims: &[FieldMap], - source: &str, - rev: &str, - ) { - for (label, spec) in specs { - let prov = self.provs.len() as u32; - self.provs.push(Provenance { - source: source.into(), - rev: rev.into(), - seal: spec.schema, - reading: spec.reading, - }); - self.digests.push((spec.key, digest(spec), spec.schema)); - let class = self.intern(Kind::Class, format!("class:{:#06x}", spec.concept)); - let m = self.intern(Kind::Method, method_ident(spec.key)); - // A method's projection replaces its earlier one: every edge - // written within it (structure, programs, verdicts, - // certificates, notes) belongs to the body it described. - self.edges.retain(|e| e.ctx != m); - self.nodes[m as usize].label = Some((*label).to_owned()); - self.link(class, Rel::HasMethod, m, Tier::Declared, m, prov); - let r = spec.reading.0; - let field = |g: &mut Self, name: &str| { - let f = g.intern(Kind::Field, field_ident(spec.concept, spec.schema, name)); - g.link(m, Rel::ReadsField, f, Tier::Declared, m, prov); - if let Some(c) = claims.iter().find(|c| c.canonical == name) { - let grade = match c.grade { - Grade::Exact => "exact", - Grade::Converted => "converted", - Grade::Hypothesized => "hypothesized", - }; - let claim = g.intern(Kind::Claim, format!("claim:{}:{grade}", c.native)); - g.link(f, Rel::DependsOnClaim, claim, Tier::Derived, m, prov); - } - g.nodes[f as usize].ident.clone() - }; - let (filter, folds, law): (&[Cond<'_>], &[Fold<'_>], Option>) = match spec.body - { - MethodBody::Query { filter, folds, law } => (filter, folds, Some(law)), - MethodBody::Select { filter } => (filter, &[], None), - MethodBody::Action { .. } => (&[], &[], None), - }; - for c in filter { - let (name, op, lit) = match *c { - Cond::Eq { field, literal } => (field, "=", literal), - Cond::Ne { field, literal } => (field, "!=", literal), - // A reference is a CALLS edge to the method it names, as - // the table resolves it; one that does not resolve has no - // edge, and the table's refusal says why. - Cond::Method(r) => { - let target = match r { - MethodRef::Key(k) => Some(k), - MethodRef::Label(l) => table.resolve(l), - }; - if let Some(k) = target { - let t = self.intern(Kind::Method, method_ident(k)); - self.link(m, Rel::Calls, t, Tier::Declared, m, prov); - } - continue; - } - // A hop's key and target field belong to the glove, not - // the spec: the condition is recorded as written. - Cond::Order { - field: name, - op, - value, - } => { - let op = match op { - Order::Lt => "<", - Order::Le => "<=", - Order::Gt => ">", - Order::Ge => ">=", - }; - let f = field(self, name); - let n = self.intern(Kind::Cond, format!("cond:{f}{op}{value}@r{r}")); - self.link(m, Rel::Filters, n, Tier::Declared, m, prov); - continue; - } - Cond::Reaches { rel } => { - let n = self.intern(Kind::Cond, format!("cond:out({rel})@r{r}")); - self.link(m, Rel::Filters, n, Tier::Declared, m, prov); - continue; - } - Cond::Via { - rel, - field, - literal, - negate, - } => { - let op = if negate { "!=" } else { "=" }; - let n = self.intern( - Kind::Cond, - format!("cond:out({rel}).{field}{op}{literal}@r{r}"), - ); - self.link(m, Rel::Filters, n, Tier::Declared, m, prov); - continue; - } - }; - let f = field(self, name); - let n = self.intern(Kind::Cond, format!("cond:{f}{op}{lit}@r{r}")); - self.link(m, Rel::Filters, n, Tier::Declared, m, prov); - } - for fold in folds { - let ident = match *fold { - Fold::Count => "fold:count(*)".to_owned(), - Fold::Sum(f) => format!("fold:sum({})", field(self, f)), - Fold::Min(f) => format!("fold:min({})", field(self, f)), - Fold::Max(f) => format!("fold:max({})", field(self, f)), - Fold::GroupSum { key, val, groups } => format!( - "fold:group_sum({},{},{groups})", - field(self, key), - field(self, val) - ), - // A relation's key and target belong to the glove; the - // fold is recorded as written. - Fold::Distinct { rel } => format!("fold:distinct({rel})"), - }; - let n = self.intern(Kind::Fold, format!("{ident}@r{r}")); - self.link(m, Rel::Folds, n, Tier::Declared, m, prov); - } - if let Some(law) = law { - let law = match law { - Law::Values => "law:values".to_owned(), - Law::NarsRevision { - independent, - scale, - horizon, - } => format!("law:nars_revision({independent},{scale},{horizon})"), - }; - let n = self.intern(Kind::Law, law); - self.link(m, Rel::FinishedBy, n, Tier::Declared, m, prov); - } - match table.get(spec.key) { - Some(Ok(p)) => { - for (i, program) in p.programs().iter().enumerate() { - let pid = format!("program:{}#{i}", hex(&spec.key.0)); - self.add_program(m, &pid, program, prov); - } - } - Some(Err(e)) => { - let n = self.intern(Kind::Refusal, format!("refusal:{}", refusal_name(e))); - self.link(m, Rel::RefusedFor, n, Tier::Compiled, m, prov); - } - None => {} - } - } - } - - /// Record one lowered program of `owner`: its primitives and lowering, - /// read from the program itself. - pub fn add_program(&mut self, owner: u32, ident: &str, program: &Program, prov: u32) { - let p = self.intern(Kind::Program, ident.to_owned()); - self.link(owner, Rel::LowersTo, p, Tier::Compiled, owner, prov); - let names = program - .ops - .iter() - .map(op_name) - .chain([terminal_name(&program.terminal).to_owned()]); - for name in names { - let n = self.intern(Kind::Primitive, format!("prim:{name}")); - self.link(p, Rel::UsesPrimitive, n, Tier::Compiled, owner, prov); - } - let l = self.intern( - Kind::Lowering, - format!("lowering:{}", lowering_name(program.lowering())), - ); - self.link(p, Rel::LowersAs, l, Tier::Compiled, owner, prov); - } - - /// A provenance row for facts not projected from a table. - pub fn note(&mut self, source: &str, rev: &str) -> u32 { - self.provs.push(Provenance { - source: source.into(), - rev: rev.into(), - seal: SchemaSeal(0), - reading: ReadingId(0), - }); - (self.provs.len() - 1) as u32 - } - - /// Record a need, or a claim, by identity: `src REL target` at `tier`. - /// Each node's kind is read from its identity's prefix, so a noted - /// `refusal:` is the same refusal node the compiler writes, never a - /// second node of another kind. Recording never ships anything (see - /// [`Self::capability`]). - /// - /// # Errors - /// - /// A compiled or verified tier, or an identity whose prefix names no - /// kind. - pub fn claim( - &mut self, - src: &str, - rel: Rel, - target: &str, - tier: Tier, - prov: u32, - ) -> Result<(), String> { - // Compiled and verified edges are written by `project`, `add_program`, - // `project_compute`, `blocked_by` and `certify`, never by a note. - if matches!(tier, Tier::Compiled | Tier::Verified) { - return Err(format!( - "{tier:?} is written by the compiler or a certificate" - )); - } - let m = self.intern_ident(src)?; - let c = self.intern_ident(target)?; - self.link(m, rel, c, tier, m, prov); - Ok(()) - } - - fn intern_ident(&mut self, ident: &str) -> Result { - let kind = kind_of(ident).ok_or_else(|| ident.to_owned())?; - Ok(self.intern(kind, ident.to_owned())) - } - - /// A method node by identity, for programs not prepared by a table. - pub fn add_method(&mut self, ident: &str) -> u32 { - self.intern(Kind::Method, ident.to_owned()) - } - - /// Record that `capability` would admit what a compiler refusal - /// rejects. Only a refusal the compiler actually recorded (a compiled - /// `RefusedFor` edge) can be named: a gap is demonstrated by a refusal, - /// never by a note. - /// - /// # Errors - /// - /// The refusal identity, when no compiled refusal carries it. - pub fn blocked_by(&mut self, refusal: &str, capability: &str, prov: u32) -> Result<(), String> { - let r = self - .node(refusal) - .filter(|&r| { - self.edges - .iter() - .any(|e| e.dst == r && e.rel == Rel::RefusedFor && e.tier == Tier::Compiled) - }) - .ok_or_else(|| refusal.to_owned())?; - let c = self.intern(Kind::Capability, capability.to_owned()); - self.link(r, Rel::BlockedBy, c, Tier::Compiled, r, prov); - Ok(()) - } - - /// Project a class's compute DAG and record the contract's verdict. - /// - /// The cycle check is `class_view::compute_dag_is_acyclic`, run on the - /// declaration; the graph only records what it said. - pub fn project_compute( - &mut self, - concept: u16, - seal: SchemaSeal, - basis: &[String], - edges: &[ComputeEdge], - prov: u32, - ) { - let class = self.intern(Kind::Class, format!("class:{concept:#06x}")); - let name = |p: u8| { - basis - .get(usize::from(p)) - .cloned() - .unwrap_or(format!("#{p}")) - }; - for e in edges { - let t = self.intern(Kind::Field, field_ident(concept, seal, &name(e.target))); - for &i in e.inputs { - let f = self.intern(Kind::Field, field_ident(concept, seal, &name(i))); - self.link(t, Rel::ComputesFrom, f, Tier::Derived, class, prov); - } - } - if !compute_dag_is_acyclic(edges) { - let r = self.intern(Kind::Refusal, "refusal:ComputeCycle".into()); - self.link(class, Rel::RefusedFor, r, Tier::Compiled, class, prov); - } - } - - /// Accept a certificate when the body and seal it names are current. - /// - /// # Errors - /// - /// [`CertError`] when the method is unknown or changed since. - pub fn certify(&mut self, cert: &Certificate, prov: u32) -> Result<(), CertError> { - let Some(&(_, d, s)) = self.digests.iter().rev().find(|(k, ..)| *k == cert.method) else { - return Err(CertError::UnknownMethod(cert.method)); - }; - if d != cert.digest { - return Err(CertError::StaleBody { - method: cert.method, - }); - } - if s != cert.seal { - return Err(CertError::StaleSeal { - method: cert.method, - }); - } - let m = self.intern(Kind::Method, method_ident(cert.method)); - let t = self.intern(Kind::Test, format!("test:{}", cert.test)); - self.link(m, Rel::VerifiedBy, t, Tier::Verified, m, prov); - Ok(()) - } - - /// Record a measurement. - pub fn measure(&mut self, m: Measurement) { - self.measurements.push(m); - } - - /// The measured cost of `program` on `tier`. - /// - /// # Errors - /// - /// [`CostError::TierMismatch`] when it was measured only on another - /// tier; a number from one CPU is never reported for another. - pub fn cost(&self, program: &str, rows: usize, tier: &CpuTier) -> Result { - let mut other = None; - for m in self - .measurements - .iter() - .filter(|m| m.program == program && m.rows == rows) - { - if &m.tier == tier { - return Ok(m.median_ns); - } - other = Some(m.tier.clone()); - } - Err( - other.map_or(CostError::Unmeasured, |measured| CostError::TierMismatch { - measured, - }), - ) - } - - // ---- queries over the edge table, through Quack ------------------- - - fn columns(&self) -> ([Vec; 5], Vec) { - let n = self.edges.len(); - let mut cols: [Vec; 5] = Default::default(); - for e in &self.edges { - cols[SRC as usize].push(e.src); - cols[REL as usize].push(rel_code(e.rel)); - cols[DST as usize].push(e.dst); - cols[TIER as usize].push(tier_code(e.tier)); - cols[CTX as usize].push(e.ctx); - } - let mut live = vec![u64::MAX; words_for(n)]; - if !n.is_multiple_of(64) { - if let Some(last) = live.last_mut() { - *last = (1u64 << (n % 64)) - 1; - } - } - (cols, live) - } - - /// Run `draft` over the edge table and hop each surviving edge to its - /// `ctx` node: one Quack program, `ScatterOrU32` as the demanded result. - /// - /// # Errors - /// - /// [`QueryError`] from binding, lowering or execution. - pub fn hop_to_ctx(&self, draft: &Draft) -> Result, QueryError> { - let bound = draft.bind(self).map_err(QueryError::Bind)?; - let q = Query { - filter: bound.filter, - agg: Agg::ScatterOrU32 { - fk: Col(CTX), - out_rows: self.nodes.len() as u32, - }, - }; - let program = lower(&q).map_err(QueryError::Lower)?; - let (cols, live) = self.columns(); - let lanes: Vec> = cols.iter().map(|c| LaneRef::U32(c)).collect(); - let planes = Planes { - n_rows: self.edges.len(), - masks: &[live.as_slice()], - lanes: &lanes, - }; - let mut scratch = - Scratch::for_program(&program, planes.n_rows).map_err(QueryError::Exec)?; - let mut out = vec![0u64; words_for(self.nodes.len())]; - execute_into( - &program, - &planes, - &Foreign::NONE, - &mut scratch, - lance_graph_mask_risc::Out::Mask(&mut out), - ) - .map_err(QueryError::Exec)?; - Ok(materialize_rows(&out, self.nodes.len()) - .into_iter() - .map(|i| self.nodes[i].ident.as_str()) - .collect()) - } - - /// Query A: the methods whose compiled programs use `primitive`. - /// - /// # Errors - /// - /// [`QueryError`]; an unknown primitive is `BindError::UnknownValue`. - pub fn methods_using(&self, primitive: &str) -> Result, QueryError> { - self.hop_to_ctx( - &lance_graph_quack::bind::table(EDGE_TABLE) - .where_eq("rel", "uses_primitive") - .where_eq("tier", "compiled") - .where_eq("dst", format!("prim:{primitive}").as_str()), - ) - } - - /// Query B: the methods (or classes) the compiler refused, for `reason`. - /// - /// # Errors - /// - /// As [`Self::hop_to_ctx`]. - pub fn refused_for(&self, reason: &str) -> Result, QueryError> { - self.hop_to_ctx( - &lance_graph_quack::bind::table(EDGE_TABLE) - .where_eq("rel", "refused_for") - .where_eq("dst", format!("refusal:{reason}").as_str()), - ) - } - - /// Falsifier 8: every primitive a method is CLAIMED to use (any tier but - /// compiled) must appear in one of its compiled programs. - #[must_use] - pub fn primitive_claim_mismatches(&self) -> Vec { - self.edges - .iter() - .filter(|e| e.rel == Rel::UsesPrimitive && e.tier != Tier::Compiled) - .filter(|claim| { - !self.edges.iter().any(|c| { - c.rel == Rel::UsesPrimitive - && c.tier == Tier::Compiled - && c.ctx == claim.src - && c.dst == claim.dst - }) - }) - .map(|e| ClaimMismatch { - method: self.nodes[e.src as usize].ident.clone(), - primitive: self.nodes[e.dst as usize].ident.clone(), - }) - .collect() - } - - /// Falsifier 9: a capability is shipped only when a COMPILED program - /// uses it; a declared need never ships it. - #[must_use] - pub fn capability(&self, ident: &str) -> CapStatus { - let Some(c) = self.node(ident) else { - return CapStatus::Unknown; - }; - let into = |p: &dyn Fn(&Edge) -> bool| -> Vec { - self.edges - .iter() - .filter(|e| e.dst == c && p(e)) - .map(|e| self.nodes[e.src as usize].ident.clone()) - .collect() - }; - if !into(&|e| e.rel == Rel::UsesPrimitive && e.tier == Tier::Compiled).is_empty() { - return CapStatus::Shipped; - } - let refusals = - into(&|e| e.rel == Rel::BlockedBy && self.nodes[e.src as usize].kind == Kind::Refusal); - if !refusals.is_empty() { - return CapStatus::Demonstrated { refusals }; - } - let by = into(&|_| true); - if by.is_empty() { - CapStatus::Unknown - } else { - CapStatus::Claimed { by } - } - } - - fn methods(&self) -> impl Iterator + '_ { - (0..self.nodes.len() as u32).filter(|&i| self.nodes[i as usize].kind == Kind::Method) - } - - fn targets(&self, src: u32, rel: Rel) -> Vec { - let mut v: Vec = self - .edges - .iter() - .filter(|e| e.src == src && e.rel == rel) - .map(|e| e.dst) - .collect(); - v.sort_unstable(); - v.dedup(); - v - } - - /// Query D (a): condition sets shared by two or more prepared methods. - /// Conditions are compared by identity (field under concept and seal, - /// operator, literal, reading), never by label. - #[must_use] - pub fn shared_predicates(&self) -> Vec { - let prepared: Vec = self - .methods() - .filter(|&m| !self.targets(m, Rel::LowersTo).is_empty()) - .collect(); - let mut out: Vec = Vec::new(); - for (i, &a) in prepared.iter().enumerate() { - let ca = self.targets(a, Rel::Filters); - if ca.is_empty() { - continue; - } - let mut group = vec![a]; - for &b in &prepared[i + 1..] { - if self.targets(b, Rel::Filters) == ca { - group.push(b); - } - } - let conds: Vec = ca - .iter() - .map(|&c| self.nodes[c as usize].ident.clone()) - .collect(); - let seen = out.iter().any( - |c| matches!(&c.kind, CandidateKind::SharedPredicate { conds: x } if *x == conds), - ); - if group.len() > 1 && !seen { - out.push(Candidate { - kind: CandidateKind::SharedPredicate { conds }, - methods: group - .iter() - .map(|&m| self.nodes[m as usize].ident.clone()) - .collect(), - obligations: vec![ - Obligation { - what: "same concept, seal, reading and effect", - discharged: Some( - "condition identities include concept, seal and reading".into(), - ), - }, - Obligation { - what: "the predicate method lowers to the same program", - discharged: None, - }, - ], - }); - } - } - out - } - - /// Query D (b): a method whose programs carry identical selection ops, - /// i.e. the same population scanned once per fold. - #[must_use] - pub fn shared_selections(&self, table: &MethodTable) -> Vec { - let mut out = Vec::new(); - for m in self.methods() { - let ident = &self.nodes[m as usize].ident; - let Some(key) = parse_key(ident) else { - continue; - }; - let Some(Ok(p)) = table.get(key) else { - continue; - }; - let progs = p.programs(); - if progs.len() > 1 && progs.iter().all(|x| x.ops == progs[0].ops) { - out.push(Candidate { - kind: CandidateKind::SharedSelection { - programs: (0..progs.len()) - .map(|i| format!("program:{}#{i}", hex(&key.0))) - .collect(), - }, - methods: vec![ident.clone()], - obligations: vec![Obligation { - what: "one mask feeding several terminals is a population \ - intermediate the executor forbids; needs a multi-terminal \ - program, not a shared mask", - discharged: None, - }], - }); - } - } - out - } - - /// H1: tiled programs whose selection feeds a `MaskedSumI32`. Each - /// candidate lists what fusing the predicate into the sum must keep. - #[must_use] - pub fn fusion_candidates(&self, programs: &[(String, &Program)]) -> Vec { - programs - .iter() - .filter(|(_, p)| { - matches!(p.terminal, Terminal::MaskedSumI32 { .. }) - && p.lowering() == Lowering::Tiled - }) - .map(|(id, p)| { - let gates = gate_planes(p); - let owner = self - .node(id) - .and_then(|pid| { - self.edges - .iter() - .find(|e| e.dst == pid && e.rel == Rel::LowersTo) - }) - .map(|e| self.nodes[e.src as usize].ident.clone()); - Candidate { - kind: CandidateKind::FusePredicateIntoSum { - program: id.clone(), - }, - methods: owner.into_iter().collect(), - obligations: vec![ - Obligation { - what: "NULL/UNKNOWN: every validity plane the program ANDs must \ - still gate the fused sum", - discharged: (gates.len() <= 1).then(|| { - "the program reads one plane (live); no validity gate".into() - }), - }, - Obligation { - what: "row bound: at most MASKED_SUM_I32_MAX_ROWS rows", - discharged: None, - }, - Obligation { - what: "the fused terminal must exist: FusedFold has only Count/Any", - discharged: None, - }, - ], - } - }) - .collect() - } -} - -/// The resident planes a program reads as gates. -fn gate_planes(p: &Program) -> Vec { - let mut v = Vec::new(); - let mut see = |o: Operand| { - if let Operand::Plane(k) = o { - if !v.contains(&k) { - v.push(k); - } - } - }; - for op in &p.ops { - match *op { - MaskOp::Pred { under, .. } | MaskOp::Gather { under, .. } => { - if let Some(u) = under { - see(u); - } - } - MaskOp::And { a, b, .. } - | MaskOp::Or { a, b, .. } - | MaskOp::Xor { a, b, .. } - | MaskOp::AndNot { a, b, .. } => { - see(a); - see(b); - } - MaskOp::Not { a, .. } => see(a), - MaskOp::Ternlog { a, b, c, .. } => { - see(a); - see(b); - see(c); - } - } - } - v -} - -fn parse_key(ident: &str) -> Option { - let h = ident.strip_prefix("method:")?; - if h.len() != 32 { - return None; - } - let mut k = [0u8; 16]; - for (i, b) in k.iter_mut().enumerate() { - *b = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).ok()?; - } - Some(MethodKey(k)) -} - -/// The edge table as a Quack table: `edges(src, rel, dst, tier, ctx)`, every -/// field a code. Node identities, relation and tier names are the codebook; -/// nothing is minted. -impl Binder for SoftwareGraph { - fn table(&self, name: &str) -> Option { - (name == EDGE_TABLE).then_some(TableId(0)) - } - fn live(&self, _: TableId) -> Mask { - Mask(0) - } - fn field(&self, _: TableId, name: &str) -> Option { - let col = match name { - "src" => SRC, - "rel" => REL, - "dst" => DST, - "tier" => TIER, - "ctx" => CTX, - _ => return None, - }; - Some(BoundField { - col: Col(col), - kind: FieldKind::Code, - validity: None, - }) - } - fn code(&self, _: TableId, field: Col, literal: &str) -> Option { - match field.0 { - REL => RELS - .iter() - .position(|(_, n)| *n == literal) - .map(|i| i as u32), - TIER => TIERS - .iter() - .position(|(_, n)| *n == literal) - .map(|i| i as u32), - _ => self.node(literal), - } - } -} - -/// Description length of the methods' declared bodies (D-SAG-1 §5). -/// -/// Transparent, deliberately crude: a body is the sequence of its condition, -/// fold and law tokens. `L(P)` = each atom × `log2(atoms)`, plus each used -/// composite × one `log2(|V|)` reference per atom it is defined over; -/// `L(M|P)` = each method's tokens × `log2(|V|)`, where `V` is the atoms and -/// the used composites. A composite (a factored predicate) replaces its -/// conditions as one token in every method that carries all of them. -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct Dl { - pub primitives: f64, - pub methods: f64, -} - -impl Dl { - #[must_use] - pub fn total(&self) -> f64 { - self.primitives + self.methods - } -} - -/// `bodies`: each method's token identities. `composites`: factored -/// condition sets. Identities are compared as given; the caller decides -/// whether they are typed identities or bare labels. -#[must_use] -pub fn description_length(bodies: &[Vec], composites: &[Vec]) -> Dl { - let mut atoms: Vec<&String> = bodies.iter().flatten().collect(); - atoms.sort(); - atoms.dedup(); - let atom_bits = (atoms.len().max(2) as f64).log2(); - let rewritten: Vec> = bodies - .iter() - .map(|b| { - let mut b = b.clone(); - for (i, c) in composites.iter().enumerate() { - if c.iter().all(|x| b.contains(x)) { - b.retain(|x| !c.contains(x)); - b.push(format!("composite#{i}")); - } - } - b - }) - .collect(); - // The vocabulary is every token a rewritten body uses, plus every atom a - // used composite is defined over: a composite is spelled in atoms, so - // its atoms stay in the alphabet even when no body names them alone. - let used: Vec = (0..composites.len()) - .filter(|i| { - rewritten - .iter() - .flatten() - .any(|t| *t == format!("composite#{i}")) - }) - .collect(); - let mut vocab: Vec<&String> = rewritten - .iter() - .flatten() - .filter(|t| !t.starts_with("composite#")) - .chain(used.iter().flat_map(|&i| composites[i].iter())) - .collect(); - vocab.sort(); - vocab.dedup(); - let v_bits = ((vocab.len() + used.len()).max(2) as f64).log2(); - // Atoms cost their spelling; a composite costs one reference per atom. - let primitives = vocab.len() as f64 * atom_bits - + used - .iter() - .map(|&i| composites[i].len() as f64 * v_bits) - .sum::(); - let methods = rewritten.iter().map(|b| b.len() as f64 * v_bits).sum(); - Dl { - primitives, - methods, - } -} - -impl SoftwareGraph { - /// Each prepared-or-not method's declared tokens, by identity. - #[must_use] - pub fn bodies(&self) -> Vec<(String, Vec)> { - self.methods() - .map(|m| { - let toks = [Rel::Filters, Rel::Folds, Rel::FinishedBy] - .into_iter() - .flat_map(|r| self.targets(m, r)) - .map(|t| self.nodes[t as usize].ident.clone()) - .collect(); - (self.nodes[m as usize].ident.clone(), toks) - }) - .filter(|(_, t): &(String, Vec)| !t.is_empty()) - .collect() - } -} diff --git a/crates/lance-graph-glove-parity/src/call.rs b/crates/lance-graph-glove-parity/src/call.rs deleted file mode 100644 index cae5665e5..000000000 --- a/crates/lance-graph-glove-parity/src/call.rs +++ /dev/null @@ -1,94 +0,0 @@ -//! The class-qualified call, as a caller's `sql(query)` carries it. -//! -//! No SQL frontend reaches Quack today: `lance-graph`'s SQL path is -//! DataFusion, which is in its grace period and gains nothing new. This -//! module recognises exactly ONE form and refuses everything else: -//! -//! ```text -//! SELECT ::() FROM
-//! ``` -//! -//! It is the seam a real SQL frontend hands its parsed call to, not a SQL -//! parser. The text is resolved once, cold, into a typed [`MethodCall`]: -//! -//! - `` through OGAR's codebook (`canonical_concept_id`), never a -//! local name table; -//! - `` through the [`MethodTable`]'s labels, into a [`MethodKey`]; -//! - the payload names the row the method reads, which must be the `FROM` -//! table, which must be the table the methods were bound against. -//! -//! Nothing is substituted back into text: the call carries numbers only. -use lance_graph_ogar::ogar_vocab::canonical_concept_id; - -use crate::method::{MethodKey, MethodTable}; - -/// A resolved class-qualified call. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MethodCall { - /// Canonical concept id. - pub concept: u16, - pub method: MethodKey, -} - -/// Why a call text did not resolve. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum CallParseError { - /// Not of the one recognised form. - Unrecognised, - /// OGAR's codebook has no concept of that name. - UnknownConcept(String), - /// The concept is not the one whose methods the table holds. - WrongConcept { called: u16, table: u16 }, - /// The concept has no method of that name. - UnknownMethod(String), - /// The payload is not the `FROM` table. - PayloadMismatch, - /// The `FROM` table is not the one the methods were bound against. - WrongTable(String), -} - -/// Resolve `SELECT ::(
) FROM
` against `t`. -/// -/// # Errors -/// -/// [`CallParseError`] naming the first part that did not resolve. -pub fn resolve_call(sql: &str, t: &MethodTable) -> Result { - let words: Vec<&str> = sql.split_whitespace().collect(); - let [select, call, from, table] = words.as_slice() else { - return Err(CallParseError::Unrecognised); - }; - if !select.eq_ignore_ascii_case("select") || !from.eq_ignore_ascii_case("from") { - return Err(CallParseError::Unrecognised); - } - let (concept, rest) = call.split_once("::").ok_or(CallParseError::Unrecognised)?; - let (method, payload) = rest - .strip_suffix(')') - .and_then(|r| r.split_once('(')) - .ok_or(CallParseError::Unrecognised)?; - let ident = - |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_'); - if ![concept, method, payload, table].into_iter().all(ident) { - return Err(CallParseError::Unrecognised); - } - let id = canonical_concept_id(concept) - .ok_or_else(|| CallParseError::UnknownConcept(concept.to_owned()))?; - if id != t.concept() { - return Err(CallParseError::WrongConcept { - called: id, - table: t.concept(), - }); - } - if payload != *table { - return Err(CallParseError::PayloadMismatch); - } - if *table != t.table() { - return Err(CallParseError::WrongTable((*table).to_owned())); - } - let method = t - .resolve(method) - .ok_or_else(|| CallParseError::UnknownMethod(method.to_owned()))?; - Ok(MethodCall { - concept: id, - method, - }) -} diff --git a/crates/lance-graph-glove-parity/src/decl.rs b/crates/lance-graph-glove-parity/src/decl.rs deleted file mode 100644 index 6c60531b4..000000000 --- a/crates/lance-graph-glove-parity/src/decl.rs +++ /dev/null @@ -1,826 +0,0 @@ -//! Method declarations loaded at run time. -//! -//! A [`MethodDecl`] is a [`MethodSpec`] whose strings are owned, parsed from -//! text after the binary was compiled. It is not executed here and has no -//! evaluator of its own: [`MethodTable::prepare_with`] lends each declaration -//! to [`MethodTable::prepare`] as a borrowed spec, so a loaded method goes -//! through the same binding, claim, law and lowering checks as a compiled one -//! and runs as the same Quack program. Preparation copies out the numbers it -//! keeps, so the declarations and the text can be dropped afterwards. -//! -//! The grammar is deliberately small. One declaration per line; `#` starts a -//! comment line: -//! -//! ```text -//! ::
` resolves through OGAR's -//! codebook to a concept id and a method key, binds once against the glove, -//! and runs as Quack algebra. Its answer is checked against the same query -//! written directly in Quack, against R2IL's procedures, and against a -//! scalar oracle. Then every way the binding could lie is refused by name, -//! and NAL revision is compiled from the same algebra plus one certified -//! law. -//! -//! Every method is declared in this file. The method keys, the reading id, -//! the population id and the NARS concept are FIXTURES. -use lance_graph_glove_parity::basis::{canonical_fields, FieldMap, Grade, SAP_FIELDS}; -use lance_graph_glove_parity::call::{resolve_call, CallParseError, MethodCall}; -use lance_graph_glove_parity::method::{ - Answer, CallError, Cond, EffectClass, Fold, FoldValue, Frontend, Glove, Law, MethodBody, - MethodKey, MethodSpec, MethodTable, PrepareError, ReadingId, SchemaSeal, Truth, Universe, -}; -use lance_graph_mask_risc::{execute_into, Foreign, LaneRef, Out, Planes, Scratch, Value}; -use lance_graph_ogar::ogar_vocab::class_ids::BILLABLE_WORK_ENTRY; -use lance_graph_quack::bind::{table, Binder, BoundField, FieldKind, TableId}; -use lance_graph_quack::{lower, Agg, Col, Mask, Query}; -use lance_graph_r2il::{procs, FoldDialect, Val}; -use lance_graph_sap::bind::{billable_code, CatsBatch, BILLING, HOURS}; -use lance_graph_sap::binder::{live_words, CatsBinder, LIVE, TABLE}; -use lance_graph_sap::schema::{CatsSchema, FIELD_COUNT}; - -const READING: ReadingId = ReadingId(1); -const UNIVERSE: Universe = Universe(42); -const BILLABLE_HOURS: MethodKey = MethodKey([0xB2; 16]); - -const BILLABLE_TRUE: Cond = Cond::Eq { - field: "billable", - literal: "true", -}; - -fn seal() -> SchemaSeal { - SchemaSeal::of(&canonical_fields()) -} - -fn billable_hours() -> MethodSpec<'static> { - MethodSpec { - key: BILLABLE_HOURS, - concept: BILLABLE_WORK_ENTRY, - schema: seal(), - reading: READING, - effect: EffectClass::Pure, - body: MethodBody::Query { - filter: &[BILLABLE_TRUE], - folds: &[Fold::Sum("duration")], - law: Law::Values, - }, - } -} - -// --- the SAP fixture (as in tests/method.rs) --------------------------------- - -const INDICATORS: [&str; 4] = ["Billable", "Non-Billable", "Internal Cost", "Unknown"]; -const HOURS_TEXT: [&str; 3] = ["8.50", "0.125", "2"]; - -fn indicator(i: usize) -> &'static str { - INDICATORS[(i * 7 + i / 3) % 4] -} - -fn batch(n: usize) -> CatsBatch { - let base: Vec> = include_str!("../../lance-graph-sap/fixtures/cats.txt") - .lines() - .map(|v| if v == "\\N" { None } else { Some(v) }) - .collect(); - let mut input: [Vec>; FIELD_COUNT] = std::array::from_fn(|i| vec![base[i]; n]); - for i in 0..n { - input[BILLING][i] = Some(indicator(i)); - input[HOURS][i] = Some(HOURS_TEXT[i % 3]); - } - CatsBatch::bind( - CatsSchema::new(42, 0), - std::array::from_fn(|i| input[i].as_slice()), - ) - .unwrap() -} - -/// FIXTURE: SAP's real claim on `duration` is `Hypothesized`; this table -/// promotes it so the method has an `I32` quantity to sum. -fn with_duration() -> Vec { - SAP_FIELDS - .iter() - .map(|m| FieldMap { - grade: if m.canonical == "duration" { - Grade::Exact - } else { - m.grade - }, - ..*m - }) - .collect() -} - -fn glove<'a>(basis: &'a [String], binder: &'a dyn Binder, claims: &'a [FieldMap]) -> Glove<'a> { - Glove { - concept: BILLABLE_WORK_ENTRY, - basis, - claims, - binder, - table: TABLE, - reading: READING, - universe: UNIVERSE, - relations: &[], - } -} - -fn hours(batch: &CatsBatch) -> Vec { - let LaneRef::I32(h) = batch.lanes()[HOURS] else { - panic!("hours is an I32 lane") - }; - h.to_vec() -} - -// --- the seam --------------------------------------------------------------- - -/// FAILS IF: the class-qualified call stops agreeing with the same query -/// written directly in Quack, with R2IL's procedures, or with a row loop. -#[test] -fn a_class_qualified_call_agrees_with_direct_quack_r2il_and_the_oracle() { - let sql = "SELECT billable_work_entry::billable_hours(cats) FROM cats"; - for n in [1, 64, 65, 4097] { - let batch = batch(n); - let cats = CatsBinder::new(&batch); - let claims = with_duration(); - let basis = canonical_fields(); - let t = MethodTable::prepare( - &glove(&basis, &cats, &claims), - &[("billable_hours", billable_hours())], - ); - let call = resolve_call(sql, &t).unwrap(); - assert_eq!( - call, - MethodCall { - concept: BILLABLE_WORK_ENTRY, - method: BILLABLE_HOURS - } - ); - - let live = live_words(n); - let lanes = batch.lanes(); - let planes = Planes { - n_rows: n, - masks: &[live.as_slice()], - lanes: &lanes, - }; - - // 1. The oracle: a row loop over the indicator text and the hours. - let h = hours(&batch); - let oracle: i64 = (0..n) - .filter(|&i| billable_code(indicator(i)) == 1) - .map(|i| i64::from(h[i])) - .sum(); - - // 2. The same query written by hand in Quack, bound from names. - let tid = cats.table(TABLE).unwrap(); - let hours_col = cats.field(tid, "hours_logged").unwrap().col; - let by_name = table(TABLE) - .where_eq("billable", "true") - .count() - .bind(&cats) - .unwrap(); - let direct = Query { - filter: by_name.filter, - agg: Agg::SumI32(hours_col), - }; - let p = lower(&direct).unwrap(); - let mut scratch = Scratch::for_program(&p, n).unwrap(); - let Value::SumI64(direct) = - execute_into(&p, &planes, &Foreign::NONE, &mut scratch, Out::None).unwrap() - else { - panic!() - }; - - // 3. R2IL's procedures, called by hand. - let billable = cats.field(tid, "billable").unwrap().col; - let mut d = FoldDialect::new(&planes, &Foreign::NONE); - let mut stack = Vec::new(); - procs::select_u32(&mut d, &mut stack, LIVE.0, &[(billable.0, 1, false)]).unwrap(); - procs::sum(&mut d, &mut stack, hours_col.0).unwrap(); - let [Val::Scalar(r2il)] = stack.as_slice() else { - panic!() - }; - - let want = Answer::Values(vec![FoldValue::Scalar(oracle)]); - assert_eq!(direct, oracle, "direct quack n={n}"); - assert_eq!(*r2il, oracle, "r2il procs n={n}"); - for f in [Frontend::Quack, Frontend::R2il] { - assert_eq!( - t.call(call.method, &planes, UNIVERSE, f).unwrap(), - want, - "method {f:?} n={n}" - ); - } - } -} - -/// The call text names a concept through OGAR's codebook, a method through -/// the table's labels, and a row; each part that does not resolve is named. -#[test] -fn a_call_that_does_not_resolve_is_refused_by_name() { - let batch = batch(2); - let cats = CatsBinder::new(&batch); - let claims = with_duration(); - let basis = canonical_fields(); - let t = MethodTable::prepare( - &glove(&basis, &cats, &claims), - &[("billable_hours", billable_hours())], - ); - let cases = [ - ( - "SELECT nars::revision(cats) FROM cats", - CallParseError::UnknownConcept("nars".into()), - ), - ( - "SELECT project::billable_hours(cats) FROM cats", - CallParseError::WrongConcept { - called: lance_graph_ogar::ogar_vocab::class_ids::PROJECT, - table: BILLABLE_WORK_ENTRY, - }, - ), - ( - "SELECT billable_work_entry::frobnicate(cats) FROM cats", - CallParseError::UnknownMethod("frobnicate".into()), - ), - ( - "SELECT billable_work_entry::billable_hours(orders) FROM cats", - CallParseError::PayloadMismatch, - ), - ( - "SELECT billable_work_entry::billable_hours(orders) FROM orders", - CallParseError::WrongTable("orders".into()), - ), - ( - "SELECT billable_work_entry::billable_hours(cats) FROM cats WHERE x = 1", - CallParseError::Unrecognised, - ), - ( - "SELECT billable_work_entry::billable_hours(cats); DROP TABLE cats FROM cats", - CallParseError::Unrecognised, - ), - ]; - for (sql, want) in cases { - assert_eq!(resolve_call(sql, &t), Err(want), "{sql}"); - } -} - -// --- refusals --------------------------------------------------------------- - -/// FAILS IF: a spec written for one basis binds a reordered one. Swapping two -/// canonical positions keeps every name, so only the seal can tell. -#[test] -fn a_spec_for_another_schema_or_reading_is_refused() { - let batch = batch(2); - let cats = CatsBinder::new(&batch); - let claims = with_duration(); - let basis = canonical_fields(); - let mut swapped = basis.clone(); - swapped.swap(0, 1); - assert_ne!(SchemaSeal::of(&swapped), seal()); - let t = MethodTable::prepare(&glove(&swapped, &cats, &claims), &[("h", billable_hours())]); - assert!(matches!( - t.get(BILLABLE_HOURS), - Some(Err(PrepareError::SchemaMismatch { .. })) - )); - - let other = MethodSpec { - reading: ReadingId(2), - ..billable_hours() - }; - let t = MethodTable::prepare(&glove(&basis, &cats, &claims), &[("h", other)]); - assert_eq!( - t.get(BILLABLE_HOURS), - Some(&Err(PrepareError::ReadingMismatch { - spec: ReadingId(2), - glove: READING - })) - ); - - // Twin: the unswapped basis and matching reading prepare. - let t = MethodTable::prepare(&glove(&basis, &cats, &claims), &[("h", billable_hours())]); - assert!(t.get(BILLABLE_HOURS).unwrap().is_ok()); -} - -/// FAILS IF: planes of another population run under a method prepared for -/// this one. -#[test] -fn planes_of_another_population_are_refused() { - let batch = batch(65); - let cats = CatsBinder::new(&batch); - let claims = with_duration(); - let basis = canonical_fields(); - let t = MethodTable::prepare(&glove(&basis, &cats, &claims), &[("h", billable_hours())]); - let live = live_words(65); - let lanes = batch.lanes(); - let planes = Planes { - n_rows: 65, - masks: &[live.as_slice()], - lanes: &lanes, - }; - for f in [Frontend::Quack, Frontend::R2il] { - assert!(matches!( - t.call(BILLABLE_HOURS, &planes, Universe(43), f), - Err(CallError::UniverseMismatch { - method: UNIVERSE, - planes: Universe(43) - }) - )); - assert!(t.call(BILLABLE_HOURS, &planes, UNIVERSE, f).is_ok()); - } -} - -/// A binder that declares `billable` nullable. -struct NullableBillable<'a>(CatsBinder<'a>); - -impl Binder for NullableBillable<'_> { - fn table(&self, name: &str) -> Option { - self.0.table(name) - } - fn live(&self, t: TableId) -> Mask { - self.0.live(t) - } - fn field(&self, t: TableId, name: &str) -> Option { - let mut f = self.0.field(t, name)?; - if name == "billable" { - f.validity = Some(Mask(7)); - } - Some(f) - } - fn code(&self, t: TableId, col: Col, literal: &str) -> Option { - self.0.code(t, col, literal) - } -} - -/// FAILS IF: a nullable field binds into a two-valued condition (NULL would -/// read as whatever the lane holds there). -#[test] -fn a_nullable_field_is_refused() { - let batch = batch(2); - let nb = NullableBillable(CatsBinder::new(&batch)); - let claims = with_duration(); - let basis = canonical_fields(); - let t = MethodTable::prepare(&glove(&basis, &nb, &claims), &[("h", billable_hours())]); - assert_eq!( - t.get(BILLABLE_HOURS), - Some(&Err(PrepareError::Nullable("billable".into()))) - ); -} - -/// FAILS IF: a basis wider than a `WideFieldMask` position can address is -/// truncated instead of refused. -#[test] -fn a_basis_past_256_positions_is_refused() { - let batch = batch(2); - let cats = CatsBinder::new(&batch); - let claims = with_duration(); - let mut wide = canonical_fields(); - // Pad with names nothing claims, until `billable` sits past position 255 - // only in the second case. - while wide.len() < 257 { - wide.push(format!("pad_{}", wide.len())); - } - let spec = MethodSpec { - schema: SchemaSeal::of(&wide), - ..billable_hours() - }; - let t = MethodTable::prepare(&glove(&wide, &cats, &claims), &[("h", spec)]); - assert_eq!( - t.get(BILLABLE_HOURS), - Some(&Err(PrepareError::BasisTooWide(257))) - ); - // Twin: exactly 256 positions prepare. - wide.pop(); - let spec = MethodSpec { - schema: SchemaSeal::of(&wide), - ..billable_hours() - }; - let t = MethodTable::prepare(&glove(&wide, &cats, &claims), &[("h", spec)]); - assert!(t.get(BILLABLE_HOURS).unwrap().is_ok()); -} - -// --- NAL revision ----------------------------------------------------------- - -/// FIXTURE: no NARS concept is minted in OGAR's codebook yet. -const NARS: u16 = 0xFFF0; -const REVISION: MethodKey = MethodKey([0xEE; 16]); -const RAW_SUMS: MethodKey = MethodKey([0xEF; 16]); -const SCALE: u32 = 1000; -const GROUPS: u32 = 4; - -const EVIDENCE: &str = "evidence"; -const STATEMENT: usize = 0; -const STAMP: usize = 1; -const INDEPENDENT: usize = 2; -const W: usize = 3; -const WF: usize = 4; - -fn nars_basis() -> Vec { - ["statement", "stamp", "independent", "w", "wf"] - .map(String::from) - .to_vec() -} - -const NARS_CLAIMS: [FieldMap; 5] = [ - FieldMap { - native: "statement", - canonical: "statement", - grade: Grade::Exact, - note: "fixture", - }, - FieldMap { - native: "stamp", - canonical: "stamp", - grade: Grade::Exact, - note: "fixture", - }, - FieldMap { - native: "independent", - canonical: "independent", - grade: Grade::Exact, - note: "fixture", - }, - FieldMap { - native: "w", - canonical: "w", - grade: Grade::Exact, - note: "fixture", - }, - FieldMap { - native: "wf", - canonical: "wf", - grade: Grade::Exact, - note: "fixture", - }, -]; - -/// The evidence table's resolution membrane: five non-nullable lanes. -struct EvidenceBinder; - -impl Binder for EvidenceBinder { - fn table(&self, name: &str) -> Option { - (name == EVIDENCE).then_some(TableId(0)) - } - fn live(&self, _: TableId) -> Mask { - Mask(0) - } - fn field(&self, _: TableId, name: &str) -> Option { - let (col, kind) = match name { - "statement" => (STATEMENT, FieldKind::Code), - "stamp" => (STAMP, FieldKind::Code), - "independent" => (INDEPENDENT, FieldKind::Code), - "w" => (W, FieldKind::I32), - "wf" => (WF, FieldKind::I32), - _ => return None, - }; - Some(BoundField { - col: Col(col as u16), - kind, - validity: None, - }) - } - fn code(&self, _: TableId, col: Col, literal: &str) -> Option { - if usize::from(col.0) != INDEPENDENT { - return None; - } - match literal { - "true" => Some(1), - "false" => Some(0), - _ => None, - } - } -} - -const REVISION_FOLDS: &[Fold] = &[ - Fold::GroupSum { - key: "statement", - val: "w", - groups: GROUPS, - }, - Fold::GroupSum { - key: "statement", - val: "wf", - groups: GROUPS, - }, -]; - -const REVISION_LAW: Law = Law::NarsRevision { - independent: "independent", - scale: SCALE, - horizon: 1, -}; - -fn nars_spec( - key: MethodKey, - filter: &'static [Cond], - folds: &'static [Fold], - law: Law<'static>, -) -> MethodSpec<'static> { - MethodSpec { - key, - concept: NARS, - schema: SchemaSeal::of(&nars_basis()), - reading: READING, - effect: EffectClass::Pure, - body: MethodBody::Query { filter, folds, law }, - } -} - -fn nars_specs() -> Vec<(&'static str, MethodSpec<'static>)> { - const INDEP: &[Cond] = &[Cond::Eq { - field: "independent", - literal: "true", - }]; - vec![ - ( - "revision", - nars_spec(REVISION, INDEP, REVISION_FOLDS, REVISION_LAW), - ), - // The same sums without the independence condition, as plain values: - // what revision would read if duplicates were not excluded. - ( - "raw_sums", - nars_spec(RAW_SUMS, &[], REVISION_FOLDS, Law::Values), - ), - ] -} - -/// One piece of evidence: `(statement, stamp, f, c)`. -type Evidence = (u32, u32, f64, f64); - -/// The OpenNARS reference revisions (`lance-graph-planner` -/// `tests/nal_revision_oracle.rs`), as evidence per statement, plus one -/// REPEATED stamp on statement 0. Statement 3 has no evidence. -const ROWS: [Evidence; 7] = [ - (0, 10, 1.0, 0.9), - (0, 11, 0.1, 0.6), - (1, 20, 0.8, 0.5), - (1, 21, 0.3, 0.7), - (2, 30, 0.0, 0.9), - (2, 31, 1.0, 0.9), - (0, 10, 1.0, 0.9), // the same stamp again -]; - -/// OpenNARS's revised truths, to two places. -const REFERENCE: [(f64, f64); 3] = [(0.87, 0.91), (0.45, 0.77), (0.50, 0.95)]; - -struct Lanes { - statement: Vec, - stamp: Vec, - independent: Vec, - w: Vec, - wf: Vec, -} - -/// The payload's producer: fixed-point weights, and the first-occurrence -/// mark of each `(statement, stamp)` (what the algebra cannot compute yet). -fn produce(rows: &[Evidence]) -> Lanes { - let mut seen = Vec::new(); - let mut l = Lanes { - statement: vec![], - stamp: vec![], - independent: vec![], - w: vec![], - wf: vec![], - }; - for &(s, stamp, f, c) in rows { - let w = (f64::from(SCALE) * c / (1.0 - c)).round(); - l.statement.push(s); - l.stamp.push(stamp); - l.independent.push(u32::from(!seen.contains(&(s, stamp)))); - seen.push((s, stamp)); - l.w.push(w as i32); - l.wf.push((w * f).round() as i32); - } - l -} - -/// The independent oracle: revision as the NAL defines it, in floating -/// point, from `(f, c)` directly, over the independent evidence. -fn oracle(rows: &[Evidence]) -> Vec> { - let mut seen = Vec::new(); - let mut acc = vec![(0.0f64, 0.0f64); GROUPS as usize]; - for &(s, stamp, f, c) in rows { - if seen.contains(&(s, stamp)) { - continue; - } - seen.push((s, stamp)); - let w = c / (1.0 - c); - acc[s as usize].0 += w; - acc[s as usize].1 += w * f; - } - acc.iter() - .map(|&(w, wf)| { - (w > 0.0).then(|| Truth { - f: wf / w, - c: w / (w + 1.0), - }) - }) - .collect() -} - -fn nars_table() -> MethodTable { - let basis = nars_basis(); - MethodTable::prepare( - &Glove { - concept: NARS, - basis: &basis, - claims: &NARS_CLAIMS, - binder: &EvidenceBinder, - table: EVIDENCE, - reading: READING, - universe: UNIVERSE, - relations: &[], - }, - &nars_specs(), - ) -} - -fn run_nars(t: &MethodTable, key: MethodKey, l: &Lanes, f: Frontend) -> Answer { - let n = l.w.len(); - let live = live_words(n); - let lanes = [ - LaneRef::U32(&l.statement), - LaneRef::U32(&l.stamp), - LaneRef::U32(&l.independent), - LaneRef::I32(&l.w), - LaneRef::I32(&l.wf), - ]; - let planes = Planes { - n_rows: n, - masks: &[live.as_slice()], - lanes: &lanes, - }; - t.call(key, &planes, UNIVERSE, f).unwrap() -} - -/// FAILS IF: revision compiled from two grouped sums and the certified law -/// disagrees with the floating-point oracle or with OpenNARS, on either -/// frontend; or counts the repeated stamp. -#[test] -fn nars_revision_compiles_from_grouped_sums_and_one_law() { - let t = nars_table(); - let lanes = produce(&ROWS); - let want = oracle(&ROWS); - for f in [Frontend::Quack, Frontend::R2il] { - let Answer::Truths(got) = run_nars(&t, REVISION, &lanes, f) else { - panic!("revision answers truths") - }; - assert_eq!(got.len(), GROUPS as usize); - for (s, (g, o)) in got.iter().zip(&want).enumerate() { - match (g, o) { - (Some(g), Some(o)) => { - // Fixed point at scale 1000 moves the third place at most. - assert!((g.f - o.f).abs() < 1e-3, "{f:?} s{s} f {g:?} vs {o:?}"); - assert!((g.c - o.c).abs() < 1e-3, "{f:?} s{s} c {g:?} vs {o:?}"); - } - (None, None) => {} - _ => panic!("{f:?} s{s}: {g:?} vs {o:?}"), - } - } - for (s, &(rf, rc)) in REFERENCE.iter().enumerate() { - let g = got[s].unwrap(); - assert!( - (g.f - rf).abs() < 0.005 && (g.c - rc).abs() < 0.005, - "{f:?} s{s} {g:?}" - ); - } - assert_eq!(got[3], None, "no evidence, no truth"); - } -} - -/// The counterexample for the missing algebra: the same two sums without the -/// independence condition count the repeated stamp, and the confidence that -/// would follow is not NAL revision. -#[test] -fn a_repeated_stamp_inflates_the_raw_sums() { - let t = nars_table(); - let lanes = produce(&ROWS); - let Answer::Values(v) = run_nars(&t, RAW_SUMS, &lanes, Frontend::Quack) else { - panic!() - }; - let [FoldValue::Groups(w), _] = v.as_slice() else { - panic!() - }; - // Statement 0: 9000 + 1500 counted, plus 9000 again. - assert_eq!(w[0], 19_500); - let weight = w[0] as f64 / f64::from(SCALE); - let inflated = weight / (weight + 1.0); - assert!((inflated - 0.951).abs() < 1e-3); - assert!( - (inflated - REFERENCE[0].1).abs() > 0.03, - "the repeat must matter" - ); -} - -/// FAILS IF: a revision body that does not select independent evidence, or -/// whose folds are not the law's shape, prepares. -#[test] -fn a_revision_body_without_independence_or_in_the_wrong_shape_is_refused() { - const MIXED_KEYS: &[Fold] = &[ - Fold::GroupSum { - key: "statement", - val: "w", - groups: GROUPS, - }, - Fold::GroupSum { - key: "stamp", - val: "wf", - groups: GROUPS, - }, - ]; - const INDEP: &[Cond] = &[Cond::Eq { - field: "independent", - literal: "true", - }]; - let basis = nars_basis(); - let specs = [ - ( - "no_indep", - nars_spec(MethodKey([1; 16]), &[], REVISION_FOLDS, REVISION_LAW), - ), - ( - "mixed", - nars_spec(MethodKey([2; 16]), INDEP, MIXED_KEYS, REVISION_LAW), - ), - ( - "one_fold", - nars_spec( - MethodKey([3; 16]), - INDEP, - &REVISION_FOLDS[..1], - REVISION_LAW, - ), - ), - ]; - let t = MethodTable::prepare( - &Glove { - concept: NARS, - basis: &basis, - claims: &NARS_CLAIMS, - binder: &EvidenceBinder, - table: EVIDENCE, - reading: READING, - universe: UNIVERSE, - relations: &[], - }, - &specs, - ); - assert_eq!( - t.get(MethodKey([1; 16])), - Some(&Err(PrepareError::EvidenceNotIndependent( - "independent".into() - ))) - ); - assert_eq!( - t.get(MethodKey([2; 16])), - Some(&Err(PrepareError::LawShape)) - ); - assert_eq!( - t.get(MethodKey([3; 16])), - Some(&Err(PrepareError::LawShape)) - ); -} - -/// KNOWN GAP, pinned: a statement at or past the declared domain is dropped -/// by the grouped-sum terminal, not refused. A method cannot see it. -#[test] -fn evidence_outside_the_declared_domain_is_dropped_silently() { - let t = nars_table(); - let mut rows = ROWS.to_vec(); - rows.push((GROUPS + 1, 99, 1.0, 0.9)); - let Answer::Truths(got) = run_nars(&t, REVISION, &produce(&rows), Frontend::Quack) else { - panic!() - }; - let Answer::Truths(base) = run_nars(&t, REVISION, &produce(&ROWS), Frontend::Quack) else { - panic!() - }; - assert_eq!(got, base, "the out-of-domain row left no trace"); -} - -// --- Experiment C: replay, layers, retraction -------------------------------- - -/// One delta as it arrives: `(delta_id, layer, statement, stamp, f, c)`. -/// Replay identity (`delta_id`) and evidential identity (`stamp`) are -/// different things: a replayed delta repeats a delta; two deltas can carry -/// the same stamp (one observation, reported in two hypothesis layers). -type Delta = (u32, u8, u32, u32, f64, f64); - -/// The payload producer: drops a replayed `delta_id` first, then marks the -/// first occurrence of each `(statement, stamp)` across ALL layers as -/// independent. Both marks are the producer's, because the algebra cannot -/// compute a first occurrence yet (ledger row 1). -fn produce_deltas(deltas: &[Delta]) -> Lanes { - let mut seen_delta = Vec::new(); - let rows: Vec = deltas - .iter() - .filter(|d| { - let fresh = !seen_delta.contains(&d.0); - seen_delta.push(d.0); - fresh - }) - .map(|&(_, _, s, stamp, f, c)| (s, stamp, f, c)) - .collect(); - produce(&rows) -} - -/// FAILS IF: a replayed delta, or the same stamp arriving in a second -/// hypothesis layer, changes the revised truth. -#[test] -fn replay_and_cross_layer_repeats_do_not_change_revision() { - let t = nars_table(); - let base: Vec = ROWS[..6] - .iter() - .enumerate() - .map(|(i, &(s, stamp, f, c))| (i as u32, 0, s, stamp, f, c)) - .collect(); - let mut noisy = base.clone(); - noisy.push(base[0]); // the same delta, replayed - noisy.push(base[3]); // replayed again, later - noisy.push((100, 1, 0, 11, 0.1, 0.6)); // stamp 11, seen in layer 1 too - noisy.push((101, 1, 2, 30, 0.0, 0.9)); // stamp 30, layer 1 - for f in [Frontend::Quack, Frontend::R2il] { - let a = run_nars(&t, REVISION, &produce_deltas(&base), f); - let b = run_nars(&t, REVISION, &produce_deltas(&noisy), f); - assert_eq!(a, b, "{f:?}"); - } - // Non-vacuity: without the marks, the noisy payload inflates the sums. - let raw = |d: &[Delta]| { - let rows: Vec = d.iter().map(|&(_, _, s, st, f, c)| (s, st, f, c)).collect(); - let Answer::Values(v) = run_nars(&t, RAW_SUMS, &produce(&rows), Frontend::Quack) else { - panic!() - }; - v - }; - assert_ne!(raw(&base), raw(&noisy)); -} - -/// TYPED INSUFFICIENCY, pinned: retraction is not declared by -/// `Law::NarsRevision`. Writing it as a negative-weight delta is legal -/// arithmetic in the sums, and wrong whenever the retracted stamp was never -/// asserted: the sums cannot see which stamps were asserted. Retraction needs -/// a semijoin of retraction stamps onto asserted stamps (ledger). -#[test] -fn retraction_by_negative_weight_is_not_revision() { - let t = nars_table(); - // Retract stamp 11 from statement 0 (it was asserted): exact. - let mut lanes = produce(&ROWS[..6]); - let retract_11 = (0, 11, 0.1, 0.6); - let w = (f64::from(SCALE) * 0.6 / 0.4).round(); - lanes.statement.push(retract_11.0); - lanes.stamp.push(retract_11.1); - lanes.independent.push(1); - lanes.w.push(-(w as i32)); - lanes.wf.push(-((w * 0.1).round() as i32)); - let Answer::Truths(got) = run_nars(&t, REVISION, &lanes, Frontend::Quack) else { - panic!() - }; - let only_10 = oracle(&[(0, 10, 1.0, 0.9)]); - assert!((got[0].unwrap().c - only_10[0].unwrap().c).abs() < 1e-3); - - // Retract stamp 99 (never asserted): the sums accept it and answer. - let mut lanes = produce(&ROWS[..6]); - lanes.statement.push(0); - lanes.stamp.push(99); - lanes.independent.push(1); - lanes.w.push(-(w as i32)); - lanes.wf.push(-((w * 0.1).round() as i32)); - let Answer::Truths(got) = run_nars(&t, REVISION, &lanes, Frontend::Quack) else { - panic!() - }; - let unchanged = oracle(&ROWS[..6])[0].unwrap(); - assert!( - (got[0].unwrap().c - unchanged.c).abs() > 0.01, - "a phantom retraction changed the truth, and nothing refused it" - ); -} - -// --- review fixes: ambiguity and law parameters are refused ------------------ - -/// FAILS IF: a repeated method key or label is resolved by slice order -/// instead of refused. -#[test] -fn a_repeated_key_or_label_is_refused() { - let batch = batch(2); - let cats = CatsBinder::new(&batch); - let claims = with_duration(); - let basis = canonical_fields(); - let other_key = MethodSpec { - key: MethodKey([0xC1; 16]), - ..billable_hours() - }; - let t = MethodTable::prepare( - &glove(&basis, &cats, &claims), - &[ - ("h", billable_hours()), - ("h2", billable_hours()), // same key, another label - ("dup", other_key), - ( - "dup", - MethodSpec { - key: MethodKey([0xC2; 16]), - ..billable_hours() - }, - ), - ], - ); - assert_eq!( - t.get(BILLABLE_HOURS), - Some(&Err(PrepareError::DuplicateKey(BILLABLE_HOURS))) - ); - assert_eq!(t.resolve("dup"), None, "an ambiguous label names nothing"); - assert_eq!( - t.get(MethodKey([0xC1; 16])), - Some(&Err(PrepareError::DuplicateLabel("dup".into()))) - ); - // Twin: a unique key and label still prepare. - let t = MethodTable::prepare(&glove(&basis, &cats, &claims), &[("h", billable_hours())]); - assert!(t.get(BILLABLE_HOURS).unwrap().is_ok()); - assert_eq!(t.resolve("h"), Some(BILLABLE_HOURS)); -} - -/// FAILS IF: a revision law with a zero scale or zero horizon prepares (a -/// zero scale yields NaN, a zero horizon a confidence of exactly 1). -#[test] -fn a_degenerate_revision_law_is_refused() { - const INDEP: &[Cond] = &[Cond::Eq { - field: "independent", - literal: "true", - }]; - let law = |scale, horizon| Law::NarsRevision { - independent: "independent", - scale, - horizon, - }; - let basis = nars_basis(); - let specs = [ - ( - "zero_scale", - nars_spec(MethodKey([4; 16]), INDEP, REVISION_FOLDS, law(0, 1)), - ), - ( - "zero_horizon", - nars_spec(MethodKey([5; 16]), INDEP, REVISION_FOLDS, law(SCALE, 0)), - ), - ( - "ok", - nars_spec(MethodKey([6; 16]), INDEP, REVISION_FOLDS, law(SCALE, 1)), - ), - ]; - let t = MethodTable::prepare( - &Glove { - concept: NARS, - basis: &basis, - claims: &NARS_CLAIMS, - binder: &EvidenceBinder, - table: EVIDENCE, - reading: READING, - universe: UNIVERSE, - relations: &[], - }, - &specs, - ); - assert_eq!( - t.get(MethodKey([4; 16])), - Some(&Err(PrepareError::LawParameter)) - ); - assert_eq!( - t.get(MethodKey([5; 16])), - Some(&Err(PrepareError::LawParameter)) - ); - assert!(t.get(MethodKey([6; 16])).unwrap().is_ok()); -} - -/// FAILS IF: two proven claims for one canonical field are resolved by -/// slice order. Reordering the claims must not change which lane binds. -#[test] -fn an_ambiguous_canonical_claim_is_refused() { - let batch = batch(2); - let cats = CatsBinder::new(&batch); - let basis = canonical_fields(); - let mut claims = with_duration(); - // A second proven source for `duration`, from another native field. - claims.push(FieldMap { - native: "work_day", - canonical: "duration", - grade: Grade::Exact, - note: "fixture: a conflicting claim", - }); - let t = MethodTable::prepare(&glove(&basis, &cats, &claims), &[("h", billable_hours())]); - assert_eq!( - t.get(BILLABLE_HOURS), - Some(&Err(PrepareError::AmbiguousClaim("duration".into()))) - ); - // Twin: the same claim repeated for the same native field is not - // ambiguous. - let mut claims = with_duration(); - let dup = *claims.iter().find(|c| c.canonical == "duration").unwrap(); - claims.push(dup); - let t = MethodTable::prepare(&glove(&basis, &cats, &claims), &[("h", billable_hours())]); - assert!(t.get(BILLABLE_HOURS).unwrap().is_ok()); -} diff --git a/crates/lance-graph-glove-parity/tests/relation.rs b/crates/lance-graph-glove-parity/tests/relation.rs deleted file mode 100644 index 065e81e05..000000000 --- a/crates/lance-graph-glove-parity/tests/relation.rs +++ /dev/null @@ -1,722 +0,0 @@ -//! D-CMH-G1-4a: a functional relation carried by the glove. -//! -//! `out(rel)` over a foreign key keeps the anchor (#1311's anchor rule): each -//! anchor row still stands for one traverser, and the target's fields are -//! read through the key. Gremlin drops a traverser whose edge leads nowhere, -//! so `out(rel)` is "the key names a LIVE row of the target": a dangling key -//! and a key to a dead row both fail it. -//! -//! FIXTURE: an ERP-shaped pair (`line -billed_to-> partner`) with synthetic -//! concept ids. Every answer is checked against a row loop that reads the -//! TEXT of each field, never a bound lane or a code. -use lance_graph_glove_parity::basis::{FieldMap, Grade}; -use lance_graph_glove_parity::gremlin::{compose, GremlinRefusal, Pred, Step}; -use lance_graph_glove_parity::method::{ - Answer, CallError, Cond, EffectClass, Fold, FoldValue, Frontend, Glove, Law, MethodBody, - MethodKey, MethodRef, MethodSpec, MethodTable, PrepareError, Reach, ReadingId, Relation, - SchemaSeal, Universe, -}; -use lance_graph_mask_risc::{words_for, Foreign, ForeignPlane, LaneRef, Planes}; -use lance_graph_quack::bind::{Binder, BoundField, FieldKind, TableId}; -use lance_graph_quack::{ - lower, Agg, Cmp, Col, Filter, ForeignLane, ForeignPlane as QPlane, Mask, Query, -}; - -/// FIXTURE concept ids. -const LINE: u16 = 0xFFE1; -const PARTNER: u16 = 0xFFE2; -const READING: ReadingId = ReadingId(1); -const LINE_U: Universe = Universe(7); -const PARTNER_U: Universe = Universe(8); -const PARTNERS: usize = 37; -const SIZES: [usize; 4] = [1, 64, 65, 4097]; - -const STATUS: [&str; 3] = ["open", "posted", "void"]; -const COUNTRY: [&str; 3] = ["DE", "FR", "US"]; - -fn status(i: usize) -> &'static str { - STATUS[(i * 5 + i / 7) % 3] -} -fn amount(i: usize) -> i32 { - ((i * 37) % 201) as i32 - 100 -} -/// Some keys dangle: `PARTNERS..PARTNERS + 4` names no partner. -fn partner_of(i: usize) -> usize { - (i * 11 + 3) % (PARTNERS + 4) -} -fn line_live(i: usize) -> bool { - i % 13 != 5 -} -fn country(p: usize) -> &'static str { - COUNTRY[(p * 3 + p / 4) % 3] -} -fn partner_live(p: usize) -> bool { - p % 7 != 2 -} - -/// The binder of both tables. Codes are the text's index in its domain. -struct Erp; - -impl Binder for Erp { - fn table(&self, name: &str) -> Option { - match name { - "line" => Some(TableId(0)), - "partner" => Some(TableId(1)), - _ => None, - } - } - fn live(&self, _: TableId) -> Mask { - Mask(0) - } - fn field(&self, t: TableId, name: &str) -> Option { - let (col, kind) = match (t.0, name) { - (0, "status") => (0, FieldKind::Code), - (0, "amount") => (1, FieldKind::I32), - (0, "partner") => (2, FieldKind::Code), - (1, "country") => (0, FieldKind::Code), - (1, "credit") => (1, FieldKind::I32), - _ => return None, - }; - Some(BoundField { - col: Col(col), - kind, - validity: None, - }) - } - fn code(&self, t: TableId, col: Col, literal: &str) -> Option { - let domain: &[&str] = match (t.0, col.0) { - (0, 0) => &STATUS, - (1, 0) => &COUNTRY, - _ => return None, - }; - domain.iter().position(|d| *d == literal).map(|p| p as u32) - } -} - -fn exact(fields: &[&'static str]) -> Vec { - fields - .iter() - .map(|f| FieldMap { - native: f, - canonical: f, - grade: Grade::Exact, - note: "fixture", - }) - .collect() -} - -fn names(fields: &[&str]) -> Vec { - fields.iter().map(|f| (*f).to_owned()).collect() -} - -/// The planes of one size, owned. -struct Data { - n: usize, - line_live: Vec, - status: Vec, - amount: Vec, - partner: Vec, - partner_live: Vec, - country: Vec, - credit: Vec, -} - -fn bits(n: usize, f: impl Fn(usize) -> bool) -> Vec { - let mut w = vec![0u64; words_for(n)]; - for i in (0..n).filter(|&i| f(i)) { - w[i / 64] |= 1 << (i % 64); - } - w -} - -fn data(n: usize) -> Data { - let code = |d: &[&str], s: &str| d.iter().position(|x| *x == s).unwrap() as u32; - Data { - n, - line_live: bits(n, line_live), - status: (0..n).map(|i| code(&STATUS, status(i))).collect(), - amount: (0..n).map(amount).collect(), - partner: (0..n).map(|i| partner_of(i) as u32).collect(), - partner_live: bits(PARTNERS, partner_live), - country: (0..PARTNERS).map(|p| code(&COUNTRY, country(p))).collect(), - credit: (0..PARTNERS).map(|p| p as i32).collect(), - } -} - -impl Data { - fn run( - &self, - table: &MethodTable, - key: MethodKey, - frontend: Frontend, - ) -> Result { - self.run_as(table, key, frontend, &[(PARTNER, PARTNER_U)]) - } - - fn run_as( - &self, - table: &MethodTable, - key: MethodKey, - frontend: Frontend, - universes: &[(u16, Universe)], - ) -> Result { - let masks = [self.line_live.as_slice()]; - let lanes = [ - LaneRef::U32(&self.status), - LaneRef::I32(&self.amount), - LaneRef::U32(&self.partner), - ]; - let planes = Planes { - n_rows: self.n, - masks: &masks, - lanes: &lanes, - }; - let fplanes = [ForeignPlane { - words: &self.partner_live, - rows: PARTNERS, - }]; - let flanes = [LaneRef::U32(&self.country), LaneRef::I32(&self.credit)]; - let reach = Reach { - foreign: Foreign { - planes: &fplanes, - lanes: &flanes, - }, - universes, - }; - table.call_with(key, &planes, LINE_U, &reach, frontend) - } -} - -/// Row loop over the text: rows that are live and pass `keep`. -fn oracle(n: usize, keep: impl Fn(usize) -> bool) -> (i64, i64) { - let rows: Vec = (0..n).filter(|&i| line_live(i) && keep(i)).collect(); - ( - rows.len() as i64, - rows.iter().map(|&i| i64::from(amount(i))).sum(), - ) -} - -/// The target row a line reaches, if any: in range and live. -fn reached(i: usize) -> Option { - let p = partner_of(i); - (p < PARTNERS && partner_live(p)).then_some(p) -} - -/// A row filter of the oracle. -type Keep = Box bool>; - -struct Fx { - line_basis: Vec, - line_claims: Vec, - partner_basis: Vec, - partner_claims: Vec, -} - -impl Fx { - fn new() -> Self { - Self { - line_basis: names(&["status", "amount", "partner"]), - line_claims: exact(&["status", "amount", "partner"]), - partner_basis: names(&["country", "credit"]), - partner_claims: exact(&["country", "credit"]), - } - } - - fn spec<'b>(&self, key: u8, body: MethodBody<'b>) -> MethodSpec<'b> { - MethodSpec { - key: MethodKey([key; 16]), - concept: LINE, - schema: SchemaSeal::of(&self.line_basis), - reading: READING, - effect: EffectClass::Pure, - body, - } - } - - /// Prepare `specs` against the line glove with the `billed_to` relation. - fn table(&self, specs: &[(&str, MethodSpec<'_>)]) -> MethodTable { - self.table_with(specs, &[("country", 0), ("credit", 1)], &self.line_claims) - } - - fn table_with( - &self, - specs: &[(&str, MethodSpec<'_>)], - lanes: &[(&str, u16)], - line_claims: &[FieldMap], - ) -> MethodTable { - let partner = Glove { - concept: PARTNER, - basis: &self.partner_basis, - claims: &self.partner_claims, - binder: &Erp, - table: "partner", - reading: READING, - universe: PARTNER_U, - relations: &[], - }; - let rels = [Relation { - name: "billed_to", - fk: "partner", - target: &partner, - plane: 0, - lanes, - }]; - let glove = Glove { - concept: LINE, - basis: &self.line_basis, - claims: line_claims, - binder: &Erp, - table: "line", - reading: READING, - universe: LINE_U, - relations: &rels, - }; - MethodTable::prepare(&glove, specs) - } -} - -fn query(filter: &'static [Cond<'static>], folds: &'static [Fold<'static>]) -> MethodBody<'static> { - MethodBody::Query { - filter, - folds, - law: Law::Values, - } -} - -const COUNT_SUM: &[Fold] = &[Fold::Count, Fold::Sum("amount")]; -const OPEN: Cond = Cond::Eq { - field: "status", - literal: "open", -}; -const TO_DE: Cond = Cond::Via { - rel: "billed_to", - field: "country", - literal: "DE", - negate: false, -}; -const NOT_DE: Cond = Cond::Via { - rel: "billed_to", - field: "country", - literal: "DE", - negate: true, -}; -const REACHES: Cond = Cond::Reaches { rel: "billed_to" }; - -fn scalars(a: Answer) -> (i64, i64) { - match a { - Answer::Values(v) => match v.as_slice() { - [FoldValue::Scalar(c), FoldValue::Scalar(s)] => (*c, *s), - other => panic!("{other:?}"), - }, - other => panic!("{other:?}"), - } -} - -#[test] -fn a_condition_through_the_relation_matches_the_row_loop() { - let fx = Fx::new(); - let specs = [ - ("open_to_de", fx.spec(1, query(&[OPEN, TO_DE], COUNT_SUM))), - ("open_not_de", fx.spec(2, query(&[OPEN, NOT_DE], COUNT_SUM))), - ("reaches", fx.spec(3, query(&[REACHES], COUNT_SUM))), - ]; - let table = fx.table(&specs); - for n in SIZES { - let d = data(n); - let open = |i: usize| status(i) == "open"; - let cases: [(u8, Keep); 3] = [ - ( - 1, - Box::new(move |i| open(i) && reached(i).is_some_and(|p| country(p) == "DE")), - ), - ( - 2, - Box::new(move |i| open(i) && reached(i).is_some_and(|p| country(p) != "DE")), - ), - (3, Box::new(|i| reached(i).is_some())), - ]; - for (k, keep) in cases { - let got = scalars(d.run(&table, MethodKey([k; 16]), Frontend::Quack).unwrap()); - assert_eq!(got, oracle(n, keep), "method {k}, n={n}"); - } - } - // Anti-vacuity at the largest size: each guard changes the answer. - let n = 4097; - let any = oracle(n, |_| true).0; - let in_range = oracle(n, |i| partner_of(i) < PARTNERS).0; - let reach = oracle(n, |i| reached(i).is_some()).0; - assert!( - reach < in_range && in_range < any, - "{reach} {in_range} {any}" - ); - let not_de = oracle(n, |i| reached(i).is_some_and(|p| country(p) != "DE")).0; - let not_eq_de = oracle(n, |i| { - let p = partner_of(i); - !(p < PARTNERS && country(p) == "DE") - }) - .0; - assert!( - not_de < not_eq_de, - "!= through a relation must require a live target" - ); -} - -#[test] -fn the_relation_lowers_to_the_direct_quack_query() { - let fx = Fx::new(); - let specs = [("open_to_de", fx.spec(1, query(&[OPEN, TO_DE], COUNT_SUM)))]; - let table = fx.table(&specs); - let m = table.get(MethodKey([1; 16])).unwrap().as_ref().unwrap(); - let filter = || { - Filter::and([ - Filter::plane(Mask(0)), - Filter::cmp(Col(0), Cmp::EqU32(0)), - Filter::semijoin(Col(2), QPlane(0)), - Filter::eq_u32_via(Col(2), ForeignLane(0), 0), - ]) - }; - let direct = [ - Query { - filter: filter(), - agg: Agg::Count, - }, - Query { - filter: filter(), - agg: Agg::SumI32(Col(1)), - }, - ]; - assert_eq!(m.queries(), direct); - let programs: Vec<_> = direct.iter().map(|q| lower(q).unwrap()).collect(); - assert_eq!(m.programs(), programs.as_slice()); - // The key is a field the method reads. - assert!(m.required.has(2), "the fk is required"); - // `out(rel)` before `has` adds no second liveness test. - let specs = [ - ( - "explicit", - fx.spec(1, query(&[OPEN, REACHES, TO_DE], COUNT_SUM)), - ), - ("implied", fx.spec(2, query(&[OPEN, TO_DE], COUNT_SUM))), - ]; - let t = fx.table(&specs); - let get = |k: u8| t.get(MethodKey([k; 16])).unwrap().as_ref().unwrap(); - assert_eq!(get(1).queries(), get(2).queries()); -} - -#[test] -fn r2il_names_the_missing_gather() { - let fx = Fx::new(); - let specs = [ - ("to_de", fx.spec(1, query(&[TO_DE], COUNT_SUM))), - ("open", fx.spec(2, query(&[OPEN], COUNT_SUM))), - ]; - let table = fx.table(&specs); - let d = data(65); - match d.run(&table, MethodKey([1; 16]), Frontend::R2il) { - Err(CallError::FrontendGap { - frontend: Frontend::R2il, - missing: "GATHER", - }) => {} - other => panic!("{other:?}"), - } - // Silence twin: a method that reads no relation still runs on R2IL. - let got = scalars(d.run(&table, MethodKey([2; 16]), Frontend::R2il).unwrap()); - assert_eq!(got, oracle(65, |i| status(i) == "open")); -} - -#[test] -fn a_predicate_through_the_relation_is_reused_by_reference() { - let fx = Fx::new(); - let specs = [ - ( - "billed_in_de", - fx.spec(1, MethodBody::Select { filter: &[TO_DE] }), - ), - ( - "open_de", - fx.spec( - 2, - query( - &[OPEN, Cond::Method(MethodRef::Label("billed_in_de"))], - COUNT_SUM, - ), - ), - ), - ("direct", fx.spec(3, query(&[OPEN, TO_DE], COUNT_SUM))), - ]; - let table = fx.table(&specs); - let get = |k: u8| table.get(MethodKey([k; 16])).unwrap().as_ref().unwrap(); - assert_eq!(get(2).programs(), get(3).programs()); - let d = data(4097); - assert_eq!( - scalars(d.run(&table, MethodKey([2; 16]), Frontend::Quack).unwrap()), - oracle(4097, |i| status(i) == "open" - && reached(i).is_some_and(|p| country(p) == "DE")) - ); -} - -#[test] -fn a_relation_that_cannot_bind_is_refused_by_name() { - let fx = Fx::new(); - let refused = |filter: &'static [Cond<'static>]| { - let specs = [("m", fx.spec(1, query(filter, COUNT_SUM)))]; - fx.table(&specs) - .get(MethodKey([1; 16])) - .unwrap() - .clone() - .unwrap_err() - }; - assert_eq!( - refused(&[Cond::Reaches { rel: "shipped_to" }]), - PrepareError::UnknownRelation("shipped_to".into()) - ); - assert_eq!( - refused(&[Cond::Via { - rel: "billed_to", - field: "vat_id", - literal: "x", - negate: false, - }]), - PrepareError::UnknownField("billed_to.vat_id".into()) - ); - assert_eq!( - refused(&[Cond::Via { - rel: "billed_to", - field: "country", - literal: "XX", - negate: false, - }]), - PrepareError::UnknownValue { - field: "billed_to.country".into(), - literal: "XX".into() - } - ); - assert_eq!( - refused(&[Cond::Via { - rel: "billed_to", - field: "credit", - literal: "1", - negate: false, - }]), - PrepareError::KindMismatch("billed_to.credit".into()) - ); - // A target field the relation supplies no lane for. - let specs = [("m", fx.spec(1, query(&[TO_DE], COUNT_SUM)))]; - assert_eq!( - fx.table_with(&specs, &[("credit", 1)], &fx.line_claims) - .get(MethodKey([1; 16])) - .unwrap() - .clone() - .unwrap_err(), - PrepareError::NoForeignLane("billed_to.country".into()) - ); - // The key itself needs a proven claim like any field the method reads. - let unproven: Vec = fx - .line_claims - .iter() - .map(|c| FieldMap { - grade: if c.canonical == "partner" { - Grade::Hypothesized - } else { - c.grade - }, - ..*c - }) - .collect(); - assert_eq!( - fx.table_with(&specs, &[("country", 0)], &unproven) - .get(MethodKey([1; 16])) - .unwrap() - .clone() - .unwrap_err(), - PrepareError::UnmappedRequired("partner".into()) - ); -} - -#[test] -fn the_target_population_is_checked_at_the_call() { - let fx = Fx::new(); - let specs = [("to_de", fx.spec(1, query(&[TO_DE], COUNT_SUM)))]; - let table = fx.table(&specs); - let d = data(65); - let key = MethodKey([1; 16]); - match d.run_as(&table, key, Frontend::Quack, &[]) { - Err(CallError::ForeignUniverse { - concept: PARTNER, - method: PARTNER_U, - planes: None, - }) => {} - other => panic!("{other:?}"), - } - match d.run_as(&table, key, Frontend::Quack, &[(PARTNER, Universe(99))]) { - Err(CallError::ForeignUniverse { - planes: Some(Universe(99)), - .. - }) => {} - other => panic!("{other:?}"), - } - // The plain call supplies no foreign planes. - let masks = [d.line_live.as_slice()]; - let lanes = [ - LaneRef::U32(&d.status), - LaneRef::I32(&d.amount), - LaneRef::U32(&d.partner), - ]; - let planes = Planes { - n_rows: d.n, - masks: &masks, - lanes: &lanes, - }; - match table.call(key, &planes, LINE_U, Frontend::Quack) { - Err(CallError::ForeignUniverse { planes: None, .. }) => {} - other => panic!("{other:?}"), - } -} - -#[test] -fn out_composes_and_keeps_the_anchor() { - let v = Step::V(LINE); - let c = compose( - LINE, - &[ - v, - Step::Out("billed_to"), - Step::Has { - field: "country", - pred: Pred::Eq("DE"), - }, - Step::Count, - ], - ) - .unwrap(); - assert_eq!(c.filter(), &[REACHES, TO_DE]); - assert_eq!(c.folds(), &[Fold::Count]); - let c = compose( - LINE, - &[ - v, - Step::Out("billed_to"), - Step::Has { - field: "country", - pred: Pred::Neq("DE"), - }, - ], - ) - .unwrap(); - assert_eq!(c.filter(), &[REACHES, NOT_DE]); - let refuse = |steps: &[Step<'static>]| compose(LINE, steps).unwrap_err(); - let out = Step::Out("billed_to"); - assert_eq!( - refuse(&[v, out, Step::Out("owned_by"), Step::Count]), - GremlinRefusal::SecondHop("owned_by".into()) - ); - assert_eq!( - refuse(&[v, out, Step::Values("credit"), Step::Sum]), - GremlinRefusal::ValueThroughRelation("billed_to".into()) - ); - // `dedup().count()` after the hop is the distinct fold (D-CMH-G1-4b, - // tests/distinct.rs); `dedup()` alone would end on the distinct - // targets, which are no population of the anchor. - assert_eq!( - refuse(&[v, out, Step::Dedup]), - GremlinRefusal::DedupThroughRelation("billed_to".into()) - ); - assert_eq!( - refuse(&[v, out, Step::Where(MethodRef::Label("m")), Step::Count]), - GremlinRefusal::WhereThroughRelation("billed_to".into()) - ); - for hop in [Step::In("billed_to"), Step::Both("billed_to")] { - assert_eq!( - refuse(&[v, hop, Step::Count]), - GremlinRefusal::FanOut("billed_to".into()) - ); - } - // The composed body prepares and answers like the hand-written one. - let fx = Fx::new(); - let composed = compose( - LINE, - &[ - v, - Step::Out("billed_to"), - Step::Has { - field: "country", - pred: Pred::Eq("DE"), - }, - Step::Count, - ], - ) - .unwrap(); - let specs = [ - ("composed", fx.spec(1, composed.body())), - ( - "direct", - fx.spec(2, query(&[REACHES, TO_DE], &[Fold::Count])), - ), - ]; - let table = fx.table(&specs); - let get = |k: u8| table.get(MethodKey([k; 16])).unwrap().as_ref().unwrap(); - assert_eq!(get(1).programs(), get(2).programs()); -} - -/// FAILS IF: two relations to one target concept from different populations -/// prepare. A call names one population per concept, so the method could not -/// say which one the second relation's planes come from. -#[test] -fn two_populations_of_one_target_concept_are_refused() { - let fx = Fx::new(); - let glove = |universe| Glove { - concept: PARTNER, - basis: &fx.partner_basis, - claims: &fx.partner_claims, - binder: &Erp, - table: "partner", - reading: READING, - universe, - relations: &[], - }; - let (now, then) = (glove(PARTNER_U), glove(Universe(9))); - let lanes: &[(&str, u16)] = &[("country", 0)]; - let rel = |name, target, plane| Relation { - name, - fk: "partner", - target, - plane, - lanes, - }; - let prepare = |second| { - let rels = [rel("billed_to", &now, 0), rel("shipped_to", second, 1)]; - let line = Glove { - concept: LINE, - basis: &fx.line_basis, - claims: &fx.line_claims, - binder: &Erp, - table: "line", - reading: READING, - universe: LINE_U, - relations: &rels, - }; - let specs = [( - "m", - fx.spec( - 1, - query( - &[ - Cond::Reaches { rel: "billed_to" }, - Cond::Reaches { rel: "shipped_to" }, - ], - &[Fold::Count], - ), - ), - )]; - MethodTable::prepare(&line, &specs) - .get(MethodKey([1; 16])) - .unwrap() - .clone() - }; - assert_eq!( - prepare(&then).unwrap_err(), - PrepareError::ConflictingUniverse(PARTNER) - ); - // Silence twin: the same population through both relations prepares. - assert!(prepare(&now).is_ok()); -}