diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index 7f2b6ed79..a718e5bb1 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,11 @@ +## 2026-10-14 — d-holo-address-indexed-algebra-v1 — falsifiers H1, H5, H6 for the address-indexed algebra → `.claude/plans/d-holo-address-indexed-algebra-v1.md` + +**Status:** PRE-REGISTERED; nothing measured. + +- Tests the D-HOLO-0 candidate: bounded parallel evaluation of coordinate-aligned constraints across up to 64 thought branches with no query-time intermediate relations. +- H1 K-branch scaling (mask-risc per-branch and gate-once arms, a positional-popcount arm outside the contract, a snapshot baseline); H5 coordinate-native vs coordinate-crossing (prefix count, one identity's perspectives, reverse `memberOf` by scan vs maintained inverse with build and update cost); H6 closure of prefix/Morton-box restrictions as interval sets vs masks vs value sweep. +- Every input mask's construction is charged; work avoided is measured in coordinates examined; a `Range` needs a layout-order witness and gets an adversarial permuted run. + ## 2026-10-13 (2) — relalg-v1 — relational algebra over coordinate populations, rewritten before it runs → `.claude/plans/relalg-v1.md` **Status:** D-RA-1 MEASURED (prototype crate); D-RA-2..4 PROPOSAL. A prototype for D-CMR, not a competing IR. diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index dd1b4fcb1..f59d3d02f 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,7 @@ +## 2026-10-14 — main after #1548 (merged) + +- **On `main` (#1548, merged `e7c5f790`):** D-NALP-4, exact truth codes for deduction, induction and abduction in `causal_edge::isa::truth::code`, packed by `execute` and `syllogize` (0 of 2³² rows off an independent oracle). Revision and synthesis keep the `f32` path. Board: entries `2026-10-14-isa-truth-exact-codes.md` and `2026-10-14-revision-layer-invariants.md` (Gadamer invariants G1–G7, test-pinned; Rubikon R1–R4, specified, tests open). + ## 2026-10-13 — main after #1505, #1506, #1507–#1509, #1514, #1516, #1518, #1520, #1523, #1524, #1525, #1526, #1529, #1530, #1531, #1534, #1536, #1537, #1538, #1539, #1540, #1545, #1546 (merged) - **On `main`, live:** `.cargo/config.toml` defaults to `-Ctarget-cpu=native` with `config-v3.toml` as the portable baseline and `debug = 0` everywhere (#1505); `compute_dag_kahn` checks all 256 field positions (#1506); R2IL `MIN`/`MAX` lowering onto mask-risc (#1518, the part outside glove-parity). diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index c158885d4..3d1184038 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -1,3 +1,10 @@ +## 2026-10-14 — lance-graph PR #1548 (merged `e7c5f790`, branch `ccr-9f3b8075-74u47l-truth-exact`, head `0e15cff7`, 4 commits) — D-NALP-4: exact truth codes in `causal_edge::isa::truth`; revision-layer invariants + +- **Added:** `truth::code::{deduction, induction, abduction}` over `u8` codes (integer arithmetic, one half-up rounding); `Opcode::truth_code` routes the three rules there, and `execute` / `syllogize` pack its result. `tests/ce64_truth_exact.rs` (8 tests) against an oracle written independently of the crate (`tests/common/truth_law.rs`); `examples/truth_exact_probe.rs` checks all 2³² inputs per law. Board entry `2026-10-14-revision-layer-invariants.md`: Gadamer gate invariants G1–G7 and the Rubikon goal-revision specifications R1–R4. +- **Locked:** the exact per-row law lives in `isa::truth`, not in a second substrate. Measured: 0 of 2³² rows off the oracle per law, where the old `f32` path missed 2,413 / 4,352 / 4,352 (each by one confidence code). Against `main`, 5 of 16,000,000 results differ, each only in the confidence field, by one code. Counting layers answer "how much"; the Gadamer gate answers "whether", and runs first. +- **Deferred / open:** exact revision and synthesis (the both-at-255 case packs `(0, 0)`, pinned as a known defect); whether D-NALP-4/5 still need a lane terminal; R1–R4 tests; derived edges write witness and epistemic bits as 0; six pre-existing clippy lints in `causal-edge`. Harvested from draft #1544, which stays open. +- **Confidence:** high; four disable runs red (`truth_code` back to `f32`, `syllogize` back to `f32`, round down, ties-to-even); the legacy golden capture unchanged. + ## 2026-10-14 — lance-graph PR #1546 (merged `c21bd165`, branch `ccr-9f3b8075-74u47l-relalg`, head `1b2e8f5b`, 4 commits) — D-RA-1: relational IR rewritten before it runs, lowered through R2IL (a D-CMR prototype) - **Added:** `crates/lance-graph-relalg` (own workspace root, excluded). `Rel` describes a row set of one table (`All`, `Empty`, plane, one `I32`/`U32` comparison, `And`/`Or`/`Not`); `normalize` rewrites to a fixpoint (negation to the leaves, unit/zero, complement, value-interval fusion per lane, interval union with `≠` reconciliation, `U32` agreement, absorption); `symbolic` answers `∅` and a counted `U` with no program; `run` lowers to R2IL calls over `FoldDialect`. No strings in the IR (`Atom`/`Cmp`/`Measure` must be `Copy`). `tests/rewrites.rs`, 11 tests: rewritten == as written == row oracle over 400 generated descriptions × 5 measures. diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 5be1a1b64..2268c1432 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -4,6 +4,12 @@ Plan: `.claude/plans/relalg-v1.md`. Crate: `crates/lance-graph-relalg` (own work | D-id | scope | status | gate / falsifier | |---|---|---|---| +| **D-HOLO-H1** | K-branch evaluation of a coordinate-aligned Boolean constraint (plan `d-holo-address-indexed-algebra-v1`) | Measured 2026-10-14: MIXED, C flat in K; beats gate-once only with row-stored coverage and a narrow gate | == row oracle; total cost at K = 64 ≤ 2× K = 1 and ≤ 0.5× best baseline | +| **D-HOLO-H5** | Coordinate-native vs coordinate-crossing: prefix count, perspectives, reverse `memberOf` with inverse build/update cost | Measured 2026-10-14, corrected after review: Q1 CONFIRMED at N = 100 (31–37×), FAILS at N = 1 (1.3–1.4×, the occupancy build dominates); Q2 passes only with row words stored; plane inverse refuted on Q3b (break-even > 1,000) | == row oracle; Range ≥ 5× sweep; break-even N reported; permuted-tile run disagrees | +| **D-HOLO-H6** | Closure of address restrictions as interval sets vs masks vs value sweep | Measured 2026-10-14: HOLDS; symbolic cheapest in all 24 cells | == row oracle; symbolic cost flat in area, linear in interval count | +| **D-HOLO-H6b** | Product normal form vs Morton intervals vs dense mask vs hybrid on the Context × Operation reading of the `u8:u8` tile; construction, normalization, ∩/∪/¬, emptiness, equality, bytes, reuse at N = 1..1,000 | Queued (pre-registered, not run) | == row oracle; structured patterns ≤ 0.5× dense at N = 100; hybrid ≤ 1.25× dense on adversarial patterns | +| **D-HOLO-H8** | One formula in three domains: logical algebra, physical realization and semantic leakage compared separately | Queued (pre-registered, not run) | no semantic leakage; families grow only with representation and value algebra | +| **D-HOLO-H9** | Relational emptiness decided symbolically (empty operand, disjoint domains, contradictory constraints) vs by fold | Queued (pre-registered, not run) | zero false empties; fraction decided symbolically reported | | **D-RA-1** | `Rel` + `normalize` + `symbolic` + R2IL lowering | Shipped #1546 `c21bd165` (11 tests, 11 disable runs red; four comparisons on one lane 8 → 2 ops; 70/2,000 small descriptions refused as written, all run rewritten) | == row oracle over 400 generated descriptions × 5 measures | | **D-RA-2** | Rules into the contract set IR (D-CMR-1/-6); relalg keeps only lowering or is deleted | Queued (maintainer decision) | relalg tests pass unchanged against the contract IR | | **D-RA-3** | Datalog rules as algebra values; names are labels resolved through the label codebook, never strings in a rule | Queued (after D-RA-2) | codebook-built rules == hand-written `Rel`; unknown label refused at resolution | @@ -104,7 +110,7 @@ Plan: `.claude/plans/nal-polyfill-v1.md`. Template: D-SQLP. No opcode, tenant or | **D-NALP-1** | Statement layout + declared ClassView reading; additive `WideFieldMask` projection path on `ClassView`; binder's mask; term width decision | Partial: #1525 merged `08635ed` (NodeGuid reading `nal-statement-v1`, term width u16, aperture selection); ClassView wide projection and binder mask Queued | only the mask's lanes are read; undeclared reading and masked-out field refuse; Small = Wide answers | | **D-NALP-2** | Query procedures: select, truth threshold, choice by confidence, support count | Shipped #1525 `08635ed` (`crates/lance-graph-nal`: NodeGuid key, byte apertures; 7 oracle tests, 7 disable runs red; 19–248 ns at 1,048,576 rows); support count and argmax not built | equal to a row-at-a-time oracle; one-pattern threshold disable red; no new opcode | | **D-NALP-3** | Revision as `GroupCrossPowerSumsI32` over stored `w`/`f` | Shipped #1538 `4ceaab3` (`nal::Revision`, key = evidence class from the stamps; refuses overlap and missing provenance; 11 tests, 7 disable runs red; ONA's mid-fold saturation is a documented divergence, the fold keeps the exact sum) | equal to exact integer revision; OpenNARS revision points; duplicate evidence counts once | -| **D-NALP-4** | Per-row truth law into a lane: `MapPairU8` vs fixed-point multiply-high | Partial, in PR: the exact scalar law for deduction / induction / abduction is `causal_edge::isa::truth::code`, packed by `execute` and `syllogize` (0 of 2³² rows off the oracle; harvested from draft #1544); whether a lane terminal is still needed is open (entry `2026-10-14-isa-truth-exact-codes.md`) | insufficiency proof; per-row parity with `isa::truth`, error in codes; one shape also serves SQL nullable arithmetic | +| **D-NALP-4** | Per-row truth law into a lane: `MapPairU8` vs fixed-point multiply-high | Partial, shipped (#1548 `e7c5f790`): the exact scalar law for deduction / induction / abduction is `causal_edge::isa::truth::code`, packed by `execute` and `syllogize` (0 of 2³² rows off the oracle; harvested from draft #1544); whether a lane terminal is still needed is open (entry `2026-10-14-isa-truth-exact-codes.md`) | insufficiency proof; per-row parity with `isa::truth`, error in codes; one shape also serves SQL nullable arithmetic | | **D-NALP-5** | Deduction / induction / abduction / analogy over populations | Queued (after D-NALP-4) | equals pairwise scalar rule; abduction direction test red for the reversed statement | | **D-NALP-6** | `lance-graph-nal`: typed `nal::{Operation}` surface (`NalOp`), parser (tokens only), binder over the reverse label index, `NAL::narsese()`, typed refusals | Queued | text and typed calls give byte-identical `Program`s; no string in the plan, rows or wire; ambiguous / unknown label refuses; lowers from the bound plan; no fallback; no `Program` outside procedures | | **D-NALP-7** | Oracle parity on NAL-1/2 fixtures vs pinned OpenNARS, `jc::reliability` | Queued | within D-HL-4 tolerance per rule | diff --git a/.claude/board/entries/2026-10-14-d-holo-falsifiers.md b/.claude/board/entries/2026-10-14-d-holo-falsifiers.md new file mode 100644 index 000000000..764f467e6 --- /dev/null +++ b/.claude/board/entries/2026-10-14-d-holo-falsifiers.md @@ -0,0 +1,30 @@ +# 2026-10-14 — D-HOLO falsifiers H1, H5, H6 (measured) + +**Status:** MEASURED (v4, avx512f). Plan `.claude/plans/d-holo-address-indexed-algebra-v1.md`, +§ Results (2026-10-14). Probes: `crates/lance-graph-mask-risc/examples/holo_{k_branch,coord,interval}_probe.rs`. + +**Outcome:** +- **H1 (K branches): MIXED.** The bit-parallel arm over per-row words is flat in K (0.84–1.47× + from K = 1 to 64). With 100-query amortization at s = 10 % it beats gate-once-then-AND only on + the 4,096-row aperture (4.8 vs 31.0 µs); on the full tile gate-once is as fast (71.2 vs 71.7 µs). + The verdict depends on storing coverage by row: with per-thought planes stored, gate-once wins + every reference cell. +- **Correction after review (`30fb38dd`):** H5 charged no input construction and both probes + recognised lowerings inside the timed path. Re-run: H5 Q1 holds at N = 100 (31–37×) but fails at + N = 1 (1.3–1.4×), because building the occupancy plane (about 110 µs) dominates both arms; Q2's + 1 ns holds only with row words stored. H1's verdict is unchanged. Details: plan § Results, + corrected after review. +- **H5 (native vs crossing), as first measured: CONFIRMED.** A prefix `Range` reads exactly the prefix's occupancy + words and is 292–573× faster than a key sweep; a permuted key lane makes it disagree with the + oracle, so it needs a layout-order witness. A row popcount answers all 64 perspectives in 1 ns. + Reverse `memberOf` costs the aperture's rows without a seal-time inverse. The per-group plane + inverse does not pay back on prefix-restricted queries within 1,000 queries (1,114–1,740); CSR + does (≤ 546), at 6–13 µs per inserted edge. +- **H6 (closure): HOLDS.** Formulas over Morton boxes and prefixes stay interval sets under ∩, ∪ + and ¬, match the oracle, cost the same as box area grows 256× at a fixed interval count, and were + cheaper than full-tile masks and value sweeps in all 24 cells. Quack's `fold_ranges` already + covers the ∩-of-ranges case. + +**OPEN:** the stored orientation of thought coverage; a vectorized positional popcount; a single +multi-interval `Range` write (not built, no new op added); external baselines (DuckDB, +differential dataflow, TACO). diff --git a/.claude/board/entries/2026-10-14-isa-truth-exact-codes.md b/.claude/board/entries/2026-10-14-isa-truth-exact-codes.md index 8bf67facb..b6f7d25da 100644 --- a/.claude/board/entries/2026-10-14-isa-truth-exact-codes.md +++ b/.claude/board/entries/2026-10-14-isa-truth-exact-codes.md @@ -1,6 +1,6 @@ # 2026-10-14 — Exact truth codes in `causal_edge::isa::truth` (D-NALP-4 harvest) -**Status:** IN PR (branch `ccr-9f3b8075-74u47l-truth-exact`). Harvested from +**Status:** MERGED (#1548, `e7c5f790`). Harvested from the draft #1544, which stays open as a reminder and is not merged. **DECISION:** the exact per-row truth law lives where NARS truth already diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 1b55917e6..1e5d891d5 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,12 +25,13 @@ 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. -328 entries, 2026-08-06 .. 2026-10-14. +329 entries, 2026-08-06 .. 2026-10-14. | date | entry id | finding | file | |---|---|---|---| | 2026-10-14 | `revision-layer-invariants` | | [2026-10-14-revision-layer-invariants.md](2026-10-14-revision-layer-invariants.md) | | 2026-10-14 | `D-NALP-4` | | [2026-10-14-isa-truth-exact-codes.md](2026-10-14-isa-truth-exact-codes.md) | +| 2026-10-14 | `D-HOLO` | | [2026-10-14-d-holo-falsifiers.md](2026-10-14-d-holo-falsifiers.md) | | 2026-10-13 | `D-PROD-V4` | | [2026-10-13-production-targets-v4.md](2026-10-13-production-targets-v4.md) | | 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) | diff --git a/.claude/plans/d-holo-address-indexed-algebra-v1.md b/.claude/plans/d-holo-address-indexed-algebra-v1.md new file mode 100644 index 000000000..94ca9f6fb --- /dev/null +++ b/.claude/plans/d-holo-address-indexed-algebra-v1.md @@ -0,0 +1,578 @@ +# D-HOLO — address-indexed algebra: falsifiers H1, H5, H6 (v1) + +**Status:** MEASURED 2026-10-14 (H1, H5, H6); H6b, H8, H9 PRE-REGISTERED, not +run. The pre-registration below is unchanged; results are in § Results +(2026-10-14), the next falsifiers in § Next falsifiers. + +## Overview + +The D-HOLO-0 research verdict (session scratchpad, 2026-10-14) classified the +address-indexed algebra (`query[$address]`) as a DISTINCT COMPOSITION with one +CANDIDATE CONTRIBUTION: + +> bounded parallel evaluation of coordinate-aligned relational constraints, +> independent of the number of selected thought branches up to the physical mask +> width, with no query-time intermediate relation materialization. + +It named three classes of computation, coordinate-native, coordinate-crossing and +semantic, and three kinds of materialization: +- representation maintenance (allowed, charged at seal and update); +- intermediate relational materialization (forbidden except at a named terminal); +- semantic reconciliation. + +This plan pre-registers the three falsifiers the operator asked to run first. + +DECISION (operator, 2026-10-14): run H1, H5, H6. SCOPE: one dense class tile, +in-process baselines only. BASIS: the D-HOLO-0 verdict. REVISIT WHEN: a +falsifier fails, or an external baseline (DuckDB, differential dataflow, TACO) +is run. + +## Checklist + +- [x] D-HOLO-0b: this pre-registration and its board rows, committed before any probe exists +- [x] D-HOLO-H1: K-branch probe, oracle tests, disable runs, v4 run, result +- [x] D-HOLO-H5: coordinate-native vs coordinate-crossing probe, same +- [x] D-HOLO-H6: closure of address restrictions probe, same +- [x] Results section, board entry, status rows + +## Accounting rules (apply to every falsifier) + +1. **Every resident mask a program reads is charged.** Its construction cost (ns + per row, bytes) is reported beside the query cost, and the decision uses the + total = construction amortized over the stated number of queries + query. A + program that only wins because an input mask was assembled outside the timed + region has not won. +2. **Work avoided is measured, not inferred.** Each arm reports the coordinates + it examined (64-bit words read, rows evaluated), next to its time. Cost should + follow admitted coordinates, not the population. +3. **A `Range` over row indexes requires a layout-order witness.** On the dense + tile the order holds by construction (row = Morton HEEL code). This is a + correctness obligation, so H5 includes one adversarial run on a permuted tile, + where `Range` must disagree with the sweep and the oracle. +4. **Value access is mathematical, not semantic.** Reading a value lane (`v < t`, + a foreign key through `Gather`) is part of the substrate; only interpretation + is optional. +5. Every arm is checked against a row-at-a-time oracle on every cell. Timings: + v4 only, the tier printed by the program, median of 11 runs, ns per query and + ns per element. Allocations per query come from a counting allocator; the + mask-risc arms must allocate 0 after scratch setup. + +## Common fixture + +- One dense class tile: 65,536 rows; row r has Morton HEEL code r + (x = even bits, y = odd bits); key lane u64 = (classid << 32) | (r << 16). +- Generated deterministically by the probe from a fixed seed; no fixture files. +- mask-risc arms run through `exec::execute_into` on the real executor. +- Out-of-contract arms are plain Rust in the same probe and are labelled as such. + +## H1 — K-branch evaluation of a coordinate-aligned Boolean constraint + +**Data.** +- Value lane v: i32, uniform in [0, 1000). +- Base predicate Q = v < t, with t chosen for selectivity s in {1 %, 10 %, 50 %}. +- Aperture P: the whole tile, or one HEEL nibble prefix (4,096 rows). +- K in {1, 8, 64} branches. Branch k covers each row independently with + probability 10 %. The same coverage is held as per-thought planes M_k + (1,024 words each) and as per-row written-by words w. + +**Query.** For each k in the selection T: count(P ∧ Q ∧ M_k). The answer is K +counts. + +**Arms.** +- **A, mask-risc per branch.** K programs, each `Range` → `LtI32` under it → + `And` with `Plane(k)` → `Count`. P ∧ Q is recomputed in every program. +- **B, mask-risc, gate once.** One program keeps G = P ∧ Q into a caller mask + (`Out::Mask`), then K programs `And(Plane(G), Plane(k))` → `Count`. G is the + cross-program population mask from the verdict's materialization audit; its + cost is charged. +- **C, bit-parallel over row words (outside the contract).** One pass over the + rows set in G, adding w[r] & T into 64 position counters. G comes from B's + first program, charged. +- **D, snapshot baseline.** Per branch, a materialized list of the branch's row + ids. The count evaluates P ∧ Q per listed row; K passes. The build cost of the + lists is reported and charged. + +**Charged inputs.** Building M_k (per-thought planes) and w (per-row words) from +the same coverage, ns per row, reported once. + +**Decision.** Reference cells: s = 10 %, both apertures. +- The candidate survives at a cell if C's total cost at K = 64 is at most 2× its + cost at K = 1 and at most 0.5× the best of A, B, D at K = 64. +- It fails if C grows linearly in K, or if A, B or D comes within 2× of C at + K = 64. + +**Not run in this pass:** DuckDB on materialized snapshots, differential dataflow +with a 64-element partial order, TACO. D is the in-process snapshot stand-in. + +## H5 — coordinate-native vs coordinate-crossing (F2c) + +**Q1: `identity[$prefix].count()`.** +- Prefix of p in {1, 2, 3} HEEL nibbles (4,096 / 256 / 16 rows). +- Occupancy plane O, density in {100 %, 30 %}. +- Arms: + - **R:** `Range` under `Plane(O)` → `Count`, fused, reading only O's words in + the range. + - **S:** `MatchU64` sweep on the key lane under `Plane(O)` → `Count`. + - **closed form:** 16^(4-p) when occupancy is 100 %, no read. +- Adversarial: R on a tile whose key lane is permuted must disagree with S and + the oracle. + +**Q2: `identity[$node].thoughts($all).count()`.** 64 thoughts at 10 % coverage. +- **W:** popcount(w[row]), one register operation. +- **P:** the contract form, 64 mask-risc programs `Range{row, row+1}` under + `Plane(t)` → `Count`, summed. +- **B:** 64 bit reads from the per-thought planes, plain Rust. + +**Q3: reverse `memberOf`.** +- Each identity has m = 4 membership lanes (u32 group id, 0 = none), over 1,024 + groups. +- Two distributions: uniform, and skewed (Zipf exponent 1). +- Queries, over 256 sampled groups: + - Q3a `reverse(g).count()`; + - Q3b `reverse(g) ∧ prefix(p = 1)`, then count; + - Q3c `reverse(g) ∧ (v < t)`, then count. The inverse orders neither the + value lane nor this predicate. +- Arms: + - **S, scan in mask-risc:** 4 `EqU32` predicates ORed, under `Range` for Q3b, + `And LtI32` for Q3c, then `Count`. + - **I, CSR inverse built at seal:** offsets per group plus sorted member rows. + Q3a is an offset difference, Q3b a binary search, Q3c per-member evaluation + of `v < t`. + - **IM, inverse as per-group mask planes (1,024 × 8 KiB):** mask-risc `And` / + `Count`. +- Maintenance metrics: + - build time (ns per membership edge) and bytes held, for I and IM; + - cost of inserting one edge (CSR shift vs one bit set); + - break-even query count N = build cost ÷ (scan cost − inverse cost), per query + shape. + +**Decision.** +- Coordinate-native advantage confirmed if: + - Q1-R reads exactly the occupancy words in the prefix; + - Q1-R is at least 5× faster than Q1-S at every cell; + - Q2-W is under 5 ns. +- Coordinate-crossing classification confirmed if: + - Q3-S's cost is proportional to the aperture's rows; + - every arm that beats the scan by more than 5× needs a seal-time structure. +- "A maintained inverse is always better" is refuted if, after build cost: + - the break-even N exceeds 1,000 queries for a shape; or + - Q3c is slower on I than on S. +- The adversarial run must disagree; if it agrees, the fixture cannot detect an + order violation and the run is invalid. + +## H6 — closure of address restrictions + +On the dense tile (row = Morton code), restrictions are: +- prefix intervals; +- Morton boxes [x0, x1) × [y0, y1), decomposed into maximal aligned quadtree + intervals; +- their unions and complements. + +**Formulas.** +- F1 = B1 ∩ P +- F2 = B1 ∪ B2 +- F3 = ¬B1 ∩ P +- F4 = (B1 ∪ B2) ∩ ¬B3 + +**Box shapes.** +- aligned squares of side 16, 64 and 256 (one interval each); +- unaligned squares; +- thin boxes 256 × 1 and 1 × 256 (many intervals). + +**Arms.** +- **S, symbolic:** interval sets as sorted (lo, hi) vectors, ∩ / ∪ / ¬ by merge, + count = Σ length. No row is touched. +- **M, masks in mask-risc:** each leaf as `Range` ops ORed into a slot, then the + formula's `And` / `Or` / `Not`, then `Count`, in one program per formula. +- **V, value sweep:** x and y as u32 lanes, `GeI32` / `LtI32` per box bound, + `And` / `Or` / `Not` → `Count`. No intervals at all. + +**Decision.** +- Closure holds if: + - every S result equals the oracle and is again an interval set; + - at a fixed interval count, S's cost stays within 2× as box area grows 256× + (aligned side 16 → 256); + - S's cost grows with the interval count, not the population. +- The interval count at which M or V becomes cheaper than S is reported. It is + not a pass/fail criterion. + +## Amendment before any run (2026-10-14) + +H1 and H5 name no query count for amortizing construction. Fixed here, before +any probe exists: construction is amortized over N = 1 and N = 100 queries, and +each decision is stated at both. H5's break-even N stays as defined. + +## Boundaries + +- No change to mask-risc, Quack, the contract or ndarray. The probes are examples. +- No new opcode or terminal. Arm C (H1) and the inverse arms (H5) are labelled + out-of-contract, because no existing terminal expresses them. +- No external dependency. + +## Results (2026-10-14) + +All runs: tier printed by the program, `v4 (avx512f)`, 4-core VM, release, +debug 0, median of 11. Every arm matched the row oracle in every cell (asserted +by the probes and by their `cargo test` arms). Every mask-risc arm allocated 0 +bytes per query after scratch setup. + +Probes and disable runs: +- `examples/holo_k_branch_probe.rs` (H1). Five disables, all red: C without its + gate, A without both extent and `Range`, D without the aperture lower bound, + B reading the wrong plane, the gate without the value predicate. A without + only the extent stays green, because `Range` alone enforces the aperture. +- `examples/holo_coord_probe.rs` (H5). Four disables, all red: the sweep without + HEEL care bits, CSR without the aperture lower bound, the scan with 3 of 4 + lanes, the permuted test without its permutation. +- `examples/holo_interval_probe.rs` (H6). Six disables, all red: complement + dropping the trailing interval, intersection always advancing one side, box + children in the wrong Morton order, `push` never merging adjacent intervals, + V without the y0 bound, M emitting only the first interval. + +### H1 — K-branch evaluation: MIXED; depends on the stored orientation + +Construction: thought planes 29.27 ns/row (512 KiB, 1.92 ms per tile); +snapshot lists 55.16 ns/row (1,636 KiB, 3.62 ms). The probe takes per-row words +w as the stored coverage, so A and B are charged the plane build and C is not. + +s = 10 %, ns per query: + +| aperture | K | A | B | C | D | +|---|---|---|---|---|---| +| 65,536 | 1 | 23,025 | 23,630 | 73,415 | 9,694 | +| 65,536 | 64 | 1,528,007 | 51,971 | 71,729 | 587,145 | +| 4,096 | 1 | 1,708 | 2,022 | 4,783 | 699 | +| 4,096 | 64 | 103,100 | 11,814 | 4,814 | 42,863 | + +- C is flat in K: C(64)/C(1) is 0.84–1.47 over all 18 cells. The K-independence + half of the candidate holds. +- Rule applied at the reference cells (C ≤ 2× its K = 1 cost, and ≤ 0.5× the + best of A, B, D at K = 64, construction amortized): + +| aperture | N | best other (total) | C | verdict | +|---|---|---|---|---| +| 65,536 | 1 | B 1,970 µs | 72 µs | PASS | +| 65,536 | 100 | B 71.2 µs | 71.7 µs | FAIL (B within 2×, in fact faster) | +| 4,096 | 1 | B 1,930 µs | 4.8 µs | PASS | +| 4,096 | 100 | B 31.0 µs | 4.8 µs | PASS | + +- The verdict depends on which orientation is stored. If the planes M_k are the + stored form, C must build w instead (same pass shape), and B wins every + reference cell at N = 100 and at N = 1. The candidate's advantage is + therefore not a property of the algebra; it is a property of storing coverage + by row. +- B is already sub-linear in K: the gate dominates, and each further branch + costs about 0.45 µs (one AND + popcount over 1,024 words, about 0.44 ns per + word). C pays about 11 ns per gated row in a scalar 64-counter loop, so it + loses at high gate density (s = 50 %, full tile: C 252 µs vs B 52 µs). +- Not measured: a vectorized positional popcount for C; external baselines + (DuckDB, differential dataflow, TACO). +- Run-to-run noise: a second run (after a lint-only change to C's loop) moved + absolute times by up to about 40 % on this 4-core VM (full tile, K = 64: B + 52.0 → 36.4 µs, C 71.7 → 60.7 µs; 4,096 rows, K = 64: B 11.8 → 6.6 µs, C 4.8 → + 3.6 µs). Both verdicts are the same in both runs (full tile N = 100: B 55.6 vs C + 60.7 µs, FAIL; 4,096 rows N = 100: B 25.8 vs C 3.6 µs, PASS). Differences under + 2× in this table are not evidence. + +### H5 — coordinate-native vs coordinate-crossing: CONFIRMED, one refutation + +Q1 `identity[$prefix].count()`: + +| occupancy | p | R ns | S ns | R words read | S rows read | R / S | +|---|---|---|---|---|---|---| +| 100 % | 1 | 154.8 | 62,076 | 64 | 65,536 | 401× | +| 100 % | 2 | 124.1 | 61,628 | 4 | 65,536 | 497× | +| 100 % | 3 | 118.5 | 59,642 | 1 | 65,536 | 503× | +| 30 % | 1 | 125.0 | 36,497 | 64 | 65,536 | 292× | +| 30 % | 2 | 65.3 | 35,267 | 4 | 65,536 | 540× | +| 30 % | 3 | 61.5 | 35,248 | 1 | 65,536 | 573× | + +- R reads exactly the occupancy words of the prefix; at p = 3 the 60–120 ns is + call overhead, not work. +- Adversarial permuted key lane: R 1,212, sweep 1,214, oracle 1,214. R + disagrees, so the fixture detects an order violation and the run is valid. + The layout-order witness is a correctness obligation, as registered. +- Q2 per identity: W (row word popcount) 1.00 ns, B (64 plane bit reads) + 55.08 ns, P (64 mask-risc programs) 4,841 ns. W under 5 ns: PASS. The 1.00 ns + is at the timer floor. +- Coordinate-native advantage: CONFIRMED. + +Q3 reverse `memberOf` (1,024 groups, 4 lanes): + +| dist | query | S ns | I (CSR) ns | IM (planes) ns | break-even N CSR / planes | +|---|---|---|---|---|---| +| uniform | Q3a count | 59,451 | 42.7 | 393 | 36 / 115 | +| uniform | Q3b ∧ prefix | 3,971 | 23.6 | 84 | 546 / 1,740 | +| uniform | Q3c ∧ v < t | 81,220 | 234.0 | 6,980 | 27 / 91 | +| zipf | Q3a count | 57,909 | 22.8 | 500 | 34 / 73 | +| zipf | Q3b ∧ prefix | 3,861 | 18.5 | 101 | 509 / 1,114 | +| zipf | Q3c ∧ v < t | 77,398 | 465.7 | 5,388 | 25 / 58 | + +Maintenance: CSR builds at 10.25–10.95 ns/edge (749–772 KiB) and costs +6,386–13,225 ns per inserted edge; the plane inverse builds at 21.95–34.40 +ns/edge (8,200 KiB) and costs 1.8–2.2 ns per inserted edge. + +- The scan's cost follows the aperture: Q3a 65,536 rows vs Q3b 4,096 rows, time + ratio 15.0 for a row ratio of 16. +- Every arm beating the scan by more than 5× is a seal-time structure. This + holds only over the arms tried; the probe contains no structure-free + alternative to S, so it is a non-finding rather than a proof. +- Coordinate-crossing classification: CONFIRMED in this sense. +- "A maintained inverse is always better": REFUTED for the plane inverse on + Q3b (break-even 1,740 and 1,114 queries, over the 1,000 bound). Not refuted + for CSR on these shapes (worst 546). Q3c is not slower on I than on S. + +### H6 — closure of address restrictions: HOLDS + +Leaves are three boxes and one prefix; `intervals_in` counts their intervals. +S_eval evaluates the formula over decomposed leaves; S_total also decomposes the +boxes. M and V run prepared programs. + +| shape | intervals in | S_eval ns | S_total ns | M ns | V ns | +|---|---|---|---|---|---| +| aligned 16 | 4 | 45–81 | 264–333 | 722–1,450 | 27,662–104,522 | +| aligned 64 | 4 | 47–79 | 190–240 | 678–1,223 | 31,399–87,844 | +| aligned 256 | 4 | 46–84 | 131–196 | 816–1,283 | 49,036–134,829 | +| unaligned | 269 | 722–2,872 | 7,187–11,613 | 77,189–117,139 | 48,019–93,460 | +| thin 256×1 | 321 | 125–2,259 | 9,814–21,487 | 52,027–154,810 | 53,188–170,007 | +| thin 1×256 | 641 | 105–7,440 | 9,117–25,420 | 92,573–384,066 | 29,252–140,791 | + +(Ranges are over F1–F4; per-cell values in the probe output.) + +- Every S result equals the oracle and is again a sorted, disjoint, + non-adjacent interval set (asserted on every cell). +- At 4 intervals, S_eval is flat as box area grows 256× (side 16 → 256): 45–81 + vs 46–84 ns. PASS. +- S grows with the interval count: about 7–20 ns per input interval for F4. + The population was not varied; S reads no row, so its independence of the + population is structural, not measured. +- Crossover: none against S. S was the cheapest arm in all 24 cells, decomposition + included. Between the two row-level arms, M (Range per interval) beats V + (value sweep) at few intervals (aligned: about 1 µs vs 28–135 µs), and V beats + M in 8 of the 12 many-interval cells. M costs about 0.24–0.30 µs per op, + because each `Range` writes the whole tile; V costs about 7 µs per + predicate over 65,536 rows. +- Two F3 cells (aligned 64 and 256) and one F1 cell (thin 1×256) answer 0; they + exercise only the empty-result path. +- Prior art: quack's `fold_ranges` already intersects ranges on one column + symbolically before lowering, and `photo_probe`'s `support` takes the hull + for OR and gives up on NOT. S is the exact closure under ∩, ∪ and ¬ of that + same idea. + +### What this changes for the D-HOLO-0 candidate + +- Kept: coordinate-native operations (prefix count, row perspective) cost + what the admitted coordinates cost, by two to three orders of magnitude over + a sweep, given a layout-order witness. +- Kept: address restrictions are closed as interval sets and can be answered + symbolically before any row is read. +- Narrowed: K-branch independence exists (C is flat in K), but its advantage + over gating once and ANDing per branch holds only when coverage is stored by + row and the gated population is small. +- Narrowed: a maintained inverse is a cost, not a default; the per-group plane + inverse does not pay back on aperture-restricted queries within 1,000 queries. + +### Open + +- Whether coverage is stored by row or by thought. H1's verdict turns on it. +- A vectorized positional popcount for C (not tested). +- One `Range`-set write per program instead of one full-tile write per + interval. Not built: no new op was added for this pass. +- External baselines (DuckDB, differential dataflow, TACO). + +## Results, corrected after review (2026-10-14) + +The Codex review of #1550 found two accounting errors in the H1 and H5 probes. +Both are fixed in `30fb38dd`; the results above are left as first measured. + +- **Lowering recognised inside the timed path.** Every timed call ran + `Program::compile()`, so arms A and B paid K recognitions per query, and H5's + Q3 also rebuilt its scan program for every group. The probes now store + `CompiledProgram` and time execution only. +- **H5 charged no input construction**, against accounting rule 1. Q1 now builds + its occupancy plane from a presence lane and charges it to both arms; Q2 + measures both coverage orientations. H1 now also measures row words built from + planes. + +Re-run, v4 (avx512f), same fixtures. + +### H1: unchanged verdict, B faster + +Construction: planes from row words 20.56 ns/row (1.35 ms per tile); row words +from planes 17.17 ns/row (1.13 ms); snapshot lists 37.67 ns/row. + +s = 10 %, K = 64: A 924,205, B 33,762, C 54,862, D 394,664 ns. Without the +repeated `compile()`, B dropped from 51,971 to 33,762 ns; each further branch +now costs about 0.31 µs. + +| stored form | aperture | N | B total | C total | verdict | +|---|---|---|---|---|---| +| row words | 65,536 | 1 | 1,381 µs | 55 µs | PASS | +| row words | 65,536 | 100 | 47.2 µs | 54.9 µs | FAIL | +| row words | 4,096 | 1 | 1,354 µs | 3.6 µs | PASS | +| row words | 4,096 | 100 | 20.2 µs | 3.6 µs | PASS | +| planes | 65,536 | 100 | 33.8 µs | 66.1 µs | FAIL | +| planes | 4,096 | 100 | 6.7 µs | 14.8 µs | FAIL | + +C stays flat in K (0.58–1.08 across all cells). The verdict still turns on +which orientation is stored. + +### H5: Q1 fails at N = 1, passes at N = 100 + +Building the occupancy plane costs 109–113 µs (1.66–1.73 ns/row). Both arms +read it, so both are charged: + +| occupancy | p | R total, N = 1 | S total, N = 1 | ratio | R total, N = 100 | S total, N = 100 | ratio | +|---|---|---|---|---|---|---|---| +| 100 % | 1 | 109.1 µs | 144.3 µs | 1.32× | 1.17 µs | 36.4 µs | 31× | +| 100 % | 2 | 109.1 µs | 150.3 µs | 1.38× | 1.21 µs | 42.4 µs | 35× | +| 100 % | 3 | 109.1 µs | 150.3 µs | 1.38× | 1.15 µs | 42.3 µs | 37× | +| 30 % | 1 | 113.2 µs | 152.8 µs | 1.35× | 1.22 µs | 40.8 µs | 33× | +| 30 % | 2 | 113.2 µs | 151.4 µs | 1.34× | 1.19 µs | 39.4 µs | 33× | +| 30 % | 3 | 113.2 µs | 151.9 µs | 1.34× | 1.20 µs | 39.9 µs | 33× | + +- At N = 1 the ≥ 5× rule FAILS in every cell: building the plane is the query. + At N = 100 it PASSES in every cell. The coordinate-native advantage is real + only once the occupancy plane is maintained, not rebuilt per query. +- At 100 % occupancy neither arm needs the plane (the closed form answers from + `16^(4-p)`). The probe charges it anyway, because it reads it. +- The adversarial permuted run still disagrees (Range 1,212, sweep and oracle + 1,214). + +Q2 per identity: + +| stored form | N | W | B | P | +|---|---|---|---|---| +| row words | 1 | 1.0 ns | 1.18 ms | 1.18 ms | +| row words | 100 | 1.0 ns | 11.9 µs | 15.2 µs | +| planes | 1 | 1.01 ms | 54.3 ns | 3.38 µs | +| planes | 100 | 10.1 µs | 54.3 ns | 3.38 µs | + +The "W under 5 ns" rule passes only when row words are the stored form, the same +dependence as H1. + +Q3, after compiling the scan programs once: + +| dist | query | S ns | I ns | IM ns | break-even N CSR / planes | +|---|---|---|---|---|---| +| uniform | Q3a count | 99,578 | 42.9 | 469 | 22 / 71 | +| uniform | Q3b ∧ prefix | 6,636 | 23.4 | 50 | 339 / 1,073 | +| uniform | Q3c ∧ v < t | 101,843 | 243.2 | 7,514 | 22 / 75 | +| zipf | Q3a count | 62,141 | 21.0 | 361 | 56 / 126 | +| zipf | Q3b ∧ prefix | 3,621 | 17.7 | 52 | 958 / 2,175 | +| zipf | Q3c ∧ v < t | 100,599 | 355.9 | 5,416 | 34 / 82 | + +- The plane inverse is still refuted on Q3b (break-even 1,073 and 2,175, over + 1,000). CSR now comes close (958 on zipf Q3b). +- This run was noisy: the uniform scan took 99.6 µs against 59.5 µs before, and + the CSR insert 40.5 µs against 13.2 µs. The scan's cost still follows the + aperture (time ratio 15.0 and 17.2 for a row ratio of 16). + +### Corrected verdicts + +| falsifier | first run | after review | +|---|---|---| +| H1 | MIXED, depends on stored orientation | unchanged | +| H5 Q1 (coordinate-native) | CONFIRMED | CONFIRMED at N = 100; FAILS at N = 1 | +| H5 Q2 | PASS | PASS only with row words stored | +| H5 Q3 (crossing; plane inverse refuted on Q3b) | as before | unchanged | +| H6 | HOLDS | not affected (it already used `CompiledProgram`) | + +## Next falsifiers: H6b, H8, H9 (PRE-REGISTERED 2026-10-14, NOT RUN) + +DECISION (operator, 2026-10-14): H6b approved as a probe; the terminal +representation selector is approved as a concept and held until H6b reports. +SCOPE: examples only, no new opcode or executor, no production selector. +BASIS: H6 above. REVISIT WHEN: H6b reports. + +**Research objective.** Whether symbolic normalization lowers the total cost of +composing a solution (construction, normalization, reuse across queries, and +the terminal), not whether individual Boolean kernels get faster. + +Separate from D-ORACLE-0 (#1549, epistemic oracle fidelity). That work asks +whether an answer is faithful to its oracle; this one asks which representation +computes it most cheaply. Cross-referenced, not combined. + +### H6b — symbolic normal forms on the Context × Operation tile + +**Universe.** The 256 × 256 tile read as Context × Operation. This is one +contextual reading of the existing `u8:u8` rail, not a new ISA schema: what a +context and an operation mean stays bound by Class / Schema / Datalog, and the +256 operations are references to (possibly composed) methods, not hardware +opcodes. + +**Representations.** +- **P, canonical product form.** Contexts grouped by identical operation mask: + a list of (context mask, operation mask), each 256 bits. +- **I, normalized Morton interval unions** (as in H6). +- **D, dense mask:** 8 KiB, operated on with existing ndarray SIMD. +- **H, hybrid:** P while it is smaller than D, else D. +- **O, row oracle** on every cell. + +**Correction to the earlier claim.** P is NOT bounded by D's footprint. +256 distinct operation masks, each paired with a context mask, take 256 × 64 B += 16 KiB, twice the dense 8 KiB. Canonicalization and comparison also cost +time. H6b measures this; neither P nor H is assumed to be optimal. + +**Patterns.** Row-oriented (one context, many operations), column-oriented, +rectangles, prefixes, diagonal, checkerboard, random at 1 % and 50 %, sparse, +dense, complementary pairs, disjoint pairs, and high fragmentation (one +distinct mask per context). + +**Measured per representation and pattern.** Construction; normalization; +∩, ∪, ¬; emptiness; equality; bytes held; total cost amortized over N = 1, 10, +100 and 1,000 queries. Tier printed, v4 only, median of 11, ns per operation. + +**Decision.** +- P (or H) is worth a selector if, on the structured patterns (row, column, + rectangle, prefix), its total cost at N = 100 is at most 0.5× D's. +- H is safe as a default only if, on the adversarial patterns (checkerboard, + random 50 %, high fragmentation), it is at most 1.25× D's total cost. +- Differences under 2× are reported as undecided: H1 measured about 40 % + run-to-run noise on this VM. +- Thresholds (group count, interval count, density) where each representation + stops being the cheapest are reported, with every counterexample. + +### H8 — semantic diversity does not add instruction families + +- One fixed formula (a restriction ∧ a forward hop ∧ a negation, then count), + written in three unrelated domains through the existing frontends (Quack, + R2IL): dir-sim `memberOf`, thought coverage, planet → location. +- Compared, as three separate dimensions: + - the canonical logical algebra (the normalized formula, before any physical + choice); + - the physical realization (op families, value families, terminals, carriers); + - semantic leakage (a meaning that reaches the algebra or the physical layer). +- Value access is mathematical, not semantic: reading a lane (`v < t`, a key + through `Gather`) is substrate; only interpretation is optional. +- Accounting: the work avoided through gating (coordinates examined), and the + construction of every resident input mask, charged. +- Passes: no semantic leakage. Instruction families may grow with + representation shape and value algebra, never with meaning, and only where an + existing generic op cannot support the new shape efficiently or correctly. + Answers match the row oracle. +- Fails: a domain needs its own predicate, op, terminal or lowering layer + because of what it means rather than how its values are represented. + +### H9 — relational emptiness without a fold + +Hop and Join stay symbolic terms until an evaluation is required. Some +simplifications need no fold: +- an empty operand; +- known disjoint domains (classes, apertures, value intervals); +- contradictory algebraic constraints. + +General relational emptiness and equality may still need evaluation. + +**Measured.** Over a generated set of composed relational queries: the fraction +decided symbolically, the fraction that needs the fold, and the cost of each +route. + +**Decision.** Soundness is one-sided and absolute: no query may be declared +empty symbolically while the fold finds a row (zero false empties). A +"cannot decide" answer is always allowed; it only costs a fold. + +### Open beyond these + +H11, the end-to-end test: `$var` composed algebraically before `$var.think()`, +with H6b's measured normal-form costs as an input. Not registered yet. diff --git a/crates/lance-graph-mask-risc/Cargo.toml b/crates/lance-graph-mask-risc/Cargo.toml index 36d1da45a..2da125c00 100644 --- a/crates/lance-graph-mask-risc/Cargo.toml +++ b/crates/lance-graph-mask-risc/Cargo.toml @@ -25,3 +25,17 @@ ndarray = { git = "https://github.com/AdaWorldAPI/ndarray", branch = "master", d # still names neither. causal-edge = { path = "../causal-edge" } lance-graph-contract = { path = "../lance-graph-contract" } + +# D-HOLO falsifier probes (plan d-holo-address-indexed-algebra-v1); their +# oracle tests run under `cargo test`. +[[example]] +name = "holo_k_branch_probe" +test = true + +[[example]] +name = "holo_coord_probe" +test = true + +[[example]] +name = "holo_interval_probe" +test = true diff --git a/crates/lance-graph-mask-risc/examples/holo_coord_probe.rs b/crates/lance-graph-mask-risc/examples/holo_coord_probe.rs new file mode 100644 index 000000000..6e2a9cf3e --- /dev/null +++ b/crates/lance-graph-mask-risc/examples/holo_coord_probe.rs @@ -0,0 +1,869 @@ +//! D-HOLO H5: coordinate-native vs coordinate-crossing queries on one tile. +//! +//! Pre-registered in `.claude/plans/d-holo-address-indexed-algebra-v1.md` (H5). +//! +//! - Q1 `identity[$prefix].count()`: `Range` under the occupancy plane (no key +//! read) vs a `MatchU64` sweep of the key lane, plus the closed form; an +//! adversarial run on a permuted tile, where `Range` must disagree. +//! - Q2 `identity[$node].thoughts($all).count()`: one popcount of the row's +//! written-by word vs 64 mask-risc programs vs 64 bit reads. +//! - Q3 reverse `memberOf`: a mask-risc scan of the four membership lanes vs a +//! CSR inverse vs per-group inverse mask planes, with build and update cost. +//! +//! `cargo run --release -p lance-graph-mask-risc --example holo_coord_probe` +//! `cargo test -p lance-graph-mask-risc --example holo_coord_probe` + +use std::hint::black_box; +use std::time::Instant; + +use lance_graph_mask_risc::exec::{execute_compiled, Scratch}; +use lance_graph_mask_risc::{ + touched_words, words_for, CompiledProgram, Foreign, LaneRef, MaskOp, Operand, Out, Planes, + Pred, Program, Terminal, Value, +}; + +const N: usize = 65_536; +const W: usize = N / 64; +const CLASSID: u64 = 0x0C0C_0000; +const THOUGHTS: usize = 64; +const GROUPS: u32 = 1024; +const SLOTS: usize = 4; + +fn tier() -> &'static str { + if cfg!(target_feature = "avx512f") { + "v4 (avx512f)" + } else if cfg!(target_feature = "avx2") { + "v3 (avx2): timings not reportable" + } else { + "below v3: timings not reportable" + } +} + +fn mix(mut x: u64) -> u64 { + x = x.wrapping_add(0x9E37_79B9_7F4A_7C15); + x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + x ^ (x >> 31) +} + +fn count(v: Value) -> u64 { + match v { + Value::Count(n) => n as u64, + other => panic!("expected a count, got {other:?}"), + } +} + +/// Lowering recognition, once, outside any timed path. +fn cp(p: Program) -> CompiledProgram { + CompiledProgram::new(p) +} + +fn run( + p: &CompiledProgram, + planes: &Planes<'_>, + s: &mut Scratch<'_>, + ext: std::ops::Range, +) -> u64 { + count(execute_compiled(&p.compiled(), planes, &Foreign::NONE, s, Out::None, ext).expect("run")) +} + +/// Median of 11 runs, ns per call. +fn time(mut f: F) -> f64 { + f(); + let mut v: Vec = (0..11) + .map(|_| { + let t = Instant::now(); + let mut n = 0u32; + while n < 3 || t.elapsed().as_micros() < 2_000 { + f(); + n += 1; + } + t.elapsed().as_nanos() as f64 / f64::from(n) + }) + .collect(); + v.sort_by(|a, b| a.total_cmp(b)); + v[5] +} + +// ---------- Q1 ---------- + +/// The key of row `r` on a dense tile: row = HEEL code. +fn key(r: usize) -> u64 { + (CLASSID << 32) | ((r as u64) << 16) +} + +fn occupancy(density_pct: u64, seed: u64) -> Vec { + let mut o = vec![0u64; W]; + for r in 0..N { + if mix(seed ^ r as u64) % 100 < density_pct { + o[r / 64] |= 1 << (r % 64); + } + } + o +} + +/// The occupancy plane packed from a per-row presence lane: the maintenance +/// step that produces the resident mask both Q1 arms read (charged). +fn pack_occupancy(present: &[u8]) -> Vec { + let mut o = vec![0u64; W]; + for (r, &b) in present.iter().enumerate() { + o[r / 64] |= u64::from(b & 1) << (r % 64); + } + o +} + +/// Prefix of `p` HEEL nibbles with value `pv`: its row interval. +fn prefix_rows(p: u32, pv: u64) -> (usize, usize) { + let span = 1usize << (4 * (4 - p)); + let lo = (pv as usize) * span; + (lo, lo + span) +} + +fn prefix_match(p: u32, pv: u64) -> Pred { + let heel_care: u64 = (0xFFFFu64 << (4 * (4 - p))) & 0xFFFF; + let pattern = (CLASSID << 32) | ((pv << (4 * (4 - p))) << 16); + Pred::MatchU64 { + lane: 0, + pattern, + care: (0xFFFF_FFFFu64 << 32) | (heel_care << 16), + } +} + +fn q1_range(lo: usize, hi: usize) -> Program { + Program::new( + vec![MaskOp::Pred { + pred: Pred::Range { + lo: lo as u32, + hi: hi as u32, + }, + under: Some(Operand::Plane(0)), + dst: 0, + }], + Terminal::Count { + mask: Operand::Scratch(0), + }, + ) +} + +fn q1_sweep(p: u32, pv: u64) -> Program { + Program::new( + vec![MaskOp::Pred { + pred: prefix_match(p, pv), + under: Some(Operand::Plane(0)), + dst: 0, + }], + Terminal::Count { + mask: Operand::Scratch(0), + }, + ) +} + +fn q1_oracle(keys: &[u64], o: &[u64], p: u32, pv: u64) -> u64 { + let Pred::MatchU64 { pattern, care, .. } = prefix_match(p, pv) else { + unreachable!() + }; + (0..N) + .filter(|&r| (o[r / 64] >> (r % 64)) & 1 == 1 && (keys[r] ^ pattern) & care == 0) + .count() as u64 +} + +// ---------- Q2 ---------- + +fn coverage(seed: u64) -> (Vec, Vec>) { + let mut w = vec![0u64; N]; + let mut m = vec![vec![0u64; W]; THOUGHTS]; + for (r, word) in w.iter_mut().enumerate() { + for (t, plane) in m.iter_mut().enumerate() { + if mix(seed ^ ((r as u64) << 8) ^ t as u64).is_multiple_of(10) { + *word |= 1 << t; + plane[r / 64] |= 1 << (r % 64); + } + } + } + (w, m) +} + +/// Per-thought planes from row words (charged when row words are stored). +fn planes_from_words(w: &[u64]) -> Vec> { + let mut m = vec![vec![0u64; W]; THOUGHTS]; + for (r, &word) in w.iter().enumerate() { + let mut b = word; + while b != 0 { + let t = b.trailing_zeros() as usize; + b &= b - 1; + m[t][r / 64] |= 1 << (r % 64); + } + } + m +} + +/// Row words from per-thought planes (charged when planes are stored). +fn words_from_planes(m: &[Vec]) -> Vec { + let mut w = vec![0u64; N]; + for (t, plane) in m.iter().enumerate() { + for (wi, &word) in plane.iter().enumerate() { + let mut b = word; + while b != 0 { + let r = wi * 64 + b.trailing_zeros() as usize; + b &= b - 1; + w[r] |= 1 << t; + } + } + } + w +} + +fn q2_prog(t: u16, row: usize) -> Program { + Program::new( + vec![MaskOp::Pred { + pred: Pred::Range { + lo: row as u32, + hi: row as u32 + 1, + }, + under: Some(Operand::Plane(t)), + dst: 0, + }], + Terminal::Count { + mask: Operand::Scratch(0), + }, + ) +} + +// ---------- Q3 ---------- + +struct Members { + /// `lanes[j][r]`: the j-th group of identity r, 0 = none. + lanes: Vec>, + v: Vec, +} + +fn members(skewed: bool, seed: u64) -> Members { + // Zipf(1) over 1..=GROUPS by inverse CDF. + let h: Vec = (1..=GROUPS) + .scan(0.0, |acc, k| { + *acc += 1.0 / f64::from(k); + Some(*acc) + }) + .collect(); + let total = *h.last().unwrap(); + let mut lanes = vec![vec![0u32; N]; SLOTS]; + for r in 0..N { + let mut have = [0u32; SLOTS]; + for (j, lane) in lanes.iter_mut().enumerate() { + let x = mix(seed ^ ((r as u64) << 4) ^ j as u64); + if x.is_multiple_of(4) { + continue; // a quarter of the slots are empty + } + let u = (x >> 8) as f64 / (1u64 << 56) as f64; + let g = if skewed { + h.partition_point(|&c| c < u * total) as u32 + 1 + } else { + (x >> 8) as u32 % GROUPS + 1 + }; + if !have.contains(&g) { + have[j] = g; + lane[r] = g; + } + } + } + let v = (0..N) + .map(|r| (mix(seed ^ 0xABCD ^ r as u64) % 1000) as i32) + .collect(); + Members { lanes, v } +} + +fn q3_scan(g: u32, lo: usize, hi: usize, t: Option) -> Program { + let mut ops = vec![MaskOp::Pred { + pred: Pred::Range { + lo: lo as u32, + hi: hi as u32, + }, + under: None, + dst: 0, + }]; + if let Some(t) = t { + ops.push(MaskOp::Pred { + pred: Pred::LtI32 { + lane: SLOTS as u16, + t, + }, + under: Some(Operand::Scratch(0)), + dst: 1, + }); + ops.push(MaskOp::Or { + a: Operand::Scratch(1), + b: Operand::Scratch(1), + dst: 0, + }); + } + for j in 0..SLOTS as u16 { + ops.push(MaskOp::Pred { + pred: Pred::EqU32 { lane: j, v: g }, + under: Some(Operand::Scratch(0)), + dst: 2 + j, + }); + } + ops.push(MaskOp::Or { + a: Operand::Scratch(2), + b: Operand::Scratch(3), + dst: 6, + }); + ops.push(MaskOp::Or { + a: Operand::Scratch(4), + b: Operand::Scratch(5), + dst: 7, + }); + ops.push(MaskOp::Or { + a: Operand::Scratch(6), + b: Operand::Scratch(7), + dst: 6, + }); + Program::new( + ops, + Terminal::Count { + mask: Operand::Scratch(6), + }, + ) +} + +struct Csr { + off: Vec, + rows: Vec, +} + +fn build_csr(m: &Members) -> Csr { + let mut deg = vec![0u32; GROUPS as usize + 2]; + for lane in &m.lanes { + for &g in lane { + if g != 0 { + deg[g as usize + 1] += 1; + } + } + } + for i in 1..deg.len() { + deg[i] += deg[i - 1]; + } + let mut fill = deg.clone(); + let mut rows = vec![0u32; *deg.last().unwrap() as usize]; + for r in 0..N { + for lane in &m.lanes { + let g = lane[r]; + if g != 0 { + rows[fill[g as usize] as usize] = r as u32; + fill[g as usize] += 1; + } + } + } + Csr { off: deg, rows } +} + +impl Csr { + fn members(&self, g: u32) -> &[u32] { + &self.rows[self.off[g as usize] as usize..self.off[g as usize + 1] as usize] + } + fn count(&self, g: u32, lo: usize, hi: usize, v: Option<(&[i32], i32)>) -> u64 { + let m = self.members(g); + let a = m.partition_point(|&r| (r as usize) < lo); + let b = m.partition_point(|&r| (r as usize) < hi); + match v { + None => (b - a) as u64, + Some((v, t)) => m[a..b].iter().filter(|&&r| v[r as usize] < t).count() as u64, + } + } +} + +fn build_group_planes(m: &Members) -> Vec> { + let mut p = vec![vec![0u64; W]; GROUPS as usize + 1]; + for lane in &m.lanes { + for (r, &g) in lane.iter().enumerate() { + if g != 0 { + p[g as usize][r / 64] |= 1 << (r % 64); + } + } + } + p +} + +fn q3_plane(lo: usize, hi: usize, t: Option) -> Program { + let mut ops = vec![MaskOp::Pred { + pred: Pred::Range { + lo: lo as u32, + hi: hi as u32, + }, + under: Some(Operand::Plane(0)), + dst: 0, + }]; + if let Some(t) = t { + ops.push(MaskOp::Pred { + pred: Pred::LtI32 { lane: 0, t }, + under: Some(Operand::Scratch(0)), + dst: 1, + }); + return Program::new( + ops, + Terminal::Count { + mask: Operand::Scratch(1), + }, + ); + } + Program::new( + ops, + Terminal::Count { + mask: Operand::Scratch(0), + }, + ) +} + +fn q3_oracle(m: &Members, g: u32, lo: usize, hi: usize, t: Option) -> u64 { + (lo..hi) + .filter(|&r| m.lanes.iter().any(|l| l[r] == g) && t.is_none_or(|t| m.v[r] < t)) + .count() as u64 +} + +struct Q3 { + m: Members, + csr: Csr, + gp: Vec>, +} + +impl Q3 { + fn scan(&self, prog: &CompiledProgram, lo: usize, hi: usize, s: &mut Scratch<'_>) -> u64 { + let lanes: [LaneRef<'_>; SLOTS + 1] = std::array::from_fn(|j| { + if j < SLOTS { + LaneRef::U32(&self.m.lanes[j]) + } else { + LaneRef::I32(&self.m.v) + } + }); + let planes = Planes { + n_rows: N, + masks: &[], + lanes: &lanes, + }; + run(prog, &planes, s, lo..hi) + } + fn inverse_planes( + &self, + g: u32, + prog: &CompiledProgram, + lo: usize, + hi: usize, + s: &mut Scratch<'_>, + ) -> u64 { + let masks = [self.gp[g as usize].as_slice()]; + let lanes = [LaneRef::I32(&self.m.v)]; + let planes = Planes { + n_rows: N, + masks: &masks, + lanes: &lanes, + }; + run(prog, &planes, s, lo..hi) + } +} + +fn main() { + println!("tier: {}", tier()); + + // ---- Q1 ---- + let keys: Vec = (0..N).map(key).collect(); + let mut perm: Vec = keys.clone(); + for i in (1..N).rev() { + perm.swap(i, mix(0xFEED ^ i as u64) as usize % (i + 1)); + } + println!("# Q1 identity[$prefix].count()"); + println!("occ\tp\trows\tR_ns\tS_ns\tR_words_read\tS_rows_read\tanswer\tR_total_N1\tS_total_N1\tR_total_N100\tS_total_N100"); + for &occ in &[100u64, 30] { + let o = occupancy(occ, 7); + let present: Vec = (0..N) + .map(|r| ((o[r / 64] >> (r % 64)) & 1) as u8) + .collect(); + let t0 = Instant::now(); + let o2 = black_box(pack_occupancy(&present)); + let o_ns = t0.elapsed().as_nanos() as f64; + assert_eq!(o2, o, "packed occupancy"); + println!( + "# occupancy {occ}%: plane built from a presence lane in {o_ns:.0} ns ({:.2} ns/row, {} KiB), charged to both arms", + o_ns / N as f64, + W * 8 / 1024 + ); + for &p in &[1u32, 2, 3] { + let pv = (1u64 << (4 * p)) / 2 + 3; + let (lo, hi) = prefix_rows(p, pv); + let masks = [o.as_slice()]; + let lanes = [LaneRef::U64(&keys)]; + let planes = Planes { + n_rows: N, + masks: &masks, + lanes: &lanes, + }; + let mut s = Scratch::for_program(&q1_sweep(p, pv), N).expect("scratch"); + let (pr, ps) = (cp(q1_range(lo, hi)), cp(q1_sweep(p, pv))); + let want = q1_oracle(&keys, &o, p, pv); + let r = run(&pr, &planes, &mut s, 0..N); + let sw = run(&ps, &planes, &mut s, 0..N); + assert_eq!((r, sw), (want, want), "Q1 occ {occ} p {p}"); + if occ == 100 { + assert_eq!(want, 1u64 << (4 * (4 - p)), "closed form"); + } + let rn = time(|| { + black_box(run(&pr, &planes, &mut s, 0..N)); + }); + let sn = time(|| { + black_box(run(&ps, &planes, &mut s, 0..N)); + }); + println!( + "{occ}%\t{p}\t{}\t{rn:.1}\t{sn:.1}\t{}\t{N}\t{want}\t{:.0}\t{:.0}\t{:.0}\t{:.0}", + hi - lo, + touched_words(lo as u32, hi as u32).len(), + o_ns + rn, + o_ns + sn, + o_ns / 100.0 + rn, + o_ns / 100.0 + sn + ); + } + } + { + // Adversarial: the key lane is permuted, so rows are no longer in key + // order. `Range` reads no key, so it must now be wrong. + let o = occupancy(30, 7); + let (p, pv) = (1u32, 11u64); + let (lo, hi) = prefix_rows(p, pv); + let masks = [o.as_slice()]; + let lanes = [LaneRef::U64(&perm)]; + let planes = Planes { + n_rows: N, + masks: &masks, + lanes: &lanes, + }; + let mut s = Scratch::for_program(&q1_sweep(p, pv), N).expect("scratch"); + let r = run(&cp(q1_range(lo, hi)), &planes, &mut s, 0..N); + let sw = run(&cp(q1_sweep(p, pv)), &planes, &mut s, 0..N); + let want = q1_oracle(&perm, &o, p, pv); + assert_eq!(sw, want); + assert_ne!( + r, want, + "the permuted tile must expose a Range without an order witness" + ); + println!( + "# adversarial permuted tile: Range {r}, sweep {sw}, oracle {want}: Range disagrees" + ); + } + + // ---- Q2 ---- + let (w, m) = coverage(11); + let t0 = Instant::now(); + let m2 = black_box(planes_from_words(&w)); + let m_ns = t0.elapsed().as_nanos() as f64; + let t0 = Instant::now(); + let w2 = black_box(words_from_planes(&m)); + let w_ns = t0.elapsed().as_nanos() as f64; + assert_eq!((&m2, &w2), (&m, &w), "rebuilt coverage"); + let probes: Vec = (0..1024).map(|i| mix(i as u64) as usize % N).collect(); + let thought_masks: [&[u64]; THOUGHTS] = std::array::from_fn(|t| m[t].as_slice()); + let planes = Planes { + n_rows: N, + masks: &thought_masks, + lanes: &[], + }; + let mut s = Scratch::for_program(&q2_prog(0, 0), N).expect("scratch"); + let progs: Vec> = probes[..16] + .iter() + .map(|&row| (0..THOUGHTS as u16).map(|t| cp(q2_prog(t, row))).collect()) + .collect(); + for (i, &row) in probes[..16].iter().enumerate() { + let want = u64::from(w[row].count_ones()); + let viap: u64 = progs[i] + .iter() + .map(|p| run(p, &planes, &mut s, row..row + 1)) + .sum(); + let bits: u64 = (0..THOUGHTS) + .map(|t| (m[t][row / 64] >> (row % 64)) & 1) + .sum(); + assert_eq!((viap, bits), (want, want)); + } + let wn = time(|| { + black_box( + probes + .iter() + .map(|&r| w[black_box(r)].count_ones()) + .sum::(), + ); + }) / probes.len() as f64; + let bn = time(|| { + black_box( + probes + .iter() + .map(|&r| { + (0..THOUGHTS) + .map(|t| (m[t][r / 64] >> (r % 64)) & 1) + .sum::() + }) + .sum::(), + ); + }) / probes.len() as f64; + let pn = time(|| { + for (i, &row) in probes[..16].iter().enumerate() { + black_box( + progs[i] + .iter() + .map(|p| run(p, &planes, &mut s, row..row + 1)) + .sum::(), + ); + } + }) / 16.0; + println!("# Q2 identity[$node].thoughts($all).count(), ns per identity"); + println!("W (row word popcount) {wn:.2}\tB (64 plane bit reads) {bn:.2}\tP (64 mask-risc programs) {pn:.0}"); + println!( + "# construction (charged): planes from row words {:.0} ns ({:.2} ns/row), row words from planes {:.0} ns ({:.2} ns/row)", + m_ns, + m_ns / N as f64, + w_ns, + w_ns / N as f64 + ); + for (stored, wc, mc) in [("row words", 0.0, m_ns), ("planes", w_ns, 0.0)] { + for q in [1.0f64, 100.0] { + println!( + "# total per identity, {stored} stored, N = {q:.0}: W {:.1} B {:.1} P {:.0}", + wc / q + wn, + mc / q + bn, + mc / q + pn + ); + } + } + + // ---- Q3 ---- + println!("# Q3 reverse memberOf"); + println!("dist\tquery\tS_ns\tI_ns\tIM_ns\tS_rows_read"); + for &skewed in &[false, true] { + let mem = members(skewed, 21); + let edges: usize = mem + .lanes + .iter() + .map(|l| l.iter().filter(|&&g| g != 0).count()) + .sum(); + let t0 = Instant::now(); + let csr = black_box(build_csr(&mem)); + let csr_ns = t0.elapsed().as_nanos() as f64; + let t0 = Instant::now(); + let gp = black_box(build_group_planes(&mem)); + let gp_ns = t0.elapsed().as_nanos() as f64; + let csr_bytes = csr.off.len() * 4 + csr.rows.len() * 4; + let gp_bytes = gp.len() * W * 8; + // One-edge insert: CSR shifts every later row and offset; the mask + // plane sets one bit. + let mut c2 = Csr { + off: csr.off.clone(), + rows: csr.rows.clone(), + }; + let t0 = Instant::now(); + for i in 0..100u32 { + let g = i % GROUPS + 1; + let at = c2.off[g as usize + 1] as usize; + c2.rows.insert(at, 7); + for o in &mut c2.off[g as usize + 1..] { + *o += 1; + } + } + let csr_ins = t0.elapsed().as_nanos() as f64 / 100.0; + let mut gp2 = vec![0u64; W]; + let t0 = Instant::now(); + for i in 0..100usize { + gp2[black_box(i * 37 % W)] |= 1 << (i % 64); + } + let gp_ins = t0.elapsed().as_nanos() as f64 / 100.0; + black_box(&gp2); + let dist = if skewed { "zipf" } else { "uniform" }; + println!( + "# {dist}: {edges} edges; CSR build {:.2} ns/edge, {} KiB, insert {csr_ins:.0} ns; group planes build {:.2} ns/edge, {} KiB, insert {gp_ins:.1} ns", + csr_ns / edges as f64, + csr_bytes / 1024, + gp_ns / edges as f64, + gp_bytes / 1024 + ); + let q = Q3 { m: mem, csr, gp }; + let groups: Vec = (0..256) + .map(|i| (mix(0x77 ^ i) % u64::from(GROUPS)) as u32 + 1) + .collect(); + let mut s = Scratch::for_program(&q3_scan(1, 0, N, Some(1)), N).expect("scratch"); + let (plo, phi) = (5 * 4096, 6 * 4096); + let shapes: [(&str, usize, usize, Option); 3] = [ + ("Q3a reverse(g).count", 0, N, None), + ("Q3b reverse(g)&prefix", plo, phi, None), + ("Q3c reverse(g)&(v = + groups.iter().map(|&g| cp(q3_scan(g, lo, hi, t))).collect(); + let plane_prog = cp(q3_plane(lo, hi, t)); + for (i, &g) in groups[..32].iter().enumerate() { + let want = q3_oracle(&q.m, g, lo, hi, t); + assert_eq!(q.scan(&scans[i], lo, hi, &mut s), want, "{name} scan g={g}"); + assert_eq!( + q.csr.count(g, lo, hi, t.map(|t| (q.m.v.as_slice(), t))), + want, + "{name} csr" + ); + assert_eq!( + q.inverse_planes(g, &plane_prog, lo, hi, &mut s), + want, + "{name} planes" + ); + } + let sn = time(|| { + for sp in &scans { + black_box(q.scan(sp, lo, hi, &mut s)); + } + }) / groups.len() as f64; + let iv = t.map(|t| (q.m.v.as_slice(), t)); + let inn = time(|| { + for &g in &groups { + black_box(q.csr.count(g, lo, hi, iv)); + } + }) / groups.len() as f64; + let imn = time(|| { + for &g in &groups { + black_box(q.inverse_planes(g, &plane_prog, lo, hi, &mut s)); + } + }) / groups.len() as f64; + let be_i = if sn > inn { + csr_ns / (sn - inn) + } else { + f64::INFINITY + }; + let be_im = if sn > imn { + gp_ns / (sn - imn) + } else { + f64::INFINITY + }; + println!( + "{dist}\t{name}\t{sn:.0}\t{inn:.1}\t{imn:.0}\t{}\tbreak-even N: CSR {be_i:.0}, planes {be_im:.0}", + hi - lo + ); + } + } + let _ = words_for(N); +} + +#[cfg(test)] +mod tests { + use super::*; + + /// FAILS IF: Range or the sweep disagree with the oracle on any prefix + /// depth or occupancy. + #[test] + fn q1_range_and_sweep_match_the_oracle() { + let keys: Vec = (0..N).map(key).collect(); + for &occ in &[100u64, 30] { + let o = occupancy(occ, 3); + for &p in &[1u32, 2, 3] { + for pv in [0u64, (1 << (4 * p)) - 1, 5] { + let (lo, hi) = prefix_rows(p, pv); + let masks = [o.as_slice()]; + let lanes = [LaneRef::U64(&keys)]; + let planes = Planes { + n_rows: N, + masks: &masks, + lanes: &lanes, + }; + let mut s = Scratch::for_program(&q1_sweep(p, pv), N).expect("scratch"); + let want = q1_oracle(&keys, &o, p, pv); + assert!(want > 0); + assert_eq!(run(&cp(q1_range(lo, hi)), &planes, &mut s, 0..N), want); + assert_eq!(run(&cp(q1_sweep(p, pv)), &planes, &mut s, 0..N), want); + } + } + } + } + + /// FAILS IF: a permuted key lane does not expose a Range used without an + /// order witness (the fixture could not detect the violation). + #[test] + fn a_permuted_tile_exposes_range_without_a_witness() { + let mut perm: Vec = (0..N).map(key).collect(); + for i in (1..N).rev() { + perm.swap(i, mix(0xFEED ^ i as u64) as usize % (i + 1)); + } + let o = occupancy(30, 3); + let (lo, hi) = prefix_rows(1, 11); + let masks = [o.as_slice()]; + let lanes = [LaneRef::U64(&perm)]; + let planes = Planes { + n_rows: N, + masks: &masks, + lanes: &lanes, + }; + let mut s = Scratch::for_program(&q1_sweep(1, 11), N).expect("scratch"); + let want = q1_oracle(&perm, &o, 1, 11); + assert_eq!(run(&cp(q1_sweep(1, 11)), &planes, &mut s, 0..N), want); + assert_ne!(run(&cp(q1_range(lo, hi)), &planes, &mut s, 0..N), want); + } + + /// FAILS IF: any Q3 arm differs from the oracle, for any shape, either + /// distribution, on groups of every size. + #[test] + fn q3_arms_match_the_oracle() { + for &skewed in &[false, true] { + let m = members(skewed, 5); + let csr = build_csr(&m); + let gp = build_group_planes(&m); + let q = Q3 { m, csr, gp }; + let mut s = Scratch::for_program(&q3_scan(1, 0, N, Some(1)), N).expect("scratch"); + for g in [1u32, 2, 17, 500, GROUPS] { + for (lo, hi, t) in [ + (0, N, None), + (4096, 8192, None), + (0, N, Some(100)), + (100, 3000, Some(500)), + ] { + let want = q3_oracle(&q.m, g, lo, hi, t); + assert_eq!( + q.scan(&cp(q3_scan(g, lo, hi, t)), lo, hi, &mut s), + want, + "scan g={g}" + ); + assert_eq!( + q.csr.count(g, lo, hi, t.map(|t| (q.m.v.as_slice(), t))), + want, + "csr" + ); + assert_eq!( + q.inverse_planes(g, &cp(q3_plane(lo, hi, t)), lo, hi, &mut s), + want, + "planes" + ); + } + } + // Anti-vacuity: groups of different sizes, and skew visible. + let big = q3_oracle(&q.m, 1, 0, N, None); + let small = q3_oracle(&q.m, GROUPS, 0, N, None); + if skewed { + assert!( + big > 20 * small.max(1), + "zipf must be skewed: {big} vs {small}" + ); + } else { + assert!(big > 0 && small > 0); + } + } + } + + /// FAILS IF: Q2's three forms disagree with the row word's popcount. + #[test] + fn q2_forms_agree() { + let (w, m) = coverage(5); + let masks: [&[u64]; THOUGHTS] = std::array::from_fn(|t| m[t].as_slice()); + let planes = Planes { + n_rows: N, + masks: &masks, + lanes: &[], + }; + let mut s = Scratch::for_program(&q2_prog(0, 0), N).expect("scratch"); + for row in [0usize, 63, 64, 4097, N - 1] { + let want = u64::from(w[row].count_ones()); + let p: u64 = (0..THOUGHTS as u16) + .map(|t| run(&cp(q2_prog(t, row)), &planes, &mut s, row..row + 1)) + .sum(); + assert_eq!(p, want); + } + } +} diff --git a/crates/lance-graph-mask-risc/examples/holo_interval_probe.rs b/crates/lance-graph-mask-risc/examples/holo_interval_probe.rs new file mode 100644 index 000000000..f90df0ef1 --- /dev/null +++ b/crates/lance-graph-mask-risc/examples/holo_interval_probe.rs @@ -0,0 +1,621 @@ +//! D-HOLO H6: closure of address restrictions as interval sets. +//! +//! Pre-registered in `.claude/plans/d-holo-address-indexed-algebra-v1.md` (H6). +//! +//! On a dense tile the row is the Morton code, so a prefix is one row +//! interval and a box [x0, x1) × [y0, y1) is a union of aligned quadtree +//! intervals. Formulas over boxes and prefixes are evaluated three ways: +//! +//! - S: symbolically, on interval endpoints, never touching a row. +//! - M: in mask-risc, each leaf as `Range` ops ORed into a slot, then the +//! formula's `And` / `Or` / `Not`, then `Count`. +//! - V: in mask-risc, x and y as value lanes with `GeI32` / `LtI32` per box +//! bound: no intervals at all. +//! +//! `cargo run --release -p lance-graph-mask-risc --example holo_interval_probe` +//! `cargo test -p lance-graph-mask-risc --example holo_interval_probe` + +use std::hint::black_box; +use std::time::Instant; + +use lance_graph_mask_risc::exec::{execute_compiled, Scratch}; +use lance_graph_mask_risc::{ + CompiledProgram, Foreign, LaneRef, MaskOp, Operand, Out, Planes, Pred, Program, Terminal, Value, +}; + +const N: u32 = 65_536; + +fn tier() -> &'static str { + if cfg!(target_feature = "avx512f") { + "v4 (avx512f)" + } else if cfg!(target_feature = "avx2") { + "v3 (avx2): timings not reportable" + } else { + "below v3: timings not reportable" + } +} + +fn gather(v: u32) -> u32 { + let mut x = v & 0x5555; + x = (x | (x >> 1)) & 0x3333; + x = (x | (x >> 2)) & 0x0F0F; + (x | (x >> 4)) & 0x00FF +} + +/// A sorted set of disjoint, non-adjacent half-open intervals. +type Set = Vec<(u32, u32)>; + +fn push(out: &mut Set, lo: u32, hi: u32) { + if lo >= hi { + return; + } + if let Some(last) = out.last_mut() { + if last.1 >= lo { + last.1 = last.1.max(hi); + return; + } + } + out.push((lo, hi)); +} + +#[derive(Clone, Copy, Debug)] +struct Box2 { + x0: u32, + x1: u32, + y0: u32, + y1: u32, +} + +/// The box as maximal aligned quadtree intervals of Morton codes. +fn box_set(b: Box2) -> Set { + fn rec(b: Box2, x: u32, y: u32, side: u32, base: u32, out: &mut Set) { + let (bx1, by1) = (x + side, y + side); + if bx1 <= b.x0 || x >= b.x1 || by1 <= b.y0 || y >= b.y1 { + return; + } + if x >= b.x0 && bx1 <= b.x1 && y >= b.y0 && by1 <= b.y1 { + push(out, base, base + side * side); + return; + } + let h = side / 2; + let q = h * h; + // Child order by Morton digit: x low bit first, then y. + rec(b, x, y, h, base, out); + rec(b, x + h, y, h, base + q, out); + rec(b, x, y + h, h, base + 2 * q, out); + rec(b, x + h, y + h, h, base + 3 * q, out); + } + let mut out = Vec::new(); + rec(b, 0, 0, 256, 0, &mut out); + out +} + +fn prefix_set(p: u32, pv: u32) -> Set { + let span = 1u32 << (4 * (4 - p)); + vec![(pv * span, (pv + 1) * span)] +} + +fn union(a: &Set, b: &Set) -> Set { + let mut out = Vec::with_capacity(a.len() + b.len()); + let (mut i, mut j) = (0, 0); + while i < a.len() || j < b.len() { + let take_a = j >= b.len() || (i < a.len() && a[i].0 <= b[j].0); + let (lo, hi) = if take_a { a[i] } else { b[j] }; + if take_a { + i += 1; + } else { + j += 1; + } + push(&mut out, lo, hi); + } + out +} + +fn intersect(a: &Set, b: &Set) -> Set { + let mut out = Vec::new(); + let (mut i, mut j) = (0, 0); + while i < a.len() && j < b.len() { + let lo = a[i].0.max(b[j].0); + let hi = a[i].1.min(b[j].1); + push(&mut out, lo, hi); + if a[i].1 < b[j].1 { + i += 1; + } else { + j += 1; + } + } + out +} + +fn complement(a: &Set) -> Set { + let mut out = Vec::with_capacity(a.len() + 1); + let mut at = 0; + for &(lo, hi) in a { + push(&mut out, at, lo); + at = hi; + } + push(&mut out, at, N); + out +} + +fn measure(a: &Set) -> u64 { + a.iter().map(|&(lo, hi)| u64::from(hi - lo)).sum() +} + +fn well_formed(a: &Set) -> bool { + a.iter().all(|&(lo, hi)| lo < hi && hi <= N) && a.windows(2).all(|w| w[0].1 < w[1].0) +} + +#[derive(Clone, Copy)] +enum F { + /// B1 ∩ P + F1, + /// B1 ∪ B2 + F2, + /// ¬B1 ∩ P + F3, + /// (B1 ∪ B2) ∩ ¬B3 + F4, +} + +struct Case { + b: [Box2; 3], + p: (u32, u32), +} + +/// The leaves of a case as interval sets: three boxes, then the prefix. +fn leaves(c: &Case) -> [Set; 4] { + [ + box_set(c.b[0]), + box_set(c.b[1]), + box_set(c.b[2]), + prefix_set(c.p.0, c.p.1), + ] +} + +/// Evaluate a formula over already-decomposed leaves. +fn eval(f: F, l: &[Set; 4]) -> Set { + match f { + F::F1 => intersect(&l[0], &l[3]), + F::F2 => union(&l[0], &l[1]), + F::F3 => intersect(&complement(&l[0]), &l[3]), + F::F4 => intersect(&union(&l[0], &l[1]), &complement(&l[2])), + } +} + +fn symbolic(f: F, c: &Case) -> Set { + eval(f, &leaves(c)) +} + +fn in_box(b: Box2, r: u32) -> bool { + let (x, y) = (gather(r), gather(r >> 1)); + x >= b.x0 && x < b.x1 && y >= b.y0 && y < b.y1 +} + +fn oracle(f: F, c: &Case) -> u64 { + let pset = prefix_set(c.p.0, c.p.1)[0]; + let in_p = |r: u32| r >= pset.0 && r < pset.1; + (0..N) + .filter(|&r| match f { + F::F1 => in_box(c.b[0], r) && in_p(r), + F::F2 => in_box(c.b[0], r) || in_box(c.b[1], r), + F::F3 => !in_box(c.b[0], r) && in_p(r), + F::F4 => (in_box(c.b[0], r) || in_box(c.b[1], r)) && !in_box(c.b[2], r), + }) + .count() as u64 +} + +// ---------- M: Range ops in mask-risc ---------- + +/// Emit `set` into slot `dst`, using `tmp` for each further interval. +fn emit_set(ops: &mut Vec, set: &Set, dst: u16, tmp: u16) { + for (i, &(lo, hi)) in set.iter().enumerate() { + let to = if i == 0 { dst } else { tmp }; + ops.push(MaskOp::Pred { + pred: Pred::Range { lo, hi }, + under: None, + dst: to, + }); + if i > 0 { + ops.push(MaskOp::Or { + a: Operand::Scratch(dst), + b: Operand::Scratch(tmp), + dst, + }); + } + } + if set.is_empty() { + ops.push(MaskOp::Pred { + pred: Pred::Range { lo: 0, hi: 0 }, + under: None, + dst, + }); + } +} + +fn mask_program(f: F, c: &Case) -> Program { + let mut ops = Vec::new(); + let s = |i: usize| box_set(c.b[i]); + let p = prefix_set(c.p.0, c.p.1); + let sc = Operand::Scratch; + match f { + F::F1 => { + emit_set(&mut ops, &s(0), 0, 4); + emit_set(&mut ops, &p, 1, 4); + ops.push(MaskOp::And { + a: sc(0), + b: sc(1), + dst: 0, + }); + } + F::F2 => { + emit_set(&mut ops, &s(0), 0, 4); + emit_set(&mut ops, &s(1), 1, 4); + ops.push(MaskOp::Or { + a: sc(0), + b: sc(1), + dst: 0, + }); + } + F::F3 => { + emit_set(&mut ops, &s(0), 0, 4); + emit_set(&mut ops, &p, 1, 4); + ops.push(MaskOp::AndNot { + a: sc(1), + b: sc(0), + dst: 0, + }); + } + F::F4 => { + emit_set(&mut ops, &s(0), 0, 4); + emit_set(&mut ops, &s(1), 1, 4); + emit_set(&mut ops, &s(2), 2, 4); + ops.push(MaskOp::Or { + a: sc(0), + b: sc(1), + dst: 0, + }); + ops.push(MaskOp::AndNot { + a: sc(0), + b: sc(2), + dst: 0, + }); + } + } + Program::new(ops, Terminal::Count { mask: sc(0) }) +} + +// ---------- V: value sweep over x / y lanes ---------- + +fn emit_box(ops: &mut Vec, b: Box2, dst: u16) { + let preds = [ + Pred::GeI32 { + lane: 0, + t: b.x0 as i32, + }, + Pred::LtI32 { + lane: 0, + t: b.x1 as i32, + }, + Pred::GeI32 { + lane: 1, + t: b.y0 as i32, + }, + Pred::LtI32 { + lane: 1, + t: b.y1 as i32, + }, + ]; + for (i, pred) in preds.into_iter().enumerate() { + ops.push(MaskOp::Pred { + pred, + under: if i == 0 { + None + } else { + Some(Operand::Scratch(5 + (i as u16 - 1) % 2)) + }, + dst: if i == 3 { dst } else { 5 + (i as u16 % 2) }, + }); + } +} + +fn value_program(f: F, c: &Case) -> Program { + let mut ops = Vec::new(); + let sc = Operand::Scratch; + let p = prefix_set(c.p.0, c.p.1)[0]; + let prefix = |ops: &mut Vec, dst: u16| { + ops.push(MaskOp::Pred { + pred: Pred::Range { lo: p.0, hi: p.1 }, + under: None, + dst, + }) + }; + match f { + F::F1 => { + emit_box(&mut ops, c.b[0], 0); + prefix(&mut ops, 1); + ops.push(MaskOp::And { + a: sc(0), + b: sc(1), + dst: 0, + }); + } + F::F2 => { + emit_box(&mut ops, c.b[0], 0); + emit_box(&mut ops, c.b[1], 1); + ops.push(MaskOp::Or { + a: sc(0), + b: sc(1), + dst: 0, + }); + } + F::F3 => { + emit_box(&mut ops, c.b[0], 0); + prefix(&mut ops, 1); + ops.push(MaskOp::AndNot { + a: sc(1), + b: sc(0), + dst: 0, + }); + } + F::F4 => { + emit_box(&mut ops, c.b[0], 0); + emit_box(&mut ops, c.b[1], 1); + emit_box(&mut ops, c.b[2], 2); + ops.push(MaskOp::Or { + a: sc(0), + b: sc(1), + dst: 0, + }); + ops.push(MaskOp::AndNot { + a: sc(0), + b: sc(2), + dst: 0, + }); + } + } + Program::new(ops, Terminal::Count { mask: sc(0) }) +} + +fn count(v: Value) -> u64 { + match v { + Value::Count(n) => n as u64, + other => panic!("expected a count, got {other:?}"), + } +} + +struct Lanes { + x: Vec, + y: Vec, +} + +fn lanes() -> Lanes { + Lanes { + x: (0..N).map(|r| gather(r) as i32).collect(), + y: (0..N).map(|r| gather(r >> 1) as i32).collect(), + } +} + +/// A program, recognised once, with its own scratch. +struct Prepared { + prog: CompiledProgram, + scratch: Scratch<'static>, +} + +fn prepare(p: Program) -> Prepared { + let scratch = Scratch::for_program(&p, N as usize).expect("scratch"); + Prepared { + prog: CompiledProgram::new(p), + scratch, + } +} + +fn run(p: &mut Prepared, l: &Lanes) -> u64 { + let ls = [LaneRef::I32(&l.x), LaneRef::I32(&l.y)]; + let planes = Planes { + n_rows: N as usize, + masks: &[], + lanes: &ls, + }; + let v = execute_compiled( + &p.prog.compiled(), + &planes, + &Foreign::NONE, + &mut p.scratch, + Out::None, + 0..N as usize, + ); + count(v.expect("run")) +} + +fn time(mut f: F) -> f64 { + f(); + let mut v: Vec = (0..11) + .map(|_| { + let t = Instant::now(); + let mut n = 0u32; + while n < 3 || t.elapsed().as_micros() < 2_000 { + f(); + n += 1; + } + t.elapsed().as_nanos() as f64 / f64::from(n) + }) + .collect(); + v.sort_by(|a, b| a.total_cmp(b)); + v[5] +} + +fn bx(x0: u32, x1: u32, y0: u32, y1: u32) -> Box2 { + Box2 { x0, x1, y0, y1 } +} + +fn cases() -> Vec<(&'static str, Case)> { + let sq = |s: u32| bx(0, s, 0, s); + vec![ + ( + "aligned 16", + Case { + b: [sq(16), bx(16, 32, 0, 16), bx(0, 8, 0, 8)], + p: (1, 0), + }, + ), + ( + "aligned 64", + Case { + b: [sq(64), bx(64, 128, 0, 64), bx(0, 32, 0, 32)], + p: (1, 0), + }, + ), + ( + "aligned 256", + Case { + b: [sq(256), sq(256), bx(0, 128, 0, 128)], + p: (1, 0), + }, + ), + ( + "unaligned", + Case { + b: [bx(3, 61, 5, 59), bx(40, 99, 30, 77), bx(10, 20, 10, 20)], + p: (1, 0), + }, + ), + ( + "thin 256x1", + Case { + b: [bx(0, 256, 7, 8), bx(0, 256, 9, 10), bx(0, 128, 7, 8)], + p: (2, 1), + }, + ), + ( + "thin 1x256", + Case { + b: [bx(7, 8, 0, 256), bx(9, 10, 0, 256), bx(7, 8, 0, 128)], + p: (2, 1), + }, + ), + ] +} + +fn main() { + println!("tier: {}", tier()); + let l = lanes(); + println!( + "shape\tformula\tintervals_in\tintervals_out\tS_eval_ns\tS_total_ns\tM_ns\tV_ns\tM_ops\tanswer" + ); + for (name, c) in cases() { + let lv = leaves(&c); + let n_in: usize = lv.iter().map(Vec::len).sum(); + for (fname, f) in [("F1", F::F1), ("F2", F::F2), ("F3", F::F3), ("F4", F::F4)] { + let sym = symbolic(f, &c); + let want = oracle(f, &c); + assert!( + well_formed(&sym), + "{name} {fname} left the interval-set form" + ); + assert_eq!(measure(&sym), want, "{name} {fname} S"); + let mprog = mask_program(f, &c); + let m_ops = mprog.ops.len(); + let mut mp = prepare(mprog); + let mut vp = prepare(value_program(f, &c)); + assert_eq!(run(&mut mp, &l), want, "{name} {fname} M"); + assert_eq!(run(&mut vp, &l), want, "{name} {fname} V"); + let se = time(|| { + black_box(measure(&eval(f, black_box(&lv)))); + }); + let st = time(|| { + black_box(measure(&symbolic(f, black_box(&c)))); + }); + let mn = time(|| { + black_box(run(&mut mp, &l)); + }); + let vn = time(|| { + black_box(run(&mut vp, &l)); + }); + println!( + "{name}\t{fname}\t{n_in}\t{}\t{se:.0}\t{st:.0}\t{mn:.0}\t{vn:.0}\t{m_ops}\t{want}", + sym.len() + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn spread(v: u32) -> u32 { + let mut x = v & 0xFF; + x = (x | (x << 4)) & 0x0F0F; + x = (x | (x << 2)) & 0x3333; + (x | (x << 1)) & 0x5555 + } + + /// FAILS IF: the x / y lanes are not the Morton decoding of the row, so + /// the V arm and the oracle would describe a different layout than S. + #[test] + fn gather_inverts_morton() { + for x in 0..256 { + for y in 0..256 { + let r = spread(x) | (spread(y) << 1); + assert_eq!((gather(r), gather(r >> 1)), (x, y)); + } + } + } + + /// FAILS IF: a box's interval set does not contain exactly the box's + /// cells, or is not well formed. + #[test] + fn box_sets_are_exact() { + for b in [ + bx(0, 16, 0, 16), + bx(3, 61, 5, 59), + bx(0, 256, 7, 8), + bx(7, 8, 0, 256), + bx(255, 256, 255, 256), + ] { + let set = box_set(b); + assert!(well_formed(&set)); + let cells = (0..N).filter(|&r| in_box(b, r)).count() as u64; + assert_eq!(measure(&set), cells); + for &(lo, hi) in &set { + assert!((lo..hi).all(|r| in_box(b, r))); + } + } + assert_eq!(box_set(bx(0, 16, 0, 16)).len(), 1); + assert!( + box_set(bx(7, 8, 0, 256)).len() >= 128, + "thin boxes fragment" + ); + } + + /// FAILS IF: any formula, in any arm, differs from the row oracle, or the + /// symbolic result leaves the interval-set form. + #[test] + fn every_formula_matches_the_oracle_in_every_arm() { + let l = lanes(); + for (_, c) in cases() { + for f in [F::F1, F::F2, F::F3, F::F4] { + let sym = symbolic(f, &c); + let want = oracle(f, &c); + assert!(well_formed(&sym)); + assert_eq!(measure(&sym), want); + assert_eq!(run(&mut prepare(mask_program(f, &c)), &l), want); + assert_eq!(run(&mut prepare(value_program(f, &c)), &l), want); + } + } + } + + /// FAILS IF: the fixture is degenerate: some formula answers 0 or N + /// everywhere, so complement and intersection would not be exercised. + #[test] + fn formulas_discriminate() { + for (name, c) in cases() { + for f in [F::F1, F::F3, F::F4] { + let n = oracle(f, &c); + assert!(n < u64::from(N), "{name}: a formula covers the whole tile"); + } + assert!(oracle(F::F3, &c) > 0 || oracle(F::F1, &c) > 0, "{name}"); + } + } +} diff --git a/crates/lance-graph-mask-risc/examples/holo_k_branch_probe.rs b/crates/lance-graph-mask-risc/examples/holo_k_branch_probe.rs new file mode 100644 index 000000000..9497090ff --- /dev/null +++ b/crates/lance-graph-mask-risc/examples/holo_k_branch_probe.rs @@ -0,0 +1,533 @@ +//! D-HOLO H1: one coordinate-aligned Boolean constraint answered for K thought +//! branches at once. +//! +//! Pre-registered in `.claude/plans/d-holo-address-indexed-algebra-v1.md` (H1). +//! Query: for each branch k in T, count(P ∧ Q ∧ M_k), with P an address +//! aperture, Q = v < t and M_k branch k's coverage. +//! +//! - A: mask-risc, one program per branch (P ∧ Q recomputed each time). +//! - B: mask-risc, G = P ∧ Q kept once into a caller mask, then one +//! `And` + `Count` program per branch. +//! - C: outside the contract: one pass over the rows of G adding the +//! per-row written-by word into 64 position counters. +//! - D: snapshot baseline: per branch a materialized row-id list, filtered by +//! P ∧ Q. +//! +//! Every answer is checked against a row-at-a-time oracle. Input +//! construction (thought planes, row words, snapshot lists) is timed and +//! charged. +//! +//! `cargo run --release -p lance-graph-mask-risc --example holo_k_branch_probe` +//! `cargo test -p lance-graph-mask-risc --example holo_k_branch_probe` + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::hint::black_box; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::Instant; + +use lance_graph_mask_risc::exec::{execute_compiled, Scratch}; +use lance_graph_mask_risc::{ + words_for, CompiledProgram, Foreign, LaneRef, MaskOp, Operand, Out, Planes, Pred, Program, + Terminal, Value, +}; + +struct Counting; +static ALLOCS: AtomicUsize = AtomicUsize::new(0); +unsafe impl GlobalAlloc for Counting { + unsafe fn alloc(&self, l: Layout) -> *mut u8 { + ALLOCS.fetch_add(1, Ordering::Relaxed); + // SAFETY: forwards the caller's layout unchanged to the system allocator. + unsafe { System.alloc(l) } + } + unsafe fn dealloc(&self, p: *mut u8, l: Layout) { + // SAFETY: `p` was returned by `System.alloc` with this layout. + unsafe { System.dealloc(p, l) } + } +} +#[global_allocator] +static A: Counting = Counting; + +const N: usize = 65_536; +const W: usize = N / 64; +const THOUGHTS: usize = 64; + +fn tier() -> &'static str { + if cfg!(target_feature = "avx512f") { + "v4 (avx512f)" + } else if cfg!(target_feature = "avx2") { + "v3 (avx2): timings not reportable" + } else { + "below v3: timings not reportable" + } +} + +fn mix(mut x: u64) -> u64 { + x = x.wrapping_add(0x9E37_79B9_7F4A_7C15); + x = (x ^ (x >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + x = (x ^ (x >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + x ^ (x >> 31) +} + +struct Fixture { + v: Vec, + /// Canonical coverage: bit t of `w[r]` = thought t covers row r. + w: Vec, +} + +fn fixture(seed: u64) -> Fixture { + let v = (0..N) + .map(|r| (mix(seed ^ r as u64) % 1000) as i32) + .collect(); + let mut w = vec![0u64; N]; + for (r, word) in w.iter_mut().enumerate() { + for t in 0..THOUGHTS { + if mix(seed.rotate_left(17) ^ ((r as u64) << 8) ^ t as u64).is_multiple_of(10) { + *word |= 1 << t; + } + } + } + Fixture { v, w } +} + +/// Per-thought planes M_t (representation maintenance, charged). +fn build_planes(w: &[u64]) -> Vec> { + let mut m = vec![vec![0u64; W]; THOUGHTS]; + for (r, &word) in w.iter().enumerate() { + let mut b = word; + while b != 0 { + let t = b.trailing_zeros() as usize; + b &= b - 1; + m[t][r / 64] |= 1 << (r % 64); + } + } + m +} + +/// Row words built from per-thought planes: the other orientation of the +/// same coverage (charged when planes are the stored form). +fn build_words(m: &[Vec]) -> Vec { + let mut w = vec![0u64; N]; + for (t, plane) in m.iter().enumerate() { + for (wi, &word) in plane.iter().enumerate() { + let mut b = word; + while b != 0 { + let r = wi * 64 + b.trailing_zeros() as usize; + b &= b - 1; + w[r] |= 1 << t; + } + } + } + w +} + +/// Per-branch materialized row lists (snapshot baseline input, charged). +fn build_lists(w: &[u64]) -> Vec> { + let mut l = vec![Vec::new(); THOUGHTS]; + for (r, &word) in w.iter().enumerate() { + let mut b = word; + while b != 0 { + let t = b.trailing_zeros() as usize; + b &= b - 1; + l[t].push(r as u32); + } + } + l +} + +#[derive(Clone, Copy)] +struct Cell { + lo: usize, + hi: usize, + t: i32, + k: usize, +} + +fn sel_mask(k: usize) -> u64 { + if k >= 64 { + u64::MAX + } else { + (1u64 << k) - 1 + } +} + +fn oracle(f: &Fixture, c: Cell) -> Vec { + let mut out = vec![0u64; c.k]; + for r in c.lo..c.hi { + if f.v[r] < c.t { + for (t, o) in out.iter_mut().enumerate() { + *o += (f.w[r] >> t) & 1; + } + } + } + out +} + +// ---------- arm A: one program per branch ---------- + +fn prog_a(c: Cell, plane: u16) -> Program { + Program::new( + vec![ + MaskOp::Pred { + pred: Pred::LtI32 { lane: 0, t: c.t }, + under: Some(Operand::Plane(plane)), + dst: 0, + }, + MaskOp::Pred { + pred: Pred::Range { + lo: c.lo as u32, + hi: c.hi as u32, + }, + under: Some(Operand::Scratch(0)), + dst: 1, + }, + ], + Terminal::Count { + mask: Operand::Scratch(1), + }, + ) +} + +// ---------- arm B: gate once, then one And + Count per branch ---------- + +fn prog_gate(c: Cell) -> Program { + Program::new( + vec![ + MaskOp::Pred { + pred: Pred::Range { + lo: c.lo as u32, + hi: c.hi as u32, + }, + under: None, + dst: 0, + }, + MaskOp::Pred { + pred: Pred::LtI32 { lane: 0, t: c.t }, + under: Some(Operand::Scratch(0)), + dst: 1, + }, + ], + Terminal::Keep { + mask: Operand::Scratch(1), + }, + ) +} + +fn prog_b(plane: u16) -> Program { + // Plane 0 is G; branch k is plane k + 1. + Program::new( + vec![MaskOp::And { + a: Operand::Plane(0), + b: Operand::Plane(plane), + dst: 0, + }], + Terminal::Count { + mask: Operand::Scratch(0), + }, + ) +} + +fn count(v: Value) -> u64 { + match v { + Value::Count(n) => n as u64, + other => panic!("expected a count, got {other:?}"), + } +} + +struct Arms<'a> { + f: &'a Fixture, + m: &'a [Vec], + lists: &'a [Vec], +} + +/// Prepared programs and scratch for one cell: planning happens once, +/// outside the timed region, like a prepared statement. +struct Prepared { + a: Vec, + gate: CompiledProgram, + b: Vec, + sa: Scratch<'static>, + sg: Scratch<'static>, + sb: Scratch<'static>, +} + +fn prepare(c: Cell) -> Prepared { + let a: Vec = (0..c.k).map(|k| prog_a(c, k as u16)).collect(); + let b: Vec = (0..c.k).map(|k| prog_b(k as u16 + 1)).collect(); + let gate = prog_gate(c); + let sa = Scratch::for_program(&a[0], N).expect("scratch"); + let sb = Scratch::for_program(&b[0], N).expect("scratch"); + let sg = Scratch::for_program(&gate, N).expect("scratch"); + // Lowering recognition happens here, once, not in the timed path. + Prepared { + a: a.into_iter().map(CompiledProgram::new).collect(), + gate: CompiledProgram::new(gate), + b: b.into_iter().map(CompiledProgram::new).collect(), + sa, + sg, + sb, + } +} + +impl Arms<'_> { + fn thought_planes(&self) -> [&[u64]; THOUGHTS] { + std::array::from_fn(|i| self.m[i].as_slice()) + } + + fn a(&self, c: Cell, p: &mut Prepared, out: &mut [u64]) { + let masks = self.thought_planes(); + let lanes = [LaneRef::I32(&self.f.v)]; + let planes = Planes { + n_rows: N, + masks: &masks, + lanes: &lanes, + }; + for (prog, o) in p.a.iter().zip(out.iter_mut()) { + *o = count( + execute_compiled( + &prog.compiled(), + &planes, + &Foreign::NONE, + &mut p.sa, + Out::None, + c.lo..c.hi, + ) + .expect("A"), + ); + } + } + + fn gate(&self, c: Cell, p: &mut Prepared, g: &mut [u64]) { + let lanes = [LaneRef::I32(&self.f.v)]; + let planes = Planes { + n_rows: N, + masks: &[], + lanes: &lanes, + }; + g.fill(0); + execute_compiled( + &p.gate.compiled(), + &planes, + &Foreign::NONE, + &mut p.sg, + Out::Mask(g), + c.lo..c.hi, + ) + .expect("gate"); + } + + fn b(&self, c: Cell, p: &mut Prepared, g: &mut [u64], out: &mut [u64]) { + self.gate(c, p, g); + let g: &[u64] = g; + let masks: [&[u64]; THOUGHTS + 1] = + std::array::from_fn(|i| if i == 0 { g } else { self.m[i - 1].as_slice() }); + let planes = Planes { + n_rows: N, + masks: &masks, + lanes: &[], + }; + for (prog, o) in p.b.iter().zip(out.iter_mut()) { + *o = count( + execute_compiled( + &prog.compiled(), + &planes, + &Foreign::NONE, + &mut p.sb, + Out::None, + c.lo..c.hi, + ) + .expect("B"), + ); + } + } + + /// Outside the contract: positional popcount of the written-by words + /// over the rows of G. Fixed 64 counters, whatever K is. + fn c(&self, c: Cell, p: &mut Prepared, g: &mut [u64], out: &mut [u64]) { + self.gate(c, p, g); + let sel = sel_mask(c.k); + let mut cnt = [0u32; 64]; + let words = g.iter().enumerate(); + for (wi, &gw) in words.take(c.hi.div_ceil(64)).skip(c.lo / 64) { + let mut b = gw; + while b != 0 { + let r = wi * 64 + b.trailing_zeros() as usize; + b &= b - 1; + let x = self.f.w[r] & sel; + for (j, ct) in cnt.iter_mut().enumerate() { + *ct += ((x >> j) & 1) as u32; + } + } + } + for (o, &ct) in out.iter_mut().zip(cnt.iter()).take(c.k) { + *o = u64::from(ct); + } + } + + /// Snapshot baseline: each branch is a materialized list of its rows. + fn d(&self, c: Cell, out: &mut [u64]) { + for (k, o) in out.iter_mut().enumerate().take(c.k) { + let l = &self.lists[k]; + let a = l.partition_point(|&r| (r as usize) < c.lo); + let b = l.partition_point(|&r| (r as usize) < c.hi); + *o = l[a..b] + .iter() + .filter(|&&r| self.f.v[r as usize] < c.t) + .count() as u64; + } + } +} + +/// Median of 11 runs, ns per call, plus the allocation count of one call. +fn time(mut f: F) -> (f64, usize) { + f(); + let before = ALLOCS.load(Ordering::Relaxed); + f(); + let allocs = ALLOCS.load(Ordering::Relaxed) - before; + let mut v: Vec = (0..11) + .map(|_| { + let t = Instant::now(); + let mut n = 0u32; + while n < 3 || t.elapsed().as_micros() < 2_000 { + f(); + n += 1; + } + t.elapsed().as_nanos() as f64 / f64::from(n) + }) + .collect(); + v.sort_by(|a, b| a.total_cmp(b)); + (v[5], allocs) +} + +fn main() { + println!("tier: {}", tier()); + let f = fixture(0x5EED); + let t0 = Instant::now(); + let m = black_box(build_planes(&f.w)); + let m_ns = t0.elapsed().as_nanos() as f64 / N as f64; + let t0 = Instant::now(); + let lists = black_box(build_lists(&f.w)); + let l_ns = t0.elapsed().as_nanos() as f64 / N as f64; + let t0 = Instant::now(); + let w2 = black_box(build_words(&m)); + let w_ns = t0.elapsed().as_nanos() as f64 / N as f64; + assert_eq!(w2, f.w, "row words rebuilt from planes"); + println!( + "# construction (charged): thought planes from row words {m_ns:.2} ns/row ({} KiB); row words from planes {w_ns:.2} ns/row ({} KiB); snapshot lists {l_ns:.2} ns/row ({} KiB)", + THOUGHTS * W * 8 / 1024, + N * 8 / 1024, + lists.iter().map(|l| l.len() * 4).sum::() / 1024 + ); + let arms = Arms { + f: &f, + m: &m, + lists: &lists, + }; + let mut g = vec![0u64; words_for(N)]; + let mut out = vec![0u64; THOUGHTS]; + println!("s\taperture\tK\tA_ns\tB_ns\tC_ns\tD_ns\tA_alloc\tB_alloc\tC_alloc\tD_alloc\tC_K/C_1"); + for &(sname, t) in &[("1%", 10), ("10%", 100), ("50%", 500)] { + for &(aname, lo, hi) in &[("65536", 0usize, N), ("4096", 7 * 4096, 8 * 4096)] { + let mut c1 = 0.0; + for &k in &[1usize, 8, 64] { + let c = Cell { lo, hi, t, k }; + let want = oracle(&f, c); + let mut p = prepare(c); + arms.a(c, &mut p, &mut out); + assert_eq!(&out[..k], &want[..], "A {sname} {aname} K={k}"); + arms.b(c, &mut p, &mut g, &mut out); + assert_eq!(&out[..k], &want[..], "B"); + arms.c(c, &mut p, &mut g, &mut out); + assert_eq!(&out[..k], &want[..], "C"); + arms.d(c, &mut out); + assert_eq!(&out[..k], &want[..], "D"); + let (a, aa) = time(|| arms.a(c, &mut p, &mut out)); + let (b, ba) = time(|| arms.b(c, &mut p, &mut g, &mut out)); + let (cc, ca) = time(|| arms.c(c, &mut p, &mut g, &mut out)); + let (d, da) = time(|| arms.d(c, &mut out)); + if k == 1 { + c1 = cc; + } + println!( + "{sname}\t{aname}\t{k}\t{a:.0}\t{b:.0}\t{cc:.0}\t{d:.0}\t{aa}\t{ba}\t{ca}\t{da}\t{:.2}", + cc / c1 + ); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn arms_check(seed: u64) { + let f = fixture(seed); + let m = build_planes(&f.w); + let lists = build_lists(&f.w); + let arms = Arms { + f: &f, + m: &m, + lists: &lists, + }; + let mut g = vec![0u64; words_for(N)]; + let mut out = vec![0u64; THOUGHTS]; + for &(lo, hi) in &[(0, N), (4096, 8192), (100, 3_000)] { + for &t in &[10, 500] { + for &k in &[1usize, 8, 64] { + let c = Cell { lo, hi, t, k }; + let want = oracle(&f, c); + let mut p = prepare(c); + // Poison the shared gate buffer before each arm, so no arm + // can pass by reading a mask another arm left behind. + g.fill(u64::MAX); + arms.a(c, &mut p, &mut out); + assert_eq!(&out[..k], &want[..], "A"); + g.fill(u64::MAX); + arms.b(c, &mut p, &mut g, &mut out); + assert_eq!(&out[..k], &want[..], "B"); + g.fill(u64::MAX); + arms.c(c, &mut p, &mut g, &mut out); + assert_eq!(&out[..k], &want[..], "C"); + arms.d(c, &mut out); + assert_eq!(&out[..k], &want[..], "D"); + } + } + } + } + + /// FAILS IF: any arm differs from the oracle on any cell, including an + /// aperture that is not word-aligned (100..3000). + #[test] + fn every_arm_matches_the_oracle() { + arms_check(3); + } + + /// FAILS IF: the fixture is degenerate: branches identical, or the + /// aperture or the predicate not excluding anything. + #[test] + fn the_fixture_discriminates() { + let f = fixture(3); + let c = Cell { + lo: 4096, + hi: 8192, + t: 100, + k: 64, + }; + let o = oracle(&f, c); + let full = oracle( + &f, + Cell { + lo: 0, + hi: N, + t: 1000, + k: 64, + }, + ); + assert!(o.iter().any(|&x| x != o[0]), "branches must differ"); + assert!(o.iter().all(|&x| x > 0)); + assert!( + o.iter().zip(&full).all(|(a, b)| a * 20 < *b), + "P ∧ Q must exclude most rows" + ); + } +}