diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 62d9fe646..b3a0080be 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,60 @@ +## 2026-09-17 (18) — E-THE-SECOND-FACET-IS-NOT-AN-EDGE-BLOCK-1 — bytes 16..32 are just another content-blind facet cascade; giving them their own type was how the V1 `12 + 4` carving survived its own retirement + +**Status:** OPERATOR-RULED (verbatim below) + SHIPPED (`pub type EdgeBlock = +FacetCascade;`, `FacetCascade::as_bytes_mut`, every field site migrated +byte-for-byte). Residue named in +`ISS-EDGE-BLOCK-WAS-A-SECOND-TYPE-FOR-THE-SAME-FACET`. +**Confidence:** HIGH — the ruling is the operator's; the mechanics are a +type alias over an identical `repr(C, align(16))` 16-byte layout, 1425 +contract tests unchanged, every in-tree consumer builds. + +### The ruling + +> *"It's forbidden for the edge block to even know it's an edge block — +> it's just another content blind facet cascade."* +> *"How hard can it be to fold `[u16;8]` / `[u8;16]` — same algorithm, +> just 6 vs 8, 12 vs 16."* +> *"What you call edge codec flavor, quoting 12+4 in/out family, is a V1 +> contamination."* + +### What it corrects, including in this session + +The canon had retired the `12 in-family + 4 out-of-family` carving as a +V1-LEGACY *reading* — but `EdgeBlock { in_family: [u8; 12], out_family: +[u8; 4] }` was that reading spelled as a *type*, and a type outlives a +regraded paragraph. Every `.in_family` / `.out_family` access re-asserted +it. This session then proposed, in order: a fourth `EdgeCodecFlavor` +(`Refs16`), a second view type (`EdgeRefs`), an 8×2×8 `T8` facet shape, a +`ColumnDescriptor::class_id` field, and a §3c ruling — each one a +consequence of treating the second 16 bytes as something other than a +facet. All of it was discarded on the ruling. What remains is one line. + +### The shape that is left + +`NodeRow = key: FacetCascade | edges: FacetCascade | value(480)`. The +second facet's 4-byte prefix says what its six `(u8:u8)` rails ARE for +this row — the predicate, the codebook — and the ClassView projects them. +"Sixteen refs" is `4 + 12` read as L1 rails; the classid that a `T8` shape +or a lane-level field was invented to carry was already in the register. +`EdgeCodecFlavor` is how a class *reads* the second facet, which is all it +ever was. The fold is `shared::` on `[u8; N]`, one algorithm, `6` or +`8` — the cascade discipline from `E-THREE-CARRIERS-THREE-FOLDS-1` applies +to both facets identically because they are the same type. + +### The rule, generalized + +**A second type for the same bytes is a carving pretending to be a +layout.** The content-blind invariant is not "the ClassView chooses the +reading"; it is "there is no type in which a reading could hide." Where +the canon has already regraded a reading as legacy, grep for the *type* +that still spells it — the paragraph did not retire it if the struct is +still there. + +Cross-ref: `E-V3-FACET-4-PLUS-12` (now visibly true of bytes 16..32 as +well), `E-V1-TAIL-FORBIDDEN-V3-IS-CONTENT-BLIND-1`, +`E-THREE-CARRIERS-THREE-FOLDS-1`, CLAUDE.md § CANON (⊘ note appended), +`le-contract.md` §4 (bullet appended). + ## 2026-09-17 (17) — E-THREE-CARRIERS-THREE-FOLDS-1 — one workspace holds THREE prefix-fold carriers; entry (16) measured a real win on one of them and shipped it into another, where it is 2.1× SLOWER **Status:** MEASURED (`crates/lance-graph-contract/examples/facet_axis_lcp_probe.rs`, diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index c4699f7d3..f79091130 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -1,3 +1,87 @@ +## ISS-EDGE-BLOCK-WAS-A-SECOND-TYPE-FOR-THE-SAME-FACET (2026-09-17) — RESOLVED at the type; the readers that still split at 12 are the named residue + +**Operator ruling (verbatim, 2026-09-17):** *"It's forbidden for the edge block to even +know it's an edge block — it's just another content blind facet cascade."* And the diagnosis +that preceded it: *"what you call edge codec flavor … quoting 12+4 in/out family is a V1 +contamination."* + +**The contamination:** bytes 16..32 of the node row had their own type, `EdgeBlock +{ in_family: [u8; 12], out_family: [u8; 4] }`. The canon had already regraded the `12 + 4` +carving as *"NOT the layout … V1-LEGACY READING"* (CLAUDE.md § CANON), but a struct with +those two field names IS that reading, stated as a type — and every reader that touched +`.in_family` / `.out_family` re-asserted it. A session drafting an ontology edge reading +(this one) reached for a *fourth* `EdgeCodecFlavor` and a *second* view type beside it, +and wrote a test that pinned the 12+4 split as the baseline the new reading sat beside. +That draft was discarded; this is what replaced it. + +**Resolved at the type (this branch):** `pub type EdgeBlock = FacetCascade;` — the second +16 bytes are the same `4 + 12` content-blind facet as the key. Byte positions unchanged; +`NodeRow`, `node_rows_from_le_bytes`, every `EdgeBlock::default()` and every equality +compile unchanged (OGAR `lance_sink.rs` pulls the contract from git `main` and uses only +`default()` / `==`, so it is unaffected until merge and unbroken after). +`FacetCascade::as_bytes_mut` added (same `SAFETY` pattern as `as_bytes`) so the in-place +writers have a lens. `EdgeCodecFlavor` stays: it is how a ClassView READS the second facet, +which was always its job. + +**The residue — readers that still split the 16 bytes at 12 on their own authority** (all +migrated to `as_bytes()[..12]` / `[12..]` mechanically so the bytes they read are exactly +the bytes they read before; none was redesigned, per the "alias only" scope): + +| site | what it still believes | +|---|---| +| `lance-graph/src/graph/mailbox_scan.rs` `edge_slots_coarse` → `EdgeNeighbors { in_family, external }` | 12 local + 4 external | +| `lance-graph-contract/src/soa_graph.rs` render (`domain.in_family_edge` / `out_family_edge` labels) | two edge kinds by byte position | +| `lance-graph-contract/src/aiwar.rs` (writes bytes 12..16 as "the 4 canonical out-of-family slots") | 4 adapter slots | +| `symbiont/src/{domino,key_render,kanban_loop}.rs` (⊘ deprecated crate) | ring neighbour at byte 0, adapter at byte 12 | +| `lance-graph-callcenter/src/graph_table.rs` test fixture | adapter byte 0x0B at byte 12 | +| `deepnsm/examples/tri_fidelity_edges.rs`, `perturbation-sim/examples/reinforce.rs` (prose only) | "12 in-family + 4 out-of-family" | +| `EdgeCodecFlavor::CoarseOnly` doc comment | "the 12+4 block read literally" | + +Each becomes correct the moment its ClassView names what the second facet's prefix and +rails mean for that class — which is the whole point: the READING belongs to the class, +never to the slot. Closing this issue = every row above reads through the ClassView (or is +deleted with its crate). + +## ISS-MASK-RISC-HAD-NO-RANGE-OP (2026-09-17) — PARTLY RESOLVED: the IR op shipped; the quack lowering still waits on ordering knowledge + +**The chain, as found:** `ndarray::simd::mask_set_range(out_words, lo, hi)` is on the +facade (`simd_masking_ops.rs`, `pub fn mask_set_range`) — a three-pass contiguous fill, +no per-row compare. `lance-graph-mask-risc`'s IR had **no op that reaches it** (`ir.rs` +`Pred` was the ten lane predicates and nothing else). So `lance-graph-quack`'s +`Filter::prefix_u64` — *"the closest DuckDB comes to the V3 address"*, matrix row R5 — +lowered an address prefix to a full `MatchU64` ternary SWEEP, and its own doc said +*"NOT yet a range WRITE … waits on the primitive."* The primitive existed; the IR +name did not. **ndarray has it → mask-risc can't name it → quack sweeps.** + +Why it matters: on an address-ordered lane a prefix names ONE contiguous subtree, and +that is the whole reason V3 has the survivor-skip lever at all — `adaptive_order_probe`'s +clustered regime skips **99.61 %** of words precisely because the prefix term's +survivors fill one block. A range write makes that selection free instead of a sweep +that happens to be cheap. It is also the "mask from root, O(1) adjacent" half of the +similarity question (`E-…-POPCOUNT-×-SELF`, LATEST_STATE 2026-09-15 (6)–(8)) as an IR +op rather than a per-row match. + +**Resolved 2026-09-17 (this branch):** `Pred::Range { lo: u32, hi: u32 }` — rows +`lo <= i < hi`, reads no lane; exec = `mask_set_range` (+ `mask_and_assign` under a +gate); oracle = row-index predicate; `ExecError::RangeOutOfBounds` for `lo > hi` or +`hi > n_rows`, refused by the shared validator before any write. Three differential +tests across the eight row counts (word-edge shapes incl. 63/64/65; `== hi − lo` +counted through the EXECUTOR; identical refusal). Disable-verified: an off-by-one in +the exec arm fails two of the three. + +**Still OPEN — the lowering.** `Filter::prefix_u64` still emits the sweep, and +correctly so: the IR does not know whether a lane is address-ordered (a V3 table's +row address is its rail; a borrowed `&[u64]` of edge targets is not), and quack's +`Filter` carries no ordering evidence. Lowering a prefix to `Range` on an unordered +lane is a wrong answer, not an error. The remaining arm is therefore on the planner +side: an ordering witness on the lane (or on `Col`) that licenses `prefix → Range`. +Until it exists the sweep is the honest lowering, and the quack doc now says exactly +that. + +**Also named, not built:** `mask_set_range` has no `_under` sibling on the facade; +the gated form here is two passes (`set_range` then `and_assign`). One fused pass is +a T1 follow-up, measured-then-pinned like every other facade member. + ## ISS-NIBLEPATH-FOLD-IS-CARRIER-2-UNMASKED (2026-09-17) — OPEN, the one prefix fold that genuinely wants a mask, and does not have one `NiblePath::common_prefix_depth` (`crates/lance-graph-contract/src/hhtl.rs`, `fn common_prefix_depth`) is diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index efd6cba4a..b7e34ea08 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,70 @@ +## 2026-09-17 (3) — CONTRACT INVENTORY DELTA: `EdgeBlock` is now `pub type EdgeBlock = FacetCascade` (the V1 `12 + 4` struct is gone); `FacetCascade::as_bytes_mut` added + +- **State consumers should know:** bytes 16..32 of `NodeRow` are the same + `4 + 12` content-blind facet type as the key. `EdgeBlock` survives as a + NAME only; `.in_family` / `.out_family` no longer exist — read through + `as_bytes()`, write through the new `as_bytes_mut()`. Byte positions are + unchanged, `NODE_ROW_STRIDE` unchanged, `node_rows_from_le_bytes` + unchanged, no `ENVELOPE_LAYOUT_VERSION` bump. `EdgeBlock::default()` and + equality compile as before (OGAR `lance_sink.rs` uses only those, via git + `main`). Every in-tree field site (symbiont ×3, soa_graph, aiwar, + callcenter, weather-poc, mailbox_scan, contract tests) migrated + mechanically to the same bytes. +- **Operator ruling:** *"It's forbidden for the edge block to even know it's + an edge block — it's just another content blind facet cascade."* Board: + `E-THE-SECOND-FACET-IS-NOT-AN-EDGE-BLOCK-1`. +- **Residue named, not touched:** the readers that still split at 12 on + their own authority — `ISS-EDGE-BLOCK-WAS-A-SECOND-TYPE-FOR-THE-SAME-FACET`. +- **Withdrawn this session before landing:** `EdgeCodecFlavor::Refs16`, + `EdgeRefs`, the `T8` facet shape, `ColumnDescriptor::class_id`, and a + `le-contract.md` §3c ruling — all consequences of the contamination, all + discarded on the ruling. Nothing of them is on `main` or on this branch. + +## 2026-09-17 (2) — `Pred::Range` lands in mask-risc: the contiguous-range write the IR could not name — one IR variant + one error, NO contract inventory delta + +- **State consumers should know:** `lance-graph-mask-risc` gains + `Pred::Range { lo: u32, hi: u32 }` (rows `lo <= i < hi`, reads no lane) and + `ExecError::RangeOutOfBounds`. Exec is `ndarray::simd::mask_set_range` + (+ `mask_and_assign` under a gate); the oracle is a row-index predicate; + `lo <= hi <= n_rows` is refused by the shared validator before any write. + `D-MRX-7`, in PR on `claude/great-pascal-k96kok` (`c3931a1a`). +- **Why:** `mask_set_range` was on the ndarray facade with nothing in the IR + able to reach it, so `lance-graph-quack`'s `Filter::prefix_u64` — an + address prefix, which on an ordered lane is ONE contiguous subtree — + lowered to a full ternary sweep and said *"waits on the primitive"*. The + primitive was there; the name was not (`ISS-MASK-RISC-HAD-NO-RANGE-OP`). + This is the "mask from root, O(1) adjacent" half of the similarity + question ((6)–(8) below) as an op, and the mechanism behind the 99.61 % + clustered word-skip in `adaptive_order_probe`. +- **What did NOT move:** quack still lowers `prefix_u64` to the sweep, and + should — the IR does not know whether a lane is address-ordered, and + `Filter` carries no ordering evidence. The remaining arm is an ordering + witness on the planner side; quack's doc now says so. The + `lance-graph-contract` inventory is untouched. +- **Process receipt:** the first draft of the population test counted + through the oracle (`Fixture::count`) and a disable-run with an + off-by-one in the exec arm PASSED it — vacuous against the side under + test. Fixed to count through `execute`; the disable-run now fails 2 of 3. + Same session also walked into the ruff "commit BEFORE you disable" trap + once (`git checkout` after a disable-run reverted uncommitted work); + re-applied, then committed first and re-ran. + +## 2026-09-17 — PR #1245 merged (`83369cad`): the facet per-axis LCP is the `shared6` byte chain again, verbatim pre-#1241 — NO contract inventory delta + +`FacetCascade::{hi_distance, lo_distance}` are back to the by-value +`shared6` chain, byte-identical to `95e28637^`; #1241's masked `u128` +readout survives only as `masked_axis_oracle` under `#[cfg(test)]`. No +type added or removed; the public signature is exactly pre-#1241. Measured +in-tree (`examples/facet_axis_lcp_probe.rs`, oracle over every pair): chain +1.72 ns vs masked 3.64 on 64K random — #1241's ordering inverted on its own +carrier. Doctrine: `.claude/knowledge/three-prefix-fold-carriers.md` — +three prefix-fold carriers (bit-planes / nibble path / facet cascade), and +the rule *masking wins when the slice is granular, PEEK wins when the slice +is addressed*. The real masking opportunity is carrier 2 +(`NiblePath::common_prefix_depth`), filed as +`ISS-NIBLEPATH-FOLD-IS-CARRIER-2-UNMASKED`, gated on its own probe. Arc: +`PR_ARC_INVENTORY.md` 2026-09-17 #1245. + ## 2026-09-16 — PR #1241 merged (`d5d3f7ab`): the facet's per-axis LCP is one masked register readout — NO contract inventory delta `FacetCascade::hi_distance` / `lo_distance` now read diff --git a/.claude/board/PR_ARC_INVENTORY.md b/.claude/board/PR_ARC_INVENTORY.md index 29346e9c1..f33b0580e 100644 --- a/.claude/board/PR_ARC_INVENTORY.md +++ b/.claude/board/PR_ARC_INVENTORY.md @@ -1,3 +1,58 @@ +## 2026-09-17 — lance-graph PR #1245 (merged `83369cad`, branch `claude/great-pascal-k96kok`) — the facet's per-axis LCP goes BACK to the `shared6` byte chain, verbatim; #1241's masked readout is the test oracle now; three prefix-fold carriers named + +- **Added:** `examples/facet_axis_lcp_probe.rs` — four-arm benchmark + (A chain PEEK / B pack-to-`u64` / C masked `u128` / D shared-load PEEK), + 64K pairs × 8 workloads, min of 7, oracle over EVERY pair before timing, + depth-knob and anti-vacuity gates that `assert!` (a flat depth-0→5 slope + fails the process). `.claude/knowledge/three-prefix-fold-carriers.md` + (doctrine); `.claude/plans/three-carrier-blast-radius-v1.md` (read-only + census, gates only); `ISS-NIBLEPATH-FOLD-IS-CARRIER-2-UNMASKED`; EPIPHANIES + (17) `E-THREE-CARRIERS-THREE-FOLDS-1` + partial strike appended to (16); + STATUS_BOARD D-TCF-1..5. 8 files, 5 commits. +- **Reversed (of #1241):** `shared_axis(xor, mask)` + `HI_BYTES`/`LO_BYTES` + are gone from the shipped path. `shared6([u8; 6], [u8; 6])` and the + by-value `hi_distance(self, other: Self)` / `lo_distance` are restored + **byte-identical to `95e28637^`** (diff-verified; the first revert commit + had shipped a semantically-equivalent respelling — `6e79c784` corrected + it). The falsifier is REVERSED, not deleted: the masked form lives on as + `masked_axis_oracle` under `#[cfg(test)]`, so the test compares shipped + against oracle rather than against a restatement of itself. Public API is + exactly pre-#1241. +- **Locked:** *masking wins when the slice is granular; PEEK wins when the + slice is addressed.* A measurement is a statement about + `(operation, carrier, workload)`; drop the carrier and it is a slogan. + Three carriers: bit-planes (`mailbox_soa` `&[u64]`, mask — untouched); + nibble path (`NiblePath`, packed `u64`, mask — NOT DONE, walks + nibble-by-nibble); facet cascade (`FacetCascade`, 6×2×8 byte-addressed, + PEEK — this PR). Before moving a fold, name its carrier. +- **Measured (64K random pairs, release, min of 7, both axes):** A chain + **1.72 ns** vs C masked 3.64 — the opposite of #1241's "12.5 → 5.8"; A + wins at every depth 0..5 and on identical; both pack-to-register arms are + slowest (the author's prediction that B would win was wrong). A's + depth-0→5 slope +81–111% across runs is the anti-vacuity evidence. + Disassembly: LLVM never materializes the `[u8; 6]` — `movzbl`/`cmp` + straight against memory/`jne`; arm C issues the same loads then + reassembles, `movabs` mask, two `tzcnt` + `cmove`, `−32`, no early exit. + *"The gather dominated"* is refuted. What #1241's 12.5 ns timed is + unknown (no harness shipped with it); entry (17) gives the carrier-2 + account as the strongest available and labels it so. +- **Deferred / named:** `as_u128()` is NOT a reinterpret — it rebuilds + through `to_bytes()` while `as_bytes()` (the documented no-op) sits unused + on that path; the E-carve (`{axis} × {near, far}`, stride 2) and the + fixed-width-register sentence for doctrine §1; carrier-2's masked rewrite + (gated on its own probe, D-TCF-4); the census passes (D-TCF-5). None + built. +- **Review:** Codex 2× P2 on the probe (oracle checked 256 of 65,536; the + anti-vacuity check warned and exited 0) — both real, both fixed in + `c3de1d2e`. Two board gates red on the first push — `D-3CF-*` does not + match the D-id pattern (two letters first) → `D-TCF-*`; two `ISSUES.md` + line-number citations → stable symbol anchors. CodeRabbit had not + finished when the operator merged. +- **Confidence:** HIGH on the numbers (in-tree, reproducible, oracle-first, + three independent runs same ordering). HIGH that the restore is verbatim + (diff). The three-carrier taxonomy is a reading of the tree offered as + explanation, not a proof of what #1241 timed. + ## 2026-09-16 — lance-graph PR #1241 (merged `d5d3f7ab`, branch `claude/c64-6502-falsifier-shztkk`) — the facet's per-axis LCP reads the single register; the `"{0}{1}" -f` fold is done ONCE at mint - **Added:** `FacetCascade::{HI_BYTES, LO_BYTES}` (const byte masks over the diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 729cb257f..5bbded313 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -58,6 +58,7 @@ evaluates one; `execute` stays the consumer's call on a scratch it owns. | D-MRX-4 | `ternlog_dispatch.rs` — generated 256-arm match, regenerate-and-diff gated | Shipped (#1226 `0b1ebaa`; PR3; CI `--check` line wired) | all 256 immediates equal the bit-serial reference | | D-MRX-5 | the differential suite at 8 row counts × every op × both CI arms | Shipped (#1226 `0b1ebaa`; PR3; `x86-64-v3` in CI, `-v4` locally on 2026-09-14 — NEON/WASM/scalar unexercised) | anti-vacuity `survivors * 3 < n_rows` for the 8 non-`Ne` predicates, UNGATED, at `n_rows >= 3` (the two `Ne` shapes are selective by complement, and `n_rows ∈ {0, 1}` are execution-only — the bound is `0 < 0` there); the gate itself is pinned selective (`gated < ungated`) at `n_rows >= 64` | | D-MRX-6 | `examples/count_probe.rs` — reference / interpreted / fused, 0 B per execute | Shipped (#1226 `0b1ebaa`; PR3; four arms agree on the COUNT — 4855 at 64k rows; `interpreted` and `fused` gated at 0 B; timings printed from one run, not pinned) | counting allocator reads 0 after warm-up; three arms agree | +| D-MRX-7 | `Pred::Range { lo, hi }` — the contiguous-range write (`ndarray::simd::mask_set_range`) as an IR op; `ExecError::RangeOutOfBounds`; gate-aware | **In PR** (branch `claude/great-pascal-k96kok`, `c3931a1a`). Closes the IR half of `ISS-MASK-RISC-HAD-NO-RANGE-OP`; quack's `prefix_u64` lowering still waits on lane-ordering evidence (planner side) | three differential tests at the 8 row counts: word-edge shapes incl. 63/64/65 ungated / under plane / under scratch (values AND scratch words); `== hi − lo` through the EXECUTOR (the oracle-counted first draft was vacuous — disable-run passed it); identical `RangeOutOfBounds` on both paths. Disable-run: off-by-one in the exec arm fails 2 of 3 | ## bbb-nars-lowering (D-id minted 2026-09-07 with the operator ruling) diff --git a/.claude/v3/soa_layout/le-contract.md b/.claude/v3/soa_layout/le-contract.md index ae245eb54..5a620ad12 100644 --- a/.claude/v3/soa_layout/le-contract.md +++ b/.claude/v3/soa_layout/le-contract.md @@ -325,6 +325,11 @@ move; jc pillars prove the READING preserves the semantics. stays authoritative for the 512-byte row: `key(16) | edges(16) | value(480)`. The facet atom is the unit INSIDE value lanes (and the key itself is 16 bytes with the same 4+12 rhythm — see §3 open items). +- **2026-09-17 (operator ruling, `E-THE-SECOND-FACET-IS-NOT-AN-EDGE-BLOCK-1`):** + `edges(16)` is not a third thing — it is the same `4 + 12` facet as the + key (`pub type EdgeBlock = FacetCascade`). Its prefix says what its rails + ARE for that row; the ClassView projects them; `EdgeCodecFlavor` is how + a class reads it. Nothing about the slot knows it holds "edges". - The 480-byte value slab holds facets per the tenant schema: `classid_read_mode(c).value_schema` selects which lanes/layouts a class carries (`tenants.md` catalogues them). diff --git a/CLAUDE.md b/CLAUDE.md index d29fb3821..22d768de3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1687,6 +1687,15 @@ stride change. > fidelity: `ndarray::hpc::reliability` (`edge_codec_compare`). Append-only: > the V1 line is regraded in place, not deleted. +> **⊘ 2026-09-17 — the TYPE is retired too (operator ruling, +> `E-THE-SECOND-FACET-IS-NOT-AN-EDGE-BLOCK-1`):** *"It's forbidden for the +> edge block to even know it's an edge block — it's just another content +> blind facet cascade."* `EdgeBlock` is now `pub type EdgeBlock = +> FacetCascade;` — bytes 16..32 are the same `4 + 12` facet as the key, and +> the `in_family` / `out_family` fields no longer exist. A regraded +> paragraph does not retire a reading while a struct still spells it. +> Readers that still split at 12: `ISS-EDGE-BLOCK-WAS-A-SECOND-TYPE-FOR-THE-SAME-FACET`. + **Zero-fallback ladder (monotonic — zero = fall through to the broader default):** - `classid == 0x0000_0000` → default class, no prefix routing (dormant) - `family == 0x00_0000` → default basin, no neighborhood grouping (dormant) diff --git a/Cargo.toml b/Cargo.toml index 6152562cf..bbb12ede0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -221,9 +221,9 @@ resolver = "2" # Crate-level pins (canonical source of truth — workspace.dependencies is not # used; each consumer crate declares its own pin with exact-equals so the # whole graph resolves to the same semver): -# lance = "=11.0.0" (lance-arrow / lance-index / lance-linalg / +# lance = "11.*" (lance-arrow / lance-index / lance-linalg / # lance-namespace bump in lock-step) -# lancedb = "=0.38.0" +# lancedb = "0.38.*" # datafusion = "54" (+ datafusion-common / -expr / -sql / # -functions-aggregate) # arrow-array = "58" (+ arrow-schema / -buffer / -ipc / arrow) @@ -294,7 +294,7 @@ resolver = "2" # fork of `aws-smithy-json` fixed against `aws-smithy-types 1.7` -- the two # call sites need `.into()` and one wildcard arm; upstream's own compiler # hints spell both out. -lance = { version = "=11.0.0", default-features = false, features = [ +lance = { version = "11.*", default-features = false, features = [ "azure", "gcp", "oss", @@ -304,13 +304,24 @@ lance = { version = "=11.0.0", default-features = false, features = [ "goosefs", "geo", ] } -lance-linalg = "=11.0.0" -lance-index = "=11.0.0" -lancedb = { version = "=0.38.0", default-features = false } -# Exact, not caret: the lance family moves in EXACT lockstep -# (E-PIN-LANCE9-LANCEDB033-DF541-ARROW58-NO-DF53-1), and with no tracked -# lock the caret ranges these replaced would have taken the newest 58.x / -# 54.x silently. A bump is one deliberate PR — this is where it happens now. +lance-linalg = "11.*" +lance-index = "11.*" +lancedb = { version = "0.38.*", default-features = false } +# ⊘ OPERATOR RULING 2026-09-18 — NEVER PIN TO `.0.0`; FLOAT THE PATCH. +# *"never pin to x.00, always float x.*"* / *"so no decimal .0.0"*. The +# `=X.Y.0` exact-equals these replaced are RETIRED. Floating the patch is +# strictly SAFER here, not looser, and the manifest already records why: +# lines 143-146 above log a graph cargo could NOT satisfy, because our +# `lance = "=6.0.1"` was narrower than lancedb's transitive `lance = +# "=6.0.0"`. An exact pin on OUR side can only ever be equal to or narrower +# than what the family asks for, so it buys nothing and breaks the moment +# lancedb's transitive requirement moves by a patch. `11.*` resolves to +# whatever lancedb demands — today the identical 11.0.0 — and survives that +# move. ⊘ The struck rationale ("Exact, not caret: the lance family moves in +# EXACT lockstep ... A bump is one deliberate PR") is kept here as the reason +# the pins were once exact; the lockstep FACT still holds, and the deliberate +# bump is now the MAJOR (11 → 12), which floating the patch does not touch. +# arrow/datafusion were already caret and are unaffected. arrow = "58" arrow-array = "58" arrow-schema = "58" diff --git a/crates/holograph/Cargo.toml b/crates/holograph/Cargo.toml index 3ec9ca1c9..5c88e2cac 100644 --- a/crates/holograph/Cargo.toml +++ b/crates/holograph/Cargo.toml @@ -36,7 +36,7 @@ arrow-buffer = "58" datafusion = { version = "54", optional = true } # Lance columnar format — 100x faster random access than Parquet -lance = { version = "=11.0.0", optional = true, default-features = false } +lance = { version = "11.*", optional = true, default-features = false } # Async runtime tokio = { version = "1.49", features = ["rt-multi-thread", "macros"], optional = true } diff --git a/crates/lance-graph-callcenter/src/graph_table.rs b/crates/lance-graph-callcenter/src/graph_table.rs index e17823c9f..b754dae52 100644 --- a/crates/lance-graph-callcenter/src/graph_table.rs +++ b/crates/lance-graph-callcenter/src/graph_table.rs @@ -151,7 +151,7 @@ mod tests { /// GraphSnapshot → arrow tables. End-to-end head → DataFusion. fn sample_snapshot() -> GraphSnapshot { let mut a_edges = EdgeBlock::default(); - a_edges.out_family[0] = 0x0B; + a_edges.as_bytes_mut()[12] = 0x0B; let rows = [ NodeRow { key: osint_key(1, 0xA, 1), diff --git a/crates/lance-graph-catalog/Cargo.toml b/crates/lance-graph-catalog/Cargo.toml index f910bb523..9c3c2123f 100644 --- a/crates/lance-graph-catalog/Cargo.toml +++ b/crates/lance-graph-catalog/Cargo.toml @@ -14,7 +14,7 @@ categories = ["database", "data-structures", "science"] arrow-schema = { workspace = true } async-trait = "0.1" datafusion = { workspace = true } -lance-namespace = "=11.0.0" +lance-namespace = "11.*" reqwest = { version = "0.12", features = ["json"], optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/lance-graph-contract/src/aiwar.rs b/crates/lance-graph-contract/src/aiwar.rs index e91563d4c..42d212248 100644 --- a/crates/lance-graph-contract/src/aiwar.rs +++ b/crates/lance-graph-contract/src/aiwar.rs @@ -108,7 +108,7 @@ pub fn aiwar_node_rows(graph: &LiteralGraph) -> Vec { // 4 canonical out-of-family slots. let mut edges = EdgeBlock::default(); for (k, &b) in slots.iter().take(4).enumerate() { - edges.out_family[k] = b; + edges.as_bytes_mut()[12 + k] = b; } NodeRow { // Route through `mint_for` so the classid's registered diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index 87545aec6..5039b5be7 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -666,19 +666,26 @@ impl core::fmt::Display for NodeGuid { } } -/// 16-byte canonical edge block: 12 in-family + 4 out-of-family. +/// Bytes 16..32 of the node row — **just another content-blind facet** +/// (operator ruling, 2026-09-17: *"It's forbidden for the edge block to even +/// know it's an edge block; it's just another content blind facet cascade"*). /// -/// Canonical, not mandatory: the 16 bytes are ALWAYS reserved (zeroed when unused). -/// A class never shrinks this block — opting out of edges is resolved via -/// classid → ClassView in the registry, never by changing the row stride. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -#[repr(C, align(16))] -pub struct EdgeBlock { - /// 12 local adjacency slots (basin-local), one byte each. - pub in_family: [u8; 12], - /// 4 inherited adapter slots (out-of-family interfaces), one byte each. - pub out_family: [u8; 4], -} +/// The same 16-byte `4 + 12` register as the key: a classid prefix that says +/// what the six `(u8:u8)` rails ARE for this row (the predicate, the codebook), +/// and rails the `ClassView` projects. Nothing about the slot knows it holds +/// "edges"; [`EdgeCodecFlavor`] is how a class *reads* this second facet. +/// +/// `EdgeBlock` is kept as a NAME for source compatibility only. The V1 +/// carving it used to present — `12 in-family + 4 out-of-family` one-byte +/// slots — is **retired**: it was never the layout (CLAUDE.md § CANON, the ⊘ +/// V1-LEGACY READING note), and giving those 16 bytes their own type with +/// those field names is what let the carving pass for one. Byte positions +/// are unchanged — a reader that still splits at 12 does so on its own +/// authority, and every such site is named in +/// `ISS-EDGE-BLOCK-WAS-A-SECOND-TYPE-FOR-THE-SAME-FACET`. New code says +/// `FacetCascade` and reads through `as_bytes()` / the ClassView; migration +/// pointer per I-LEGACY-API-FEATURE-GATED. +pub type EdgeBlock = crate::facet::FacetCascade; /// Which edge-codec flavor a class uses to *read* its node's edge block. /// @@ -1672,9 +1679,15 @@ impl<'a> SoaEnvelope for NodeRowPacket<'a> { // alignment of NodeRow (64) ⊇ alignment of u8 (1)). // // The NodeGuid and EdgeBlock fields hold their bytes in canon-LE - // order (NodeGuid::new uses to_le_bytes; EdgeBlock is plain [u8;_]), - // so the resulting byte slice IS the envelope's LE packet — no - // translation needed at the boundary. + // order, so the resulting byte slice IS the envelope's LE packet — no + // translation needed at the boundary. NodeGuid earns that by being + // `[u8; 16]` outright. EdgeBlock does NOT: since the 2026-09-17 alias + // it is a `FacetCascade`, whose `facet_classid` is a native-endian + // `u32`, so this cast reproduces the LE packet only on a little-endian + // target. That is enforced, not assumed — `facet::` carries a + // crate-level `const _: () = assert!(cfg!(target_endian = "little"))` + // for exactly this path (codex P2 on #1246). Do not restate EdgeBlock + // as a byte array here; it is the one field that is not. unsafe { core::slice::from_raw_parts( self.rows.as_ptr().cast::(), @@ -1726,9 +1739,12 @@ pub fn node_rows_from_le_bytes(bytes: &[u8]) -> Option<&[NodeRow]> { // (const-asserted above). We checked (1) bytes.len() is an exact multiple of // the stride, so n rows span the whole slice with no trailing bytes, and (2) // the pointer is aligned to align_of::() (64). Every bit pattern in - // the 512 bytes is a valid NodeRow (NodeGuid is bytes, EdgeBlock is [u8;16], - // value is [u8;480] — no niche/enum to invalidate), so the reinterpretation - // is sound. The returned slice borrows `bytes` for its lifetime (no copy). + // the 512 bytes is a valid NodeRow (NodeGuid is `[u8; 16]`; EdgeBlock is a + // `FacetCascade`, 16 B `repr(C, align(16))` whose fields are all plain + // integers, so it too has no niche; value is `[u8; 480]`) — nothing to + // invalidate, so the reinterpretation is sound. Soundness does not imply + // byte-identity across targets: see `as_le_bytes` above and the + // little-endian assert in `facet::` that this path also relies on. The returned slice borrows `bytes` for its lifetime (no copy). Some(unsafe { core::slice::from_raw_parts(bytes.as_ptr().cast::(), n) }) } @@ -2038,18 +2054,18 @@ mod tests { let rows = vec![ NodeRow { key: NodeGuid::new(NodeGuid::CLASSID_OSINT, 1, 2, 3, 0xAB, 0xCD), - edges: EdgeBlock { - in_family: [0xA1; 12], - out_family: [0xB2; 4], - }, + edges: EdgeBlock::from_bytes(&[ + 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xA1, 0xB2, + 0xB2, 0xB2, 0xB2, + ]), value: [7u8; 480], }, NodeRow { key: NodeGuid::new(NodeGuid::CLASSID_PROJECT, 4, 5, 6, 0x11, 0x22), - edges: EdgeBlock { - in_family: [0xC3; 12], - out_family: [0xD4; 4], - }, + edges: EdgeBlock::from_bytes(&[ + 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xD4, + 0xD4, 0xD4, 0xD4, + ]), value: [9u8; 480], }, ]; @@ -2070,10 +2086,9 @@ mod tests { for (i, row) in rows.iter().enumerate() { let start = i * NODE_ROW_STRIDE + offset; let slice = &bytes[start..start + elems]; - // 16 bytes/row, straight out of the store: 12 in-family then - // 4 out-of-family (repr(C) field order within the block). - assert_eq!(&slice[..12], &row.edges.in_family); - assert_eq!(&slice[12..], &row.edges.out_family); + // 16 bytes/row, straight out of the store: the second facet's + // own bytes, in order. + assert_eq!(slice, row.edges.as_bytes()); } } @@ -2133,12 +2148,24 @@ mod tests { assert_eq!(&g.as_bytes()[10..16], &[0xAB, 0x00, 0x00, 0xCD, 0x00, 0x00]); } + /// The second facet IS a facet: same type, same 16 bytes, same alignment + /// as the key. A `12 + 4` carving appears nowhere here — that reading is + /// retired, not relocated. #[test] - fn edge_block_is_twelve_plus_four() { + fn edge_block_is_the_same_facet_type_as_the_key() { let e = EdgeBlock::default(); - assert_eq!(e.in_family.len(), 12); - assert_eq!(e.out_family.len(), 4); assert_eq!(core::mem::size_of_val(&e), 16); + assert_eq!( + core::mem::align_of::(), + core::mem::align_of::() + ); + let f: crate::facet::FacetCascade = e; // an alias, not a conversion + assert_eq!(f.as_bytes(), &[0u8; 16]); + let mut g = EdgeBlock::from_bytes(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]); + g.as_bytes_mut()[12] = 0; + assert_eq!(g.as_bytes()[11], 12); + assert_eq!(g.as_bytes()[12], 0); + assert_eq!(g.as_bytes()[13], 14); } #[test] diff --git a/crates/lance-graph-contract/src/facet.rs b/crates/lance-graph-contract/src/facet.rs index e4a62c631..6a0af6179 100644 --- a/crates/lance-graph-contract/src/facet.rs +++ b/crates/lance-graph-contract/src/facet.rs @@ -99,6 +99,24 @@ pub struct FacetCascade { pub tiers: [FacetTier; 6], } +// The facet is a STORED row field (`NodeRow::edges`), and `as_bytes` / +// `ref_from_bytes` are pure pointer reinterprets — so the struct's in-memory +// image IS the canonical LE row image. `facet_classid` is a native-endian +// `u32`, which makes that identity hold on little-endian targets ONLY: on a +// big-endian target `from_bytes` (explicitly `u32::from_le_bytes`) and +// `as_bytes` (a reinterpret) would disagree on bytes `[0..4)`, silently +// byte-swapping a non-zero class id through serialization. The predecessor +// type at this row offset (`EdgeBlock { in_family: [u8; 12], out_family: +// [u8; 4] }`) was byte-backed and so endian-independent; aliasing it to this +// typed facet is what introduced the dependency. Fail LOUD at compile time +// rather than corrupt a row image at runtime — the round trip is pinned by +// `le_byte_image_round_trips_with_a_non_zero_classid` below. +// (codex P2 on PR #1246; `ISS-EDGE-BLOCK-WAS-A-SECOND-TYPE-FOR-THE-SAME-FACET`.) +const _: () = assert!( + cfg!(target_endian = "little"), + "FacetCascade's reinterpret-based LE byte image assumes a little-endian target" +); + const _: () = assert!(core::mem::size_of::() == 2, "one 8:8 tile"); const _: () = assert!( core::mem::size_of::() == 16, @@ -174,6 +192,18 @@ impl FacetCascade { unsafe { &*(self as *const Self).cast::<[u8; 16]>() } } + /// Mutable twin of [`as_bytes`](Self::as_bytes): write the facet's own 16 + /// backing bytes in place. Every byte pattern is a valid facet (it is + /// content-blind by construction), so no invariant can be broken through + /// this view. + #[inline] + #[must_use] + pub fn as_bytes_mut(&mut self) -> &mut [u8; 16] { + // SAFETY: as for `as_bytes`; `&mut self` guarantees exclusivity, and + // all bit patterns of [u8; 16] are valid `FacetCascade` values. + unsafe { &mut *(self as *mut Self).cast::<[u8; 16]>() } + } + /// **Zero-copy borrow** of 16 slab bytes AS a facet — the literal no-op decode: the /// compiler reads fields/lanes straight from the slab, nothing materializes. Returns /// `None` if `b` is not 16-byte aligned (then copy via [`from_bytes`](Self::from_bytes)). @@ -662,6 +692,44 @@ mod tests { ); } + /// The **reinterpret** view and the **explicit LE codec** must agree byte for + /// byte on a non-zero `facet_classid`. + /// + /// These are two different mechanisms, and only one of them was pinned before: + /// `to_bytes` encodes with `u32::to_le_bytes`, while `as_bytes` (and + /// `ref_from_bytes`) reinterpret the struct's own memory — which is what + /// `NodeRowPacket::as_le_bytes` and `row_bytes` serialize, now that a facet is + /// a STORED row field (`NodeRow::edges`). A zero class id byte-swaps to itself + /// and would hide the divergence, so the fixture's `0xDEAD_BEEF` is doing the + /// work here. The compile-time `target_endian` guard above is what keeps this + /// property from being merely asserted on the one target that satisfies it. + /// (codex P2 on PR #1246.) + #[test] + fn le_byte_image_round_trips_with_a_non_zero_classid() { + let b = sample(); + let f = FacetCascade::from_bytes(&b); + assert_ne!(f.facet_classid, 0, "a zero class id cannot detect a swap"); + assert_ne!( + f.facet_classid.swap_bytes(), + f.facet_classid, + "the fixture class id must be byte-order sensitive" + ); + + assert_eq!( + f.as_bytes(), + &f.to_bytes(), + "the reinterpret view must equal the explicit LE encoding" + ); + assert_eq!(f.as_bytes(), &b, "and both must equal the source bytes"); + + // The borrowed no-op decode reads the same image. + let aligned = FacetCascade::from_bytes(&b); + assert_eq!( + FacetCascade::ref_from_bytes(aligned.as_bytes()), + Some(&aligned) + ); + } + #[test] fn redout_is_granularity_free_and_orthogonal() { let f = FacetCascade::from_bytes(&sample()); diff --git a/crates/lance-graph-contract/src/soa_graph.rs b/crates/lance-graph-contract/src/soa_graph.rs index 563629019..e4035869d 100644 --- a/crates/lance-graph-contract/src/soa_graph.rs +++ b/crates/lance-graph-contract/src/soa_graph.rs @@ -323,7 +323,7 @@ pub fn project_snapshot(rows: &[NodeRow], domain: &DomainSpec) -> GraphSnapshot }); // 16 family-node adapters: 12 in-family + 4 out-of-family, each → a family. let eb = row.edges; - for &b in eb.in_family.iter().filter(|&&b| b != 0) { + for &b in eb.as_bytes()[..12].iter().filter(|&&b| b != 0) { if let Some(target) = resolve(b, fam) { edges.push(RenderEdge { source: g.to_string(), @@ -335,7 +335,7 @@ pub fn project_snapshot(rows: &[NodeRow], domain: &DomainSpec) -> GraphSnapshot }); } } - for &b in eb.out_family.iter().filter(|&&b| b != 0) { + for &b in eb.as_bytes()[12..].iter().filter(|&&b| b != 0) { if let Some(target) = resolve(b, fam) { edges.push(RenderEdge { source: g.to_string(), @@ -439,10 +439,10 @@ mod tests { use crate::canonical_node::classid_read_mode; let mut edges = EdgeBlock::default(); for (i, &b) in in_fam.iter().enumerate().take(12) { - edges.in_family[i] = b; + edges.as_bytes_mut()[i] = b; } for (i, &b) in out_fam.iter().enumerate().take(4) { - edges.out_family[i] = b; + edges.as_bytes_mut()[12 + i] = b; } NodeRow { // Route through `mint_for` so the domain classid's `tail_variant` diff --git a/crates/lance-graph-mask-risc/src/exec.rs b/crates/lance-graph-mask-risc/src/exec.rs index 70b7ea40a..374c63b4e 100644 --- a/crates/lance-graph-mask-risc/src/exec.rs +++ b/crates/lance-graph-mask-risc/src/exec.rs @@ -27,10 +27,10 @@ use ndarray::simd::{ ge_i32_to_mask, ge_i32_to_mask_under, gt_i32_to_mask, gt_i32_to_mask_under, le_i32_to_mask, le_i32_to_mask_under, lt_i32_to_mask, lt_i32_to_mask_under, mask_all, mask_and, mask_and_assign, mask_andnot, mask_andnot_assign, mask_any, mask_not, mask_not_assign, mask_or, - mask_or_assign, mask_xor, mask_xor_assign, masked_max_i32, masked_min_i32, masked_sum_i32, - ne_i32_to_mask, ne_i32_to_mask_under, ne_u32_to_mask, ne_u32_to_mask_under, popcount_batch_u64, - ternary_match_u32_to_mask, ternary_match_u32_to_mask_under, ternary_match_u64_to_mask, - ternary_match_u64_to_mask_under, + mask_or_assign, mask_set_range, mask_xor, mask_xor_assign, masked_max_i32, masked_min_i32, + masked_sum_i32, ne_i32_to_mask, ne_i32_to_mask_under, ne_u32_to_mask, ne_u32_to_mask_under, + popcount_batch_u64, ternary_match_u32_to_mask, ternary_match_u32_to_mask_under, + ternary_match_u64_to_mask, ternary_match_u64_to_mask_under, }; use crate::ir::{LaneRef, MaskOp, Operand, Planes, Pred, Program, Terminal, MAX_SCRATCH_SLOTS}; @@ -535,6 +535,13 @@ fn run_pred<'a>( read(planes, s, u), dst, ), + // `lo <= hi <= n_rows` was validated, so the bounds are in-range for + // the scratch words and `mask_set_range`'s own asserts cannot fire. + (Pred::Range { lo, hi }, None) => mask_set_range(dst, lo as usize, hi as usize), + (Pred::Range { lo, hi }, Some(u)) => { + mask_set_range(dst, lo as usize, hi as usize); + mask_and_assign(dst, read(planes, s, u)); + } } } diff --git a/crates/lance-graph-mask-risc/src/ir.rs b/crates/lance-graph-mask-risc/src/ir.rs index 6f362f419..9fd4b0a14 100644 --- a/crates/lance-graph-mask-risc/src/ir.rs +++ b/crates/lance-graph-mask-risc/src/ir.rs @@ -80,6 +80,28 @@ pub enum Pred { MatchU32 { lane: u16, pattern: u32, care: u32 }, /// `((lane[i] ^ pattern) & care) == 0` over a `u64` lane. MatchU64 { lane: u16, pattern: u64, care: u64 }, + /// `lo <= i < hi` — a predicate on the ROW INDEX, reading no lane. + /// + /// The contiguous-range write. On an address-ordered lane an address + /// prefix names a contiguous subtree, so the prefix's mask is a range, + /// not a sweep: `ndarray::simd::mask_set_range` fills it in three passes + /// over the words (zero before, ones inside, zero after, two computed + /// edge words) with no per-row compare at all. This is the op + /// `lance-graph-quack`'s `Filter::prefix_u64` doc named as *"NOT yet a + /// range WRITE … waits on the primitive"* — the primitive is in ndarray; + /// this is the IR name for it (`ISS-MASK-RISC-HAD-NO-RANGE-OP`). + /// + /// The IR does NOT decide whether a lane is address-ordered — that is the + /// planner's knowledge (a V3 table's row address is its rail). A caller + /// that lowers a prefix to `Range` on an unordered lane gets a wrong + /// answer, not an error; the oracle agrees with the executor on the range + /// itself, which is all either can check. + /// + /// Validated: `lo <= hi` and `hi <= n_rows` + /// ([`crate::ExecError::RangeOutOfBounds`]), so the write can never reach + /// the tail. `lo == hi` is a legal empty range (an all-zero write, not a + /// no-op). With `under`, the result is `range & gate`. + Range { lo: u32, hi: u32 }, } /// One instruction. Destinations are always [`Operand::Scratch`]; input planes @@ -278,6 +300,30 @@ impl Program { let mut h = OpHistogram::default(); for op in &self.ops { match op { + // `Pred::Range` reads NO value lane (it is a row-index + // predicate), so it is not a value-lane predicate pass; it + // spends one linear write over the destination mask instead. + // + // A GATED range spends a SECOND pass, and that is not true of + // any other predicate: every lane predicate has a fused + // `*_to_mask_under` kernel, so gating it costs nothing extra. + // There is no `mask_set_range_under`, so `exec` runs the gated + // range as `mask_set_range` followed by `mask_and_assign` — + // literally an `and`, which is what `two_input` already counts. + // Charged there rather than to a new field, so the asymmetry is + // visible in the histogram instead of hidden behind a name. + // (CodeRabbit, PR #1246. Closing this would need the fused + // primitive upstream, tracked as `mask_set_range_under`.) + MaskOp::Pred { + pred: Pred::Range { .. }, + under, + .. + } => { + h.ranges += 1; + if under.is_some() { + h.two_input += 1; + } + } MaskOp::Pred { .. } => h.predicates += 1, MaskOp::And { .. } | MaskOp::Or { .. } @@ -302,12 +348,24 @@ pub struct OpHistogram { pub not: usize, /// Three-input passes. pub ternlog: usize, + /// Row-index range writes (`Pred::Range`) — counted apart from + /// [`predicates`](Self::predicates) because they read no value lane. + /// Each is ONE pass over the destination mask: `mask_set_range` writes + /// every word of `out_words` exactly once across disjoint segments (the + /// two zero-fills below/above the range, then the head/body/tail of the + /// range itself), never re-walking the whole mask per segment. + /// + /// A range under a gate spends a second pass, counted in + /// [`two_input`](Self::two_input) because it IS one: `exec` follows the + /// write with `mask_and_assign`. Unlike every lane predicate, which has a + /// fused `*_to_mask_under` kernel, no `mask_set_range_under` exists. + pub ranges: usize, } impl OpHistogram { /// Total mask-word passes the program spends after its predicates. pub fn mask_passes(&self) -> usize { - self.two_input + self.not + self.ternlog + self.two_input + self.not + self.ternlog + self.ranges } } @@ -404,6 +462,84 @@ mod tests { /// predicate as a mask pass (predicates sweep VALUE lanes and are /// reported separately). Fixture: one of each kind, so every counter is /// exactly 1 and the pass total is 4. + /// `Pred::Range` is a row-index predicate: it reads no value lane, so it + /// must NOT land in `predicates`, and its destination write must be + /// counted in `mask_passes()`. Before this split a range-only program + /// reported one predicate and ZERO mask passes, which is the one shape a + /// cost model would read as free. (CodeRabbit, PR #1246.) + #[test] + fn range_is_counted_apart_from_value_lane_predicates() { + let range_only = Program::new( + vec![MaskOp::Pred { + pred: Pred::Range { lo: 3, hi: 9 }, + under: None, + dst: 0, + }], + Terminal::Count { + mask: Operand::Scratch(0), + }, + ); + let h = range_only.op_histogram(); + assert_eq!(h.ranges, 1, "the range must be counted"); + assert_eq!(h.predicates, 0, "a range reads no value lane"); + assert_eq!(h.mask_passes(), 1, "and its write is not free"); + + // A lane-reading predicate still counts as one, and not as a range. + let lane_only = Program::new( + vec![MaskOp::Pred { + pred: Pred::GtI32 { lane: 0, t: 3 }, + under: None, + dst: 0, + }], + Terminal::Count { + mask: Operand::Scratch(0), + }, + ); + let h2 = lane_only.op_histogram(); + assert_eq!((h2.predicates, h2.ranges), (1, 0)); + assert_eq!(h2.mask_passes(), 0, "a bare lane predicate spends none"); + } + + /// A GATED range costs two passes, and a gated LANE predicate costs none: + /// every lane predicate has a fused `*_to_mask_under` kernel, while the + /// range has no `mask_set_range_under`, so `exec` runs write-then-`and`. + /// Both halves matter — asserting only the range would not show that the + /// extra pass is specific to it. (CodeRabbit, PR #1246.) + #[test] + fn a_gated_range_costs_the_intersection_but_a_gated_lane_predicate_does_not() { + let gated_range = Program::new( + vec![MaskOp::Pred { + pred: Pred::Range { lo: 3, hi: 9 }, + under: Some(Operand::Plane(0)), + dst: 0, + }], + Terminal::Count { + mask: Operand::Scratch(0), + }, + ); + let h = gated_range.op_histogram(); + assert_eq!((h.ranges, h.two_input), (1, 1), "write + intersection"); + assert_eq!(h.mask_passes(), 2, "a gated range spends BOTH"); + + let gated_lane = Program::new( + vec![MaskOp::Pred { + pred: Pred::GtI32 { lane: 0, t: 3 }, + under: Some(Operand::Plane(0)), + dst: 0, + }], + Terminal::Count { + mask: Operand::Scratch(0), + }, + ); + let h2 = gated_lane.op_histogram(); + assert_eq!( + (h2.predicates, h2.two_input), + (1, 0), + "the fused `*_to_mask_under` kernel adds no pass" + ); + assert_eq!(h2.mask_passes(), 0, "so gating a lane predicate is free"); + } + #[test] fn op_histogram_counts_each_physical_kind_once() { let p = Program::new( @@ -441,7 +577,8 @@ mod tests { predicates: 1, two_input: 1, not: 1, - ternlog: 1 + ternlog: 1, + ranges: 0, } ); assert_eq!(h.mask_passes(), 3); diff --git a/crates/lance-graph-mask-risc/src/reference.rs b/crates/lance-graph-mask-risc/src/reference.rs index b464a6e5a..997c4ef31 100644 --- a/crates/lance-graph-mask-risc/src/reference.rs +++ b/crates/lance-graph-mask-risc/src/reference.rs @@ -31,14 +31,16 @@ fn kind_of(lane: &LaneRef<'_>) -> LaneKind { } } -/// The lane a predicate reads, and the width that lane must have. +/// The lane a predicate reads, and the width that lane must have — `None` +/// for a predicate that reads no lane at all ([`Pred::Range`] is on the row +/// index). /// /// One place where `Pred`'s variants map to lane widths, so the executor and /// the oracle cannot disagree about which column a predicate touches. Adding a /// `Pred` variant without extending this match is a compile error, which is /// the point of listing the variants explicitly rather than matching a field. -fn pred_lane_and_kind(pred: Pred) -> (u16, LaneKind) { - match pred { +fn pred_lane_and_kind(pred: Pred) -> Option<(u16, LaneKind)> { + Some(match pred { Pred::GtI32 { lane, .. } | Pred::LtI32 { lane, .. } | Pred::GeI32 { lane, .. } @@ -49,7 +51,8 @@ fn pred_lane_and_kind(pred: Pred) -> (u16, LaneKind) { (lane, LaneKind::U32) } Pred::MatchU64 { lane, .. } => (lane, LaneKind::U64), - } + Pred::Range { .. } => return None, + }) } /// One operand's index, against the two address spaces it could name. @@ -282,8 +285,19 @@ pub(crate) fn validate( } written(u)?; } - let (lane, kind) = pred_lane_and_kind(pred); - check_lane(planes, lane, kind)?; + if let Some((lane, kind)) = pred_lane_and_kind(pred) { + check_lane(planes, lane, kind)?; + } + if let Pred::Range { lo, hi } = pred { + let hi_fits = usize::try_from(hi).is_ok_and(|h| h <= planes.n_rows); + if lo > hi || !hi_fits { + return Err(ExecError::RangeOutOfBounds { + lo, + hi, + n_rows: planes.n_rows, + }); + } + } check_operand(p, planes, Operand::Scratch(dst))?; } MaskOp::And { a, b, dst } @@ -409,6 +423,8 @@ fn eval_pred(planes: &Planes<'_>, pred: Pred, row: usize) -> bool { pattern, care, } => (u64_at(lane) ^ pattern) & care == 0, + // `hi <= n_rows` and `lo <= hi` were validated, so both fit a usize. + Pred::Range { lo, hi } => (lo as usize..hi as usize).contains(&row), } } diff --git a/crates/lance-graph-mask-risc/src/value.rs b/crates/lance-graph-mask-risc/src/value.rs index 2133aaf52..def20dab8 100644 --- a/crates/lance-graph-mask-risc/src/value.rs +++ b/crates/lance-graph-mask-risc/src/value.rs @@ -103,4 +103,9 @@ pub enum ExecError { /// A gated predicate whose gate IS its destination: the facade cannot /// read the gate while overwriting it, so the program is refused. GateAliasesDst { dst: u16 }, + /// [`crate::Pred::Range`] with `lo > hi` or `hi > n_rows`. Refused by the + /// validator both paths share, so the executor never hands + /// `mask_set_range` a bound past the scratch words (its own assert would + /// panic) and the oracle never indexes a row that does not exist. + RangeOutOfBounds { lo: u32, hi: u32, n_rows: usize }, } diff --git a/crates/lance-graph-mask-risc/tests/differential.rs b/crates/lance-graph-mask-risc/tests/differential.rs index da47df739..e3a96ef8e 100644 --- a/crates/lance-graph-mask-risc/tests/differential.rs +++ b/crates/lance-graph-mask-risc/tests/differential.rs @@ -408,3 +408,176 @@ fn every_terminal() { } } } + +// ───────────────────────────────────────────────────────────────────────────── +// `Pred::Range` — the contiguous-range write (`ISS-MASK-RISC-HAD-NO-RANGE-OP`). +// ───────────────────────────────────────────────────────────────────────────── + +/// Every `(lo, hi)` the word geometry can make interesting at `n` rows: empty, +/// single-word, word-straddling, edge-aligned at 63/64/65, and the full table. +fn range_cases(n: usize) -> Vec<(u32, u32)> { + let n32 = n as u32; + let mut v = vec![(0, 0), (0, n32), (n32, n32)]; + for &(lo, hi) in &[ + (0, 1), + (1, 2), + (62, 63), + (63, 64), + (63, 65), + (64, 65), + (60, 70), + (1, 127), + (64, 128), + (65, 129), + (100, 900), + (7, 65_000), + ] { + if hi <= n32 { + v.push((lo, hi)); + } + } + v +} + +/// FAILS IF: the executor's `mask_set_range` path and the oracle's row-index +/// predicate disagree on any range at any row count — ungated, under a plane, +/// or under a scratch gate — on values OR scratch words. +#[test] +fn range_matches_the_oracle_at_every_word_edge() { + for n in ROWS { + let f = Fixture::new(n, 23); + for (lo, hi) in range_cases(n) { + let pred = Pred::Range { lo, hi }; + for under in [None, Some(P0), Some(S1)] { + let mut ops = Vec::new(); + if under == Some(S1) { + ops.push(MaskOp::Not { a: P1, dst: 1 }); + } + ops.push(MaskOp::Pred { + pred, + under, + dst: 0, + }); + let p = Program::new(ops, Terminal::Keep { mask: S0 }); + f.run(&p, &format!("{pred:?} under {under:?}")); + } + } + } +} + +/// Count through the EXECUTOR. `Fixture::count` goes through the oracle, which +/// is the right side for fixture-validity guards but the wrong side for a +/// population check on the `mask_set_range` path: a disable-run (an +/// off-by-one in `exec.rs`'s `Range` arm) passed the oracle-counted version of +/// this test, which is exactly the vacuity it now cannot have. +fn exec_count(f: &Fixture, p: &Program) -> usize { + let masks: Vec<&[u64]> = f.masks.iter().map(|m| m.as_slice()).collect(); + let lanes = [LaneRef::I32(&f.i32s)]; + let planes = Planes { + n_rows: f.n, + masks: &masks, + lanes: &lanes, + }; + let mut scratch = Scratch::for_program(p, f.n).expect("addressable"); + match execute(p, &planes, &mut scratch, None) { + Ok(Value::Count(c)) => c, + other => panic!("expected a count, got {other:?}"), + } +} + +/// FAILS IF: the executor's range population is not EXACTLY `hi - lo` — the +/// can-it-fire half (a non-empty range selects something) and the +/// can-it-stay-silent half (an empty range selects nothing, and it selects +/// nothing OUTSIDE its bounds) in one equality, `==` not `>=`. +#[test] +fn range_population_is_exactly_hi_minus_lo() { + for n in ROWS { + let f = Fixture::new(n, 29); + for (lo, hi) in range_cases(n) { + let c = Program::new( + vec![MaskOp::Pred { + pred: Pred::Range { lo, hi }, + under: None, + dst: 0, + }], + Terminal::Count { mask: S0 }, + ); + assert_eq!( + exec_count(&f, &c), + (hi - lo) as usize, + "Range {{ {lo}, {hi} }} @ n={n}" + ); + } + // Silent half with a non-trivial input: a range gated by its own + // complement selects nothing, and this is only informative when the + // range is non-empty. + if n >= 2 { + let (lo, hi) = (0u32, (n / 2) as u32); + let disjoint = Program::new( + vec![ + MaskOp::Pred { + pred: Pred::Range { lo, hi }, + under: None, + dst: 1, + }, + MaskOp::Not { a: S1, dst: 2 }, + MaskOp::Pred { + pred: Pred::Range { lo, hi }, + under: Some(Operand::Scratch(2)), + dst: 0, + }, + ], + Terminal::Count { mask: S0 }, + ); + assert_eq!( + exec_count(&f, &disjoint), + 0, + "range under its complement @ n={n}" + ); + } + } +} + +/// FAILS IF: an out-of-bounds range is accepted by either path, or the two +/// paths refuse it with different errors. `lo > hi` and `hi > n_rows` are the +/// two ways to be out of bounds; both must be refused BEFORE any write. +#[test] +fn range_out_of_bounds_is_refused_identically() { + use lance_graph_mask_risc::ExecError; + for n in ROWS { + let f = Fixture::new(n, 31); + let n32 = n as u32; + for (lo, hi) in [(n32 + 1, n32 + 1), (0, n32 + 1), (3, 2), (n32, n32 + 5)] { + if lo <= hi && hi <= n32 { + continue; // not actually out of bounds at this n + } + let p = Program::new( + vec![MaskOp::Pred { + pred: Pred::Range { lo, hi }, + under: None, + dst: 0, + }], + Terminal::Count { mask: S0 }, + ); + let masks: Vec<&[u64]> = f.masks.iter().map(|m| m.as_slice()).collect(); + let lanes = [LaneRef::I32(&f.i32s)]; + let planes = Planes { + n_rows: n, + masks: &masks, + lanes: &lanes, + }; + let mut scratch = Scratch::for_program(&p, n).expect("addressable"); + let got = execute(&p, &planes, &mut scratch, None); + let want = reference_execute(&p, &planes, None); + assert_eq!( + got, want, + "Range {{ {lo}, {hi} }} @ n={n}: refusal must match" + ); + assert_eq!( + got, + Err(ExecError::RangeOutOfBounds { lo, hi, n_rows: n }), + "Range {{ {lo}, {hi} }} @ n={n}: must be refused as out of bounds" + ); + } + } +} diff --git a/crates/lance-graph-quack/src/lib.rs b/crates/lance-graph-quack/src/lib.rs index 320866766..78c4d6cbf 100644 --- a/crates/lance-graph-quack/src/lib.rs +++ b/crates/lance-graph-quack/src/lib.rs @@ -462,9 +462,23 @@ impl Filter { /// /// A ternary match — a full sweep of the lane, one pass, no allocation. It /// is NOT yet a range WRITE. The range write is `mask_set_range`, the - /// matrix's T1 gap G6, and it is absent from `ndarray::simd`; per the + /// matrix's T1 gap G6. ~~and it is absent from `ndarray::simd`; per the /// missing-capability STOP rule a consumer does not hand-roll it one layer - /// up, so this crate spells the PREDICATE and waits for the primitive. + /// up, so this crate spells the PREDICATE and waits for the primitive.~~ + /// ⊘ STRUCK — that clause is false as of the note below, which says the + /// opposite; it is kept struck rather than deleted so the reason the + /// lowering was once blocked stays legible. The STOP rule still applies; + /// what changed is that the capability is no longer missing. + /// + /// ⊘ 2026-09-17 (`ISS-MASK-RISC-HAD-NO-RANGE-OP`): both halves of that + /// paragraph have moved. `mask_set_range` IS on `ndarray::simd` + /// (`simd_masking_ops.rs`), and mask-risc now names it — + /// `Pred::Range { lo, hi }`, validated `lo <= hi <= n_rows`, gate-aware. + /// What this crate still lacks is the one thing the IR deliberately does + /// not carry: whether the lane is ADDRESS-ORDERED. A prefix is a range + /// only on an ordered lane, and `Filter` has no ordering evidence to + /// lower on, so this still lowers to the sweep. The lowering waits on + /// the planner's ordering knowledge now, not on any primitive. /// /// That the primitive is real rather than wished for is the harvest's /// evidence, not this crate's opinion: DuckDB's own bit-plane carries diff --git a/crates/lance-graph/Cargo.toml b/crates/lance-graph/Cargo.toml index 3b6e1571d..79f4b997d 100644 --- a/crates/lance-graph/Cargo.toml +++ b/crates/lance-graph/Cargo.toml @@ -50,7 +50,7 @@ lance-graph-catalog = { path = "../lance-graph-catalog", version = "0.5.4" } lance-graph-contract = { path = "../lance-graph-contract" } lance = { workspace = true } lance-linalg = { workspace = true } -lance-namespace = "=11.0.0" +lance-namespace = "11.*" lancedb = { workspace = true, optional = true } nom = "7.1" serde = { version = "1", features = ["derive"] } @@ -142,7 +142,7 @@ lite-unified = [] deepnsm = { path = "../deepnsm" } lance-graph-contract = { path = "../lance-graph-contract" } futures = "0.3" -lance-arrow = "=11.0.0" +lance-arrow = "11.*" lance-index = { workspace = true } tempfile = "3" # D-MW-P2 probe only: `ShardWriterConfig::shard_id` is a `Uuid`, so the probe diff --git a/crates/lance-graph/src/graph/mailbox_scan.rs b/crates/lance-graph/src/graph/mailbox_scan.rs index 9fc40d61b..a984f55e4 100644 --- a/crates/lance-graph/src/graph/mailbox_scan.rs +++ b/crates/lance-graph/src/graph/mailbox_scan.rs @@ -197,14 +197,12 @@ pub fn edge_slots_coarse( } let block = view.edge_block_at(row)?; Some(EdgeNeighbors { - in_family: block - .in_family + in_family: block.as_bytes()[..12] .iter() .copied() .filter(|&b| b != 0) .collect(), - external: block - .out_family + external: block.as_bytes()[12..] .iter() .copied() .filter(|&b| b != 0) @@ -469,10 +467,9 @@ mod tests { ], // row0 has in-family edges to refs 2,5 and one external ref 1; rest empty. blocks: vec![ - Some(EdgeBlock { - in_family: [2, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - out_family: [1, 0, 0, 0], - }), + Some(EdgeBlock::from_bytes(&[ + 2, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + ])), Some(EdgeBlock::default()), None, None, diff --git a/crates/symbiont/src/domino.rs b/crates/symbiont/src/domino.rs index 0e0333a02..710b1996a 100644 --- a/crates/symbiont/src/domino.rs +++ b/crates/symbiont/src/domino.rs @@ -93,8 +93,8 @@ pub fn energy_of(row: &NodeRow) -> f32 { /// tenants), so seeding edges here is free for the AMX path. fn seed_board(idx: usize) -> NodeRow { let mut edges = EdgeBlock::default(); - edges.in_family[0] = ((idx % 255) + 1) as u8; // ring neighbour (always 1..=255) - edges.out_family[0] = (1 + (idx % 4)) as u8; // inherited-adapter slot (1..=4) + edges.as_bytes_mut()[0] = ((idx % 255) + 1) as u8; // ring neighbour (always 1..=255) + edges.as_bytes_mut()[12] = (1 + (idx % 4)) as u8; // byte 12 (the V1 "adapter slot 0" position) let mut row = NodeRow { key: NodeGuid::local(idx as u32), edges, @@ -155,7 +155,11 @@ fn domino_batch(boards: &mut [NodeRow], w: &[u16; K * BATCH], stages: usize) { /// The POC: build `n_boards` SoA boards, run a `stages`-deep Domino sweep in AMX /// 16-board batches, project onto the NaN-detection surface, report. pub fn run_poc(n_boards: usize, stages: usize) { - assert_eq!(n_boards % BATCH, 0, "n_boards must be a multiple of {BATCH}"); + assert_eq!( + n_boards % BATCH, + 0, + "n_boards must be a multiple of {BATCH}" + ); let w = weight(); let mut rows: Vec = (0..n_boards).map(seed_board).collect(); @@ -173,7 +177,10 @@ pub fn run_poc(n_boards: usize, stages: usize) { report.nonfinite ); } - assert!(report.is_clean(), "Domino sweep produced a non-finite board"); + assert!( + report.is_clean(), + "Domino sweep produced a non-finite board" + ); let path = if amx_available() { "AMX TDPBF16PS" diff --git a/crates/symbiont/src/kanban_loop.rs b/crates/symbiont/src/kanban_loop.rs index de6c579f0..2b9307e8d 100644 --- a/crates/symbiont/src/kanban_loop.rs +++ b/crates/symbiont/src/kanban_loop.rs @@ -254,7 +254,10 @@ mod tests { let mut board = SymbiontBoard::spawn(4096, 13); let trail = board.run_to_absorbing(&NextPhaseScheduler); - assert!(board.phase().is_absorbing(), "must reach the absorbing Commit"); + assert!( + board.phase().is_absorbing(), + "must reach the absorbing Commit" + ); assert!(!trail.is_empty(), "the arc must have advanced"); let energy = board.energy(); @@ -263,10 +266,19 @@ mod tests { let inf = energy.iter().filter(|e| e.is_infinite()).count(); let nonzero = energy.iter().filter(|&&e| e != 0.0).count(); - assert_eq!(nan, 0, "live-cycle NaN census: {nan}/{total} NaN over the arc"); - assert_eq!(inf, 0, "live-cycle Inf census: {inf}/{total} Inf over the arc"); + assert_eq!( + nan, 0, + "live-cycle NaN census: {nan}/{total} NaN over the arc" + ); + assert_eq!( + inf, 0, + "live-cycle Inf census: {inf}/{total} Inf over the arc" + ); // The sweep must have produced real, finite energy (not a no-op all-zeros). - assert!(nonzero > 0, "the BF16 Domino sweep produced no finite energy"); + assert!( + nonzero > 0, + "the BF16 Domino sweep produced no finite energy" + ); } #[test] @@ -286,8 +298,8 @@ mod tests { let eb = board .edge_block_at(3) .expect("owner materialises the edge block"); - assert_eq!(eb.in_family[0], 4); // ring edge (3 % 255) + 1 - assert_eq!(eb.out_family[0], 4); // adapter slot 1 + (3 % 4) + assert_eq!(eb.as_bytes()[0], 4); // ring edge (3 % 255) + 1 + assert_eq!(eb.as_bytes()[12], 4); // byte 12: 1 + (3 % 4) assert!( board.hhtl_path_at(3).is_some(), "owner materialises the HHTL path" diff --git a/crates/symbiont/src/key_render.rs b/crates/symbiont/src/key_render.rs index 7f05b7659..611be5f75 100644 --- a/crates/symbiont/src/key_render.rs +++ b/crates/symbiont/src/key_render.rs @@ -68,15 +68,13 @@ pub fn render_key_only(rows: &[NodeRow]) -> KeyGraph { // ── the ONLY two field reads: the 32-byte head ── let guid = row.key; let eb = row.edges; - let in_family: Vec<(u8, u8)> = eb - .in_family + let in_family: Vec<(u8, u8)> = eb.as_bytes()[..12] .iter() .enumerate() .filter(|(_, &b)| b != 0) .map(|(i, &b)| (i as u8, b)) .collect(); - let out_family: Vec<(u8, u8)> = eb - .out_family + let out_family: Vec<(u8, u8)> = eb.as_bytes()[12..] .iter() .enumerate() .filter(|(_, &b)| b != 0) diff --git a/crates/weather-poc/src/bake.rs b/crates/weather-poc/src/bake.rs index d8a9f57d2..a34a48a34 100644 --- a/crates/weather-poc/src/bake.rs +++ b/crates/weather-poc/src/bake.rs @@ -166,13 +166,9 @@ where let key = encode_key(classid, lat_idx, lon_idx); let mut facets = [[0u8; FACET_LEN]; W1_FACET_COUNT]; for (facet_idx, facet_bytes) in facets.iter_mut().enumerate() { - *facet_bytes = pack_facet( - classid, - facet_idx as u8, - manifest, - floors, - |entry| value_of(entry), - )?; + *facet_bytes = pack_facet(classid, facet_idx as u8, manifest, floors, |entry| { + value_of(entry) + })?; } Ok(PackedWeatherCell { @@ -213,14 +209,9 @@ where let mut written = 0usize; for lat_idx in 0..LAT_COUNT { for lon_idx in 0..LON_COUNT { - let cell = pack_cell( - classid, - lat_idx, - lon_idx, - manifest, - floors, - |entry| value_of(lat_idx, lon_idx, entry), - ) + let cell = pack_cell(classid, lat_idx, lon_idx, manifest, floors, |entry| { + value_of(lat_idx, lon_idx, entry) + }) .map_err(BakeStreamError::Bake)?; sink(cell).map_err(BakeStreamError::Sink)?; written += 1; @@ -330,24 +321,10 @@ mod tests { #[test] fn manifest_mutation_is_load_bearing_end_to_end() { - let a = pack_cell( - WEATHER_W1_CLASSID, - 10, - 20, - &manifest("a"), - &floors(), - value, - ) - .expect("a packs"); - let b = pack_cell( - WEATHER_W1_CLASSID, - 10, - 20, - &manifest("b"), - &floors(), - value, - ) - .expect("b packs"); + let a = pack_cell(WEATHER_W1_CLASSID, 10, 20, &manifest("a"), &floors(), value) + .expect("a packs"); + let b = pack_cell(WEATHER_W1_CLASSID, 10, 20, &manifest("b"), &floors(), value) + .expect("b packs"); assert_ne!( a.facet_image(), diff --git a/crates/weather-poc/src/canonical.rs b/crates/weather-poc/src/canonical.rs index fd0106593..8d7494a1d 100644 --- a/crates/weather-poc/src/canonical.rs +++ b/crates/weather-poc/src/canonical.rs @@ -166,8 +166,7 @@ pub fn assemble_row(cell: &PackedWeatherCell) -> Result [u8; NODE_ROW_STRIDE] { let mut out = [0u8; NODE_ROW_STRIDE]; out[0..16].copy_from_slice(row.key.as_bytes()); - out[16..28].copy_from_slice(&row.edges.in_family); - out[28..32].copy_from_slice(&row.edges.out_family); + out[16..32].copy_from_slice(row.edges.as_bytes()); out[VALUE_SLAB_ROW_OFFSET..].copy_from_slice(&row.value); out } @@ -202,9 +201,7 @@ where |lat, lon, entry| value_of(lat, lon, entry), |cell| { let row = assemble_row(&cell).map_err(SinkError::Canonical)?; - writer - .write_all(&row_bytes(&row)) - .map_err(SinkError::Io) + writer.write_all(&row_bytes(&row)).map_err(SinkError::Io) }, ); @@ -239,24 +236,26 @@ mod tests { fn fixture_floors() -> HashMap { let sample: Vec = (0..10_000).map(|i| i as f64 / 100.0).collect(); - HashMap::from([( - "f".to_string(), - calibrate(&sample).expect("fixture floor"), - )]) + HashMap::from([("f".to_string(), calibrate(&sample).expect("fixture floor"))]) } #[test] fn live_contract_places_weather_after_every_named_tenant() { let manifest = fixture_manifest(); let floors = fixture_floors(); - let cell = pack_cell(WEATHER_W1_CLASSID, 720, 1439, &manifest, &floors, |entry| { - match entry.variable.as_str() { + let cell = pack_cell( + WEATHER_W1_CLASSID, + 720, + 1439, + &manifest, + &floors, + |entry| match entry.variable.as_str() { "a" => Some(10.0), "b" => Some(20.0), "c" => Some(30.0), _ => None, - } - }) + }, + ) .expect("cell packs"); let row = assemble_row(&cell).expect("canonical row assembles"); @@ -265,18 +264,15 @@ mod tests { assert_eq!(weather_value_offset(), expected_offset); assert_eq!(row.key.as_bytes(), &cell.key); - assert!(row.edges.in_family.iter().all(|b| *b == 0)); - assert!(row.edges.out_family.iter().all(|b| *b == 0)); + assert!(row.edges.as_bytes().iter().all(|b| *b == 0)); assert!(row.value[..expected_offset].iter().all(|b| *b == 0)); assert_eq!( &row.value[expected_offset..expected_offset + W1_IMAGE_LEN], &image ); - assert!( - row.value[expected_offset + W1_IMAGE_LEN..] - .iter() - .all(|b| *b == 0) - ); + assert!(row.value[expected_offset + W1_IMAGE_LEN..] + .iter() + .all(|b| *b == 0)); assert!(VALUE_SLAB_LEN - expected_offset >= W1_IMAGE_LEN); } @@ -284,11 +280,13 @@ mod tests { fn canonical_serialization_is_exactly_512_bytes_and_key_agrees() { let manifest = fixture_manifest(); let floors = fixture_floors(); - let cell = pack_cell(WEATHER_W1_CLASSID, 123, 456, &manifest, &floors, |_| Some(42.0)) - .expect("cell packs"); + let cell = pack_cell(WEATHER_W1_CLASSID, 123, 456, &manifest, &floors, |_| { + Some(42.0) + }) + .expect("cell packs"); let mut row = assemble_row(&cell).expect("row"); - row.edges.in_family[0] = 0xA5; - row.edges.out_family[3] = 0x5A; + row.edges.as_bytes_mut()[0] = 0xA5; + row.edges.as_bytes_mut()[15] = 0x5A; let bytes = row_bytes(&row); let expected_offset = ValueSchema::Full.tenant_bytes();