diff --git a/.claude/board/EPIPHANIES.md b/.claude/board/EPIPHANIES.md index 621669000..62d9fe646 100644 --- a/.claude/board/EPIPHANIES.md +++ b/.claude/board/EPIPHANIES.md @@ -1,3 +1,97 @@ +## 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`, +four arms, 64K pairs, min of 7, oracle-first, in-tree and reproducible) + +SHIPPED (the revert, same PR). The carrier-2 opportunity is CONJECTURE — named, +not measured. +**Confidence:** HIGH on the numbers and the disassembly. The three-carrier +taxonomy is a reading of the tree, offered as the strongest available +explanation of (16)'s inversion, not as a proof of what (16) originally timed. + +### The correction + +Entry (16) records *"loop 12.5 ns → masked readout 5.8 ns"* for the facet +per-axis LCP. **On the facet carrier that ordering is inverted.** Measured +2026-09-17 on its own 64K-random workload, both axes, ns/op: + +| workload | A chain fold (retired by #1244) | C masked `u128` (shipped by #1244) | +|---|---|---| +| random | **1.72** | 3.64 | +| depth 0 | **1.66** | 4.68 | +| depth 5 | **3.50** | 3.56 | +| identical | **3.30** | 3.73 | + +Arm A wins at every workload; on the entry's own workload by **2.1×**. Arm A +depth-0 1.66 → depth-5 3.50 (+111%) — the early exit is real and is where the +win comes from. Two further arms (a pack-to-`u64` PEEK, and a shared-load +variant) were slower than both; the prediction that packing would win was +wrong and is recorded as wrong. + +**⊘ Struck from (16): "the gather dominated".** The disassembly refutes the +premise. LLVM never materializes the `[u8; 6]` and never gathers: + +``` +movzbl 0x5(%rdi),%eax ; PEEK hi[0] of a +cmp 0x5(%rsi),%al ; compare straight against b's memory +jne ; done — one tier compared +movzbl 0x7(%rdi),%r8d ; only now PEEK hi[1] +``` + +Arm C performs the *same* byte loads, then pays to reassemble them +(`shl`/`or`), materialize `movabs $0xff00ff00ff00ff00`, run two `tzcnt` + a +`cmove`, and apply the `−32` correction — and cannot exit early. It is strictly +more work on the same loads. + +**What survives (16) unchanged:** the `-f` naming; `shared_prefix_tiles` (a +genuine whole-register `xor`+`tzcnt`, different op, untouched); the entire +ndarray `ternlogq` descent half (5–8× over padding — measured on its own +carrier and not in question); the `−32` correction note; the 23-tail-site count. + +### Why it inverted — three carriers, three folds + +The same words ("prefix fold", "LCP", "shared depth") name three different +operations on three different carriers. The op that is optimal on one is +pessimal on the next, and nothing in the vocabulary flags the crossing. + +| # | carrier | shape | right fold | state | +|---|---|---|---|---| +| 1 | **bit-planes** — `mailbox_soa.rs` `identity_plane_at → &[u64]`, `N × WORDS_PER_FP` | sub-byte, no byte addresses | **mask / popcount** (`DistanceMeans::Hamming`) | correct, untouched | +| 2 | **nibble path** — `NiblePath` (`hhtl.rs:251`), packed `u64`, 16 nibbles | sub-byte, packed into one register | **mask** — `xor` + `leading_zeros() >> 2` | **walks nibble-by-nibble today; the one real opportunity** | +| 3 | **facet cascade** — `FacetCascade`, 6×2×8 bytes at fixed offsets | byte-addressed, offsets known at compile time | **PEEK** — `movzbl` + `cmp` + early exit | reverted to PEEK by this entry | + +The rule, in the operator's formulation (2026-09-17): **masking wins when the +slice is granular; PEEK wins when the slice is addressed.** A fold result is +scoped to its carrier and does not travel. (16)'s 12.5 ns is entirely plausible +as a measurement of a *carrier-2-shaped* loop — a per-step walk against a +packed integer, which is exactly what `common_prefix_depth` still is. What is +not supportable is transferring that conclusion to carrier 3, where the loop +compiles to compares against memory. + +### The opportunity this names (carrier 2 — CONJECTURE, unmeasured) + +`NiblePath::common_prefix_depth` is the fold `mailbox_scan.rs:263` actually +calls for CAKES nearest-ranking. It walks up to 16 nibbles, each step a shift, +an `Option` construct and a two-field compare, to compute what is +`((a.path ^ b.path).leading_zeros() >> 2)` clamped to `min(depth)`. This is the +carrier where (16)'s instinct was right and was never applied. **Gate:** the +same four-arm probe harness, against this carrier, before any rewrite — the +whole point of this entry is that a fold is not portable on argument alone. + +### The generalized rule + +**Before moving a fold, name its carrier.** A measurement is a statement about +(operation, carrier, workload); dropping the carrier makes it a slogan. Any PR +that changes a prefix/LCP/distance fold must state which of the three carriers +it touches and carry a probe on *that* carrier. Sibling of the falsifiability +rule in `CLAUDE.md`: an assertion implied by the code it tests is not a test, +and a measurement transferred off its carrier is not a measurement. + +Doctrine: `.claude/knowledge/three-prefix-fold-carriers.md`. +Blast radius: `.claude/plans/three-carrier-blast-radius-v1.md`. +Cross-ref: (16) above (struck in part, cited in full); `E-PANCAKES-IS-RADIX-IS-HHTL` +(carrier 2's doctrine); `E-VACUOUS-ASSERTION-IS-THE-HOUSE-STYLE-1` (the +differential test was inverted in the revert so it stays falsifiable). + ## 2026-09-16 (16) — E-FORMAT-SLOT-FOLD-IS-THE-SAME-OP-AS-THE-VL-DESCENT-1 — `"{0}{1}" -f hi,lo`: the register is a template with fixed arity, and both the facet LCP and the ternlogq tail are "pick the template whose arity matches the arguments, never pad them" **Status:** MEASURED on the ndarray side (the descent probe, ndarray @@ -79,6 +173,20 @@ carried):** `pack` follow-up would retire all 23, not 12; whether the Morton-shift tail fits the same helper is part of that follow-up, not settled here. +**⊘ PARTIAL STRIKE 2026-09-17 (appended; see entry (17) +`E-THREE-CARRIERS-THREE-FOLDS-1` above).** The facet half of this entry is +inverted on its own carrier: measured four ways in-tree, the retired chain fold +is **1.72 ns** and the shipped masked readout **3.64 ns** on this entry's own +64K-random workload — the opposite ordering, 2.1×. The premise *"the gather +dominated"* is refuted by disassembly: LLVM never materializes the `[u8; 6]`. +`shared_axis` is reverted to the chain fold; the masked form is retained as its +test oracle. Everything else here stands unchanged — the `-f` naming, +`shared_prefix_tiles`, the `−32` note, the 23-site count, and the whole ndarray +`ternlogq` descent half (a different carrier, measured on its own and not in +question). What the 12.5 ns actually timed is unknown — no harness was +committed with it; entry (17) gives the strongest available account (a +carrier-2-shaped loop) and labels it as such. + ## 2026-09-16 (15) — E-THE-SPINE-IS-WHATEVER-THE-READER-ALREADY-HAS-AN-ADDRESS-FOR-1 — the operator's quack redirect, and the four errors of one session that all substituted an address for the thing **Status:** OPERATOR-RULED (the redirect, verbatim below) + MEASURED (the census diff --git a/.claude/board/INTEGRATION_PLANS.md b/.claude/board/INTEGRATION_PLANS.md index c9558b00b..4cf9916f5 100644 --- a/.claude/board/INTEGRATION_PLANS.md +++ b/.claude/board/INTEGRATION_PLANS.md @@ -1,3 +1,20 @@ +## 2026-09-17 (1) — three-carrier-blast-radius-v1 — how far do the three prefix-fold carriers reach, and where do they touch → `.claude/plans/three-carrier-blast-radius-v1.md` + +**Status:** PROPOSAL. Read-only census + seam map; gates only, no code +authorized. Arose from `E-THREE-CARRIERS-THREE-FOLDS-1`: an optimization +measured on the bit-plane/nibble side was shipped into the byte-addressed facet +side, where it is 2.1× slower. The defect was a **scope error, not a bad +optimization** — and no gate the workspace runs can see one, because the three +carriers share a vocabulary ("prefix fold", "LCP", "shared depth"). + +Four passes (type census · fold census · **seams** · outward radius), five +gates (G-EXH / G-FOLD / G-SEAM / G-PROBE / G-ZERO). Pass 3 is the highest-value +one: `mailbox_scan.rs` already consumes two carriers, which is the exact +confusion surface. Explicitly NOT authorized: any carrier-2 rewrite before its +own probe, any carrier-1 change, and any widening of `FacetTier`'s `u8:u8` to +make a mask cheaper (`E-V3-FACET-4-PLUS-12` — the fold adapts to the layout, +never the reverse). + ## 2026-09-16 (1) — lance-graph-as-the-modelgraph-v1 — the ModelGraph stops being a transient Rust object and becomes addressed rows in the V3 SoA → `.claude/plans/lance-graph-as-the-modelgraph-v1.md` **Status:** PROPOSAL (operator-set endgame: *"endgame should be to wire diff --git a/.claude/board/ISSUES.md b/.claude/board/ISSUES.md index 5f26a3ce0..c4699f7d3 100644 --- a/.claude/board/ISSUES.md +++ b/.claude/board/ISSUES.md @@ -1,3 +1,37 @@ +## 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 +carrier 2 per `.claude/knowledge/three-prefix-fold-carriers.md`: 16 nibbles packed into +one `u64`, sub-byte, no byte addresses. It walks depth-by-depth, each step a shift, an +`Option` construct and a two-field compare: + +```rust +while d < max { + match (self.prefix(next), other.prefix(next)) { + (Some(a), Some(b)) if a.path == b.path && a.depth == b.depth => d = next, + _ => break, + } +} +``` + +Up to 16 iterations for what is `((self.path ^ other.path).leading_zeros() >> 2)` clamped +to `min(self.depth, other.depth)` — root-first, so `leading_zeros`, not `trailing_zeros`. + +**Live, not hypothetical:** `crates/lance-graph/src/graph/mailbox_scan.rs` (`pa.common_prefix_depth(pb)`) calls it for +CAKES nearest-ranking (`E-PANCAKES-IS-RADIX-IS-HHTL`). Two further local copies of the +same fold shape exist over `HhtlKey` in `crates/perturbation-sim/examples/` +(`outage_over_hhtl_hops.rs:76`, `basin_placement_learning.rs:76`) and should be +classified with it. + +**This is the opportunity the 2026-09-16 sweep was reaching for and applied to the wrong +carrier** (`E-THREE-CARRIERS-THREE-FOLDS-1`). Which is exactly why it must NOT be rewritten +on that entry's authority: **CONJECTURE until probed on this carrier.** Gate: the four-arm +harness from `examples/facet_axis_lcp_probe.rs`, re-armed for `NiblePath`, with workloads +covering `EMPTY`, unequal depths, ancestor pairs, and full-16 agreement — the depth clamp +and the edge cases are where a fast prefix fold gets quietly wrong. + +Sequencing and the gates: `.claude/plans/three-carrier-blast-radius-v1.md` steps 4–5. + ## ISS-LANCEDB-038-NEEDS-REMOTE-TO-COMPILE (2026-09-15) — OPEN, upstream bug, our `lancedb-sdk` feature does not build `lancedb 0.38.0` does not compile with its own default feature set. Measured, reading the diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index b7fab254b..729cb257f 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -1,3 +1,17 @@ +## three-carrier prefix folds (D-ids minted 2026-09-17, plan `.claude/plans/three-carrier-blast-radius-v1.md`) + +Arose from the −32 offset correction sweep (#1244) and the operator's challenge to it. +Doctrine: `.claude/knowledge/three-prefix-fold-carriers.md`. Board: +`E-THREE-CARRIERS-THREE-FOLDS-1`. + +| D-id | scope | status | gate / falsifier | +|---|---|---|---| +| D-TCF-1 | probe the facet per-axis LCP four ways and settle which fold the byte-addressed carrier wants | **Shipped.** `examples/facet_axis_lcp_probe.rs`, 64K pairs, min-of-7, oracle-first. Chain PEEK **1.72 ns** vs masked `u128` **3.64 ns** on random; PEEK wins at every workload; the two pack-to-register arms are slowest | all four arms must agree with the shipped API before timing; the depth knob must bind (A: 1.66 → 3.50 ns over depth 0..5, +111%) — a flat slope would mean the early exit is not the mechanism | +| D-TCF-2 | revert `shared_axis` to the chain fold; keep the differential test falsifiable | **Shipped.** Masked form retained as `masked_axis_oracle` under `#[cfg(test)]` — direction reversed so the test compares shipped-vs-oracle, not shipped-vs-itself | post-revert re-run reproduces the ordering (A 1.76 / C 4.48); 1354 contract tests green; the prior disable-run (swap hi/lo masks ⇒ fail at `hi flip at tier 0`) still holds on the oracle | +| D-TCF-3 | name the three carriers and the law that separates them | **Shipped.** `.claude/knowledge/three-prefix-fold-carriers.md` — bit-planes / nibble path / facet cascade; *masking wins when the slice is granular, PEEK wins when the slice is addressed* | falsified by a fourth carrier, or by one carrier consuming another's fold. Two greps must stay empty: `FacetCascade` in `mailbox_soa.rs`; `trailing_zeros\|leading_zeros` in `hhtl.rs` | +| D-TCF-4 | carrier-2 (`NiblePath::common_prefix_depth`) masked rewrite | **Queued — CONJECTURE, deliberately unbuilt.** `ISS-NIBLEPATH-FOLD-IS-CARRIER-2-UNMASKED`. This is the carrier the 2026-09-16 instinct was right about | must be probed on ITS OWN carrier first (G-PROBE). Landing it on entry (16)'s number would be the original defect recurring | +| D-TCF-5 | blast-radius census of all three carriers + their seams | **Queued.** Plan above, passes 1–4, read-only | G-EXH (walk, not a chosen file list) · G-FOLD (every fold site carries a carrier) · G-SEAM (owner-in / owner-out / cost) · G-ZERO (a "no callers" claim verified by opening, never by an empty grep) | + ## elk/ro factfinder — the meet, the horizon and their generality (D-ids minted 2026-09-15, probe `.claude/probes/elk-generality-v1/`) Arose from the EWA 12-hop fanout question ("would MQ offer a cheap gating"). No plan diff --git a/.claude/knowledge/three-prefix-fold-carriers.md b/.claude/knowledge/three-prefix-fold-carriers.md new file mode 100644 index 000000000..6abdd605b --- /dev/null +++ b/.claude/knowledge/three-prefix-fold-carriers.md @@ -0,0 +1,155 @@ +# Three prefix-fold carriers — name the carrier before you move the fold + +> **READ BY:** any session touching a prefix / LCP / shared-depth / distance +> fold, any `trailing_zeros` / `leading_zeros` / `popcount` readout, any +> "optimize this loop into a mask" proposal, `facet.rs`, `hhtl.rs`, +> `mailbox_soa.rs`, `mailbox_scan.rs`, or `ndarray::simd`'s masking ops. +> **MANDATORY** before changing any of the three folds catalogued below. +> +> Born 2026-09-17 from `E-THREE-CARRIERS-THREE-FOLDS-1`: a fold optimization +> measured on one carrier was shipped into another, where it is 2.1× slower. +> Nothing in the shared vocabulary ("prefix fold", "LCP", "shared depth") +> flagged the crossing. + +## §1 The law + +**A measurement is a statement about `(operation, carrier, workload)`.** Drop +the carrier and it becomes a slogan that will be applied where it is false. + +**Masking wins when the slice is GRANULAR. PEEK wins when the slice is +ADDRESSED.** (Operator, 2026-09-17.) Granular = the datum has no byte address +of its own (a bit in a plane, a nibble in a packed word) and must be extracted +arithmetically, so you may as well extract all of them at once. Addressed = the +datum sits at a constant byte offset, so the CPU can load and compare it +directly and stop at the first mismatch. + +**Corollary — PEEK is not a fallback, it is a different instruction sequence.** +A masked readout over an addressed carrier performs *the same loads* as the +PEEK and then pays extra to reassemble them, and it forfeits the early exit. +It is strictly more work, never less. + +## §2 The three carriers + +| # | name | type / site | granularity | stride | fold today | right fold | +|---|---|---|---|---|---|---| +| 1 | **bit-planes** | `MailboxSoA` / `MailboxSoaView::identity_plane_at → &[u64]` (`cognitive-shader-driver/src/mailbox_soa.rs`), `N × WORDS_PER_FP`; consumed by `graph::mailbox_scan::DistanceMeans::Hamming` | 1 bit | 64-bit word | mask + popcount | **mask** ✓ correct | +| 2 | **nibble path** | `NiblePath { path: u64, depth: u8 }` (`lance-graph-contract/src/hhtl.rs:251`), 16 nibbles, `MAX_DEPTH = 16`; consumed by `graph/mailbox_scan.rs:263` for CAKES nearest | 4 bits | 4-bit nibble | **per-nibble walk** | **mask** ✗ opportunity | +| 3 | **facet cascade** | `FacetCascade` (`lance-graph-contract/src/facet.rs`), `repr(C, align(16))`, `classid: u32` + `tiers: [FacetTier { lo: u8, hi: u8 }; 6]` | 8 bits | 16-bit tier, byte-addressed | **PEEK chain** | **PEEK** ✓ correct (since the 2026-09-17 revert) | + +### Carrier 1 — bit-planes + +Sub-byte with no addresses at all: a fingerprint bit has no offset you can name +at compile time. Every operation is necessarily whole-word arithmetic — +`popcount`, `ternlog`, `xor`-then-count. There is no early exit to forfeit +because there is nothing to exit from: Hamming distance needs every word. +**Masking is not merely right here, it is the only shape.** This carrier is +also where `ndarray::simd`'s masking ops and the `ternlogq` tail descent live, +and those results are sound on their own terms. + +### Carrier 2 — the nibble path + +Sub-byte but packed into ONE register. A nibble has no byte address, so +extracting nibble `t` costs a shift and a mask; extracting *all* of them costs +one `xor` and one `leading_zeros`. The shipped fold does the expensive thing 16 +times: + +```rust +// hhtl.rs:251 — as of 2026-09-17 +while d < max { + match (self.prefix(next), other.prefix(next)) { // shift + reconstruct + Option + (Some(a), Some(b)) if a.path == b.path && a.depth == b.depth => d = next, + _ => break, + } +} +``` + +The masked form is `((self.path ^ other.path).leading_zeros() >> 2)` clamped to +`min(self.depth, other.depth)` — root-first, so `leading_zeros`, not +`trailing_zeros`. **This is the one place in the tree where the 2026-09-16 +instinct was correct and was never applied.** It is CONJECTURE until probed: +see §4. + +### Carrier 3 — the facet cascade + +Byte-addressed with compile-time-constant offsets. Tier `t`'s `lo` byte is at +`4 + 2t`, its `hi` at `5 + 2t`. In C64 terms: a **PEEK**. The address is known, +so the load is one instruction and the comparison can happen against the other +facet's memory without either side being assembled into anything: + +``` +movzbl 0x5(%rdi),%eax ; tier 0 hi of a +cmp 0x5(%rsi),%al ; straight against b's memory +jne ; first mismatch ends it +``` + +`[u8; 6]` in the source (`hi_chain` / `lo_chain`) **is a lens, not a +materialization** — LLVM never builds the array. That is what the operator +meant by *"`[u8; 6]` was meant to preserve zero copy"*: it names six addressed +bytes; it does not gather them. + +The masked alternative (`xor` the `u128`, `&` an axis mask, `tzcnt`, subtract +32, divide by 16) issues the same `movzbl`s, then `shl`/`or`s them back +together, materializes a 64-bit mask constant, runs `tzcnt` + `cmove`, and +applies the offset correction — with no early exit. + +**The `−32`** is where the carrier mismatch shows up in the source itself: the +classid's 32 bits sit *below* the tiers in the LE `u128`, so the mask removes +the classid's *bits* but not its *offset*. A correction term whose only job is +to undo a coordinate system the operation didn't want is a carrier smell. + +## §3 Measured (2026-09-17, `examples/facet_axis_lcp_probe.rs`) + +Four arms, 64K pairs, min of 7 runs, both axes, ns/op. All four arms verified +against the shipped API on every workload before timing (oracle-first); the +depth knob is verified to bind 0..=6. + +| workload | A chain PEEK | B pack-to-`u64` PEEK | C masked `u128` | D PEEK, shared loads | +|---|---|---|---|---| +| random | **1.72** | 4.75 | 3.64 | 5.18 | +| depth 0 | **1.66** | 4.60 | 4.68 | 4.70 | +| depth 1 | **1.91** | 5.07 | 3.55 | 4.85 | +| depth 2 | **2.27** | 5.34 | 3.45 | 4.70 | +| depth 3 | **2.50** | 5.14 | 3.56 | 4.61 | +| depth 4 | **2.98** | 4.77 | 3.56 | 5.07 | +| depth 5 | **3.50** | 4.88 | 3.56 | 4.50 | +| identical | **3.30** | 4.46 | 3.73 | 4.49 | + +Re-run post-revert reproduced the ordering and magnitudes (A 1.76 / C 4.48 on +random; run-to-run spread ≈ ±0.9 ns on the masked arms, ≈ ±0.05 on A). + +Reading, including the parts that went against the author's prediction: + +- **A wins everywhere**, and by most at shallow depth — the early exit is the + mechanism. A's own depth-0 → depth-5 slope (+111%) is the anti-vacuity + evidence that the knob binds. +- **B is the slowest arm.** Packing six addressed bytes into a `u64` to then + `tzcnt` them costs twelve shift-or pairs — more than the compare it replaces. + The prediction that B would win was wrong. *Recorded as wrong: a PEEK carrier + does not want its bytes assembled, even into a register it could then mask.* +- **C only catches A at depth 5 / identical**, i.e. exactly where A has no early + exit left to use. That is the honest boundary of the masked form's value on + this carrier, and it is a tie, not a win. + +## §4 What is NOT settled + +- **What (16)'s 12.5 ns measured.** No harness was committed with it. The + carrier-2 account is the strongest available explanation, not a finding. +- **Carrier 2's rewrite.** Unmeasured. `leading_zeros` on a `u64` is one + instruction, but the depth-clamp and the `EMPTY`/ancestor edge cases are + where a fast fold gets subtly wrong. Probe before rewriting. +- **Production callers of carrier 3's axis distances.** `hi_distance` / + `lo_distance` have **zero** callers in this tree (out-of-tree consumers are + not verifiable from here). The revert is therefore near-zero-risk *and* the + performance of either form is currently unobserved by anything shipping. + +## §5 Falsifier for this page + +- The four-arm probe is in-tree, runs in ~2 s, and is the falsifier for §3. If + a future toolchain inverts the ordering, this page is wrong and says so by + failing to reproduce. +- §2's carrier assignment is falsified by finding a *fourth* carrier, or by + finding one of the three consuming another's fold. Both are greps that must + come back empty: `FacetCascade` in `mailbox_soa.rs` (it does not appear); + `trailing_zeros`/`leading_zeros` in `hhtl.rs` (it does not appear). +- The law in §1 is falsified by a measured case where a masked readout beats a + PEEK on a byte-addressed carrier with a live early exit. None is known. diff --git a/.claude/plans/three-carrier-blast-radius-v1.md b/.claude/plans/three-carrier-blast-radius-v1.md new file mode 100644 index 000000000..605b2081b --- /dev/null +++ b/.claude/plans/three-carrier-blast-radius-v1.md @@ -0,0 +1,132 @@ +# three-carrier-blast-radius-v1 — how far do the three prefix-fold carriers reach? + +**Status:** PROPOSAL (2026-09-17). Gates only; no code change is authorized by +this document. +**Doctrine:** `.claude/knowledge/three-prefix-fold-carriers.md` +**Board:** `E-THREE-CARRIERS-THREE-FOLDS-1` (EPIPHANIES entry 17) +**D-ids (STATUS_BOARD § three-carrier prefix folds):** the four census passes +below are **D-TCF-5**; the carrier-2 probe + rewrite they gate is **D-TCF-4**. +The shipped rows this plan builds on are D-TCF-1 (probe), D-TCF-2 (revert) and +D-TCF-3 (doctrine). + +## Why a blast-radius pass at all + +The defect that produced this plan was not a bad optimization — arm C is +competently written and correctly tested. It was a **scope error**: a result +true of one carrier was applied to another because the two share a vocabulary. +That failure mode is invisible to every gate the workspace already runs. Clippy +does not know what a carrier is; the differential test passed (both forms +compute the same answer); the board entry was well-formed. **The only thing +that catches it is knowing how far each carrier reaches, and where they touch.** + +So the deliverable is not "a faster fold". It is a **carrier census**: for each +of the three, the exhaustive set of types, folds, call sites, tests, docs and +cross-repo consumers — and, critically, the set of places where one carrier's +output feeds another's input, because those seams are where a future transfer +will happen. + +## Method, in four passes + +Each pass is `grep FINDS, reading DECIDES` (CLAUDE.md P0): grep produces +candidates, every candidate is **opened** before it enters a census row, and a +negative result is never recorded without opening the place the thing would +live. Delegate the sweeps to Sonnet (grindwork: "find every site that X"), +keep the classification on the main thread (accumulation: "is this site +carrier 2 or carrier 3"). + +### Pass 1 — type census (per carrier, exhaustive) — D-TCF-5 + +For each carrier, the type(s) that ARE it, and every constructor / accessor / +`from_*_bytes` / `to_*_bytes` on them. Output: a table of +`type → file:line → carrier → is it a fold, a lens, or a materialization`. + +Seed greps (candidates only): +- C1: `identity_plane_at`, `WORDS_PER_FP`, `IdentityPlane`, `DistanceMeans` +- C2: `NiblePath`, `common_prefix_depth`, `HhtlKey`, `prefix(`, `MAX_DEPTH` +- C3: `FacetCascade`, `FacetTier`, `hi_chain`, `lo_chain`, `shared_prefix_tiles`, + `row_match_mask`, `CascadeShape` + +Known-present trap: `perturbation-sim/examples/{outage_over_hhtl_hops, +basin_placement_learning}.rs` each define a LOCAL `common_prefix_depth` over +`HhtlKey`. Two more carrier-2-shaped folds, neither routed through `NiblePath`. +They must be classified, not skipped as examples. + +### Pass 2 — fold census (the thing that actually moved) + +Every site that computes a prefix, an LCP, a shared depth, a Hamming distance, +or a "how far do these agree" answer — regardless of what it is named. Grep +`trailing_zeros|leading_zeros|count_ones|popcnt|common_prefix|shared_|_distance| +is_ancestor|prefix_depth`, then open each and assign a carrier. + +**Gate G-FOLD:** every row in this census names its carrier. A row that cannot +be assigned is a finding — it means a fourth carrier exists, or that a site +mixes two. + +### Pass 3 — the seams (highest value; do not skip) — D-TCF-5 + +Where does one carrier's output become another's input? Three candidate seams +are already visible and each must be opened and characterized: + +- **S1 — GUID → `NiblePath`.** `NiblePath::from_guid_prefix` / `_v3` fold a + 128-bit GUID (byte-addressed, carrier-3-shaped) into a packed `u64` + (carrier 2). This is a *deliberate* carrier change and is the seam most + likely to invite "the fold is the same on both sides". +- **S2 — `NiblePath` → `mailbox_scan`.** `mailbox_scan.rs:263` calls carrier + 2's fold while `mailbox_soa.rs` supplies carrier 1's planes to the same + scan. **One file consuming two carriers is exactly the confusion surface** + that produced this plan; `identity_plane_at`'s own doc comment points at + `DistanceMeans::Hamming`, and that pointer was the thread that unravelled it. +- **S3 — facet ↔ SoA row.** The 12-byte facet register and the 480-byte value + slab live in one 512-byte row. Any code that reads both in one sweep is a + seam. + +**Gate G-SEAM:** each seam gets one sentence stating which carrier owns the +input, which owns the output, and what the conversion costs. A seam with no +stated cost is unassessed, not free. + +### Pass 4 — outward radius (docs, tests, cross-repo) + +- Board + knowledge + plan mentions of each fold (the words travel further than + the code; entry (16) is the proof). +- `ndarray::simd` masking-ops docs that cite the facet result. +- Cross-repo consumers of `lance-graph-contract`: in-tree (planner, callcenter, + smb-bridge), `ladybug-rs`, `lance-graph-java` (`lgj-abi` imports + `lance_graph_contract::facet` — the G11 fence names it). **`hi_distance` / + `lo_distance` have zero in-tree callers; the Java side is where an + out-of-tree caller would be**, and it is checkable — the G11 fence test + enumerates exactly what crosses. + +## Falsification gates (the plan is only real if it can fail) + +| gate | passes when | fails when | +|---|---|---| +| **G-EXH** | the type census is exhaustive by construction — a directory walk, not a file list someone chose | any row was found by intuition rather than by the walk | +| **G-FOLD** | every fold site carries a carrier assignment | a site resists assignment (⇒ fourth carrier, or a mixed site) | +| **G-SEAM** | every seam states owner-in / owner-out / conversion cost | a seam is listed as "just a cast" | +| **G-PROBE** | each carrier that gets a *change* has a probe on **that** carrier, red-then-green | a change lands citing another carrier's number — the original defect, recurring | +| **G-ZERO** | a claimed "no callers" was verified by opening the place a caller would live, not by an empty grep | a negative grep is the only evidence | + +## Sequencing, and what is explicitly NOT authorized + +1. Passes 1–2 (census). Read-only. +2. Pass 3 (seams). Read-only. **Highest value — if only one pass runs, run this.** +3. Pass 4 (outward radius). Read-only. +4. *Then* the carrier-2 probe — **D-TCF-4** (four arms, same harness, `NiblePath` workloads + incl. `EMPTY`, unequal depths, ancestor pairs, full-16 agreement). +5. *Then*, and only on a green probe, the carrier-2 rewrite. + +**Not authorized by this plan:** any rewrite of carrier 2 before step 4; any +change to carrier 1 (it is correct and this plan produces no evidence about +it); any widening of `FacetTier`'s `u8:u8` into a `u16` to "make the mask +cheaper" — that is a canon violation (`E-V3-FACET-4-PLUS-12`), and it is +precisely the shape a carrier-3 masking argument tends toward. **The fold +adapts to the layout; the layout never adapts to the fold.** + +## Cost + +Passes 1–4 are one session with 3–4 Sonnet sweeps and main-thread +classification. The carrier-2 probe reuses `facet_axis_lcp_probe.rs`'s harness +(SplitMix64 seed `0x9E37_79B9_7F4A_7C15`, min-of-7, oracle-first, anti-vacuity +gates) — the arms change, the scaffolding does not. That reuse is itself a +finding worth keeping: **the harness is carrier-agnostic even though the +folds are not.** diff --git a/crates/lance-graph-contract/examples/facet_axis_lcp_probe.rs b/crates/lance-graph-contract/examples/facet_axis_lcp_probe.rs new file mode 100644 index 000000000..8d62d388e --- /dev/null +++ b/crates/lance-graph-contract/examples/facet_axis_lcp_probe.rs @@ -0,0 +1,406 @@ +//! Four-arm probe for the per-axis facet LCP — does the PEEK shape or the +//! masked single-register shape actually win, and at which workload? +//! +//! ## Why this exists +//! +//! `EPIPHANIES.md` (16) `E-FORMAT-SLOT-FOLD-IS-THE-SAME-OP-AS-THE-VL-DESCENT-1` +//! records *"loop 12.5 ns → masked readout 5.8 ns for both axes"* on 64K random +//! pairs, and that number retired the `[u8; 6]` chain fold in favour of the +//! masked `u128` readout (`shared_axis`, `facet.rs`). **No harness for that +//! number is committed anywhere in this tree** — the sibling ndarray number in +//! the same entry names `examples/ternlogq_tail_descent_probe.rs`, this one +//! names nothing, and the entry's own status line says MEASURED for the ndarray +//! side and only SHIPPED for the contract side. So the comparison is not +//! reproducible and not falsifiable as recorded. This probe makes it both. +//! +//! ## The two open questions +//! +//! 1. **Was the baseline the fold, or the fold compiled badly?** A `[u8; 6]` +//! pick off a `repr(C, align(16))` struct is six constant-offset byte reads — +//! a PEEK. That is nearly free *when the bytes have addresses* (in memory, or +//! in an xmm register a shuffle can index). Hoisted into a GPR pair it has no +//! byte addressing and the pick degrades to scalar shift-and-mask. Arms A and +//! B differ in exactly that: A takes the facet **by value** (`hi_chain()` +//! consumes `self`), B reads **through `as_bytes()`** (the documented +//! reinterpret no-op) at constant offsets. +//! 2. **Was the workload representative?** Uniformly random pairs diverge at +//! tier 0 almost always, which is the *shortest* possible prefix. An LCP is +//! asked about near neighbours, where the prefix is long. Every arm is +//! therefore measured at each shared-prefix depth 0..=6, not just random. +//! +//! ## Arms (all four compute the same two numbers: shared hi- and lo-prefix) +//! +//! | arm | shape | readout | +//! |---|---|---| +//! | A `chain_loop` | by-value `hi_chain()`/`lo_chain()` + byte loop — the shape #1244 retired | early-exit compare | +//! | B `peek_u64` | `as_bytes()` + six constant-offset reads packed to `u64` | `xor`, `tzcnt`, `>> 3` — contiguous, **no mask, no −32** | +//! | C `masked_u128` | the shipped `hi_distance`/`lo_distance` | `xor`, `& AXIS`, `tzcnt`, `− 32`, `/ 16` | +//! | D `peek_both` | one `as_bytes()` pair, both axes from the same reads | as B, shared loads | +//! +//! D exists because A/B/C are each called twice when both axes are wanted, and +//! the recorded 5.8 ns for both axes (2.9 ns each) shows zero sharing. +//! +//! ## Falsifiability +//! +//! * **Oracle first.** Every arm is cross-checked against every other arm AND +//! against the shipped `hi_distance`/`lo_distance` on every generated pair, +//! before any timing. A mismatch aborts with the pair printed — no numbers are +//! reported from a wrong arm. +//! * **Anti-vacuity on the workload.** The per-depth rows prove the depth knob +//! binds: arm A's cost must rise with depth (its loop runs longer) or the +//! generator is not actually producing longer prefixes. That is asserted. +//! * **Dead-code guard.** Inputs and the accumulated checksum go through +//! `black_box`; the checksum is printed so the loop cannot be elided. +//! * **Arms are `#[inline(never)]`** so `cargo asm` can find them. Call overhead +//! is therefore included, identically, in all four — it does not move the +//! comparison, and it is what makes the asm question answerable: +//! `cargo asm --example facet_axis_lcp_probe ` on A vs B is expected to +//! show scalar shift/mask for A and byte loads (or a shuffle) for B. +//! +//! This probe measures; it rules nothing. Whatever it prints is a measurement on +//! one machine and one toolchain, and belongs in the board entry as such. +//! +//! ## Run +//! +//! ```text +//! cargo run --release --example facet_axis_lcp_probe -p lance-graph-contract +//! ``` + +use lance_graph_contract::facet::FacetCascade; +use std::hint::black_box; +use std::time::Instant; + +// ───────────────────────────────────────────────────────────────────────────── +// Deterministic input generation (SplitMix64, the workspace's probe seed). +// ───────────────────────────────────────────────────────────────────────────── + +struct SplitMix64(u64); + +impl SplitMix64 { + fn next_u64(&mut self) -> u64 { + self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = self.0; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) + } + + fn fill16(&mut self) -> [u8; 16] { + let (a, b) = (self.next_u64().to_le_bytes(), self.next_u64().to_le_bytes()); + let mut out = [0u8; 16]; + out[..8].copy_from_slice(&a); + out[8..].copy_from_slice(&b); + out + } +} + +/// What kind of pair to generate. +#[derive(Clone, Copy)] +enum Workload { + /// Two independent random facets — both axes diverge at tier 0 with + /// probability (1 - 2^-8) ≈ 0.996. This is what the recorded 12.5/5.8 used. + Random, + /// `b` equals `a` except one byte flipped at tier `d` on BOTH axes, so the + /// shared prefix is exactly `d` on each. `d == 6` ⇒ identical pair. + Depth(u8), +} + +impl Workload { + fn label(self) -> String { + match self { + Workload::Random => "random".to_string(), + Workload::Depth(6) => "identical".to_string(), + Workload::Depth(d) => format!("depth {d}"), + } + } +} + +fn make_pairs(w: Workload, n: usize, rng: &mut SplitMix64) -> Vec<(FacetCascade, FacetCascade)> { + (0..n) + .map(|_| { + let ba = rng.fill16(); + let bb = match w { + Workload::Random => rng.fill16(), + Workload::Depth(d) => { + let mut bb = ba; + if d < 6 { + let t = d as usize; + bb[4 + 2 * t] ^= 0x80; // lo byte of tier d + bb[5 + 2 * t] ^= 0x80; // hi byte of tier d + } + bb + } + }; + (FacetCascade::from_bytes(&ba), FacetCascade::from_bytes(&bb)) + }) + .collect() +} + +// ───────────────────────────────────────────────────────────────────────────── +// The four arms. Each returns (shared_hi, shared_lo), both 0..=6. +// ───────────────────────────────────────────────────────────────────────────── + +/// **Arm A** — the `[u8; 6]` chain fold that #1244 retired, verbatim: `hi_chain` +/// takes `self` by value, so the facet is copied out of memory before the pick. +#[inline(never)] +fn arm_a_chain_loop(a: &FacetCascade, b: &FacetCascade) -> (u8, u8) { + fn looped(x: [u8; 6], y: [u8; 6]) -> u8 { + let mut n = 0u8; + while (n as usize) < 6 && x[n as usize] == y[n as usize] { + n += 1; + } + n + } + ( + looped(a.hi_chain(), b.hi_chain()), + looped(a.lo_chain(), b.lo_chain()), + ) +} + +/// Pack one axis's six bytes, coarse→fine, into the low 48 bits of a `u64`. +/// `axis_off` is 1 for `hi` (bytes 5,7,…,15), 0 for `lo` (bytes 4,6,…,14). +#[inline(always)] +fn peek_axis(bytes: &[u8; 16], axis_off: usize) -> u64 { + let mut v = 0u64; + let mut t = 0; + while t < 6 { + v |= (bytes[4 + 2 * t + axis_off] as u64) << (8 * t); + t += 1; + } + v +} + +/// Shared prefix of two packed axes: contiguous bytes, so `tz >> 3` with no +/// mask and no offset correction — the classid was never picked up. +#[inline(always)] +fn shared_packed(x: u64) -> u8 { + if x == 0 { + 6 + } else { + (x.trailing_zeros() >> 3) as u8 + } +} + +/// **Arm B** — the PEEK shape: read through `as_bytes()` (the documented +/// reinterpret no-op) at constant offsets, pack contiguous, one `tzcnt` each. +#[inline(never)] +fn arm_b_peek_u64(a: &FacetCascade, b: &FacetCascade) -> (u8, u8) { + let (pa, pb) = (a.as_bytes(), b.as_bytes()); + ( + shared_packed(peek_axis(pa, 1) ^ peek_axis(pb, 1)), + shared_packed(peek_axis(pa, 0) ^ peek_axis(pb, 0)), + ) +} + +/// **Arm C** — the shipped masked `u128` readout, through the public API. +#[inline(never)] +fn arm_c_masked_u128(a: &FacetCascade, b: &FacetCascade) -> (u8, u8) { + const fn axis_mask(axis_off: u32) -> u128 { + let mut m = 0u128; + let mut t = 0; + while t < 6 { + m |= 0xFF << (8 * (4 + 2 * t + axis_off)); + t += 1; + } + m + } + const fn shared(x: u128, mask: u128) -> u8 { + let x = x & mask; + if x == 0 { + 6 + } else { + ((x.trailing_zeros() - 32) / 16) as u8 + } + } + let x = a.as_u128() ^ b.as_u128(); + (shared(x, axis_mask(1)), shared(x, axis_mask(0))) +} + +/// **Arm D** — arm B with the two `as_bytes()` reads shared across both axes, +/// which is what a caller wanting locality on both hierarchies actually needs. +#[inline(never)] +fn arm_d_peek_both(a: &FacetCascade, b: &FacetCascade) -> (u8, u8) { + let (pa, pb) = (a.as_bytes(), b.as_bytes()); + let (mut h, mut l) = (0u64, 0u64); + let mut t = 0; + while t < 6 { + let (i_lo, i_hi) = (4 + 2 * t, 5 + 2 * t); + h |= ((pa[i_hi] ^ pb[i_hi]) as u64) << (8 * t); + l |= ((pa[i_lo] ^ pb[i_lo]) as u64) << (8 * t); + t += 1; + } + (shared_packed(h), shared_packed(l)) +} + +type Arm = (&'static str, fn(&FacetCascade, &FacetCascade) -> (u8, u8)); + +const ARMS: [Arm; 4] = [ + ("A chain_loop", arm_a_chain_loop), + ("B peek_u64", arm_b_peek_u64), + ("C masked_u128", arm_c_masked_u128), + ("D peek_both", arm_d_peek_both), +]; + +// ───────────────────────────────────────────────────────────────────────────── +// Oracle — every arm against every other arm, and against the shipped API. +// ───────────────────────────────────────────────────────────────────────────── + +fn check_arms_agree(pairs: &[(FacetCascade, FacetCascade)], what: &str) { + for (a, b) in pairs { + let shipped = (6 - a.hi_distance(*b), 6 - a.lo_distance(*b)); + for (name, f) in ARMS { + let got = f(a, b); + assert_eq!( + got, + shipped, + "ORACLE MISMATCH in {what}: arm {name} gave {got:?}, shipped \ + hi_distance/lo_distance gave {shipped:?}\n a = {:02x?}\n b = {:02x?}", + a.as_bytes(), + b.as_bytes(), + ); + } + } +} + +/// The depth knob must bind: a pair generated at depth `d` must actually have +/// shared prefix `d` on both axes. Without this the per-depth rows are theatre. +fn check_depth_knob_binds(rng: &mut SplitMix64) { + for d in 0..=6u8 { + let pairs = make_pairs(Workload::Depth(d), 64, rng); + for (a, b) in &pairs { + let got = arm_b_peek_u64(a, b); + assert_eq!( + got, + (d, d), + "depth knob does not bind: asked for depth {d}, measured {got:?}" + ); + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Timing +// ───────────────────────────────────────────────────────────────────────────── + +/// Min-of-`runs` ns/op. Min is the right statistic here: the true cost is a +/// floor and every perturbation (interrupt, migration, frequency dip) only adds. +fn time_arm( + f: fn(&FacetCascade, &FacetCascade) -> (u8, u8), + pairs: &[(FacetCascade, FacetCascade)], + runs: usize, +) -> f64 { + // Warm up: caches, branch predictors, frequency. + let mut sink = 0u64; + for _ in 0..2 { + for (a, b) in pairs { + let (h, l) = f(black_box(a), black_box(b)); + sink = sink.wrapping_add(h as u64).wrapping_add(l as u64); + } + } + black_box(sink); + + let mut best = f64::MAX; + for _ in 0..runs { + let mut acc = 0u64; + let t0 = Instant::now(); + for (a, b) in pairs { + let (h, l) = f(black_box(a), black_box(b)); + acc = acc.wrapping_add(h as u64).wrapping_add(l as u64); + } + let ns = t0.elapsed().as_nanos() as f64 / pairs.len() as f64; + black_box(acc); + if ns < best { + best = ns; + } + } + best +} + +fn main() { + const N: usize = 65_536; // the entry's "64K random pairs" + const RUNS: usize = 7; + + let mut rng = SplitMix64(0x9E37_79B9_7F4A_7C15); + + println!("facet per-axis LCP — four arms, {N} pairs, min of {RUNS} runs, ns/op (both axes)"); + println!("target: {}", std::env::var("RUSTFLAGS").unwrap_or_default()); + println!(); + + // Gates before numbers. + check_depth_knob_binds(&mut rng); + println!("depth knob binds (0..=6 verified on 64 pairs each)"); + + let workloads = [ + Workload::Random, + Workload::Depth(0), + Workload::Depth(1), + Workload::Depth(2), + Workload::Depth(3), + Workload::Depth(4), + Workload::Depth(5), + Workload::Depth(6), + ]; + + let mut rows: Vec<(String, Vec)> = Vec::new(); + let mut arm_a_by_depth: Vec = Vec::new(); + + for w in workloads { + let pairs = make_pairs(w, N, &mut rng); + check_arms_agree(&pairs, &w.label()); + let times: Vec = ARMS + .iter() + .map(|(_, f)| time_arm(*f, &pairs, RUNS)) + .collect(); + if let Workload::Depth(_) = w { + arm_a_by_depth.push(times[0]); + } + rows.push((w.label(), times)); + } + println!("oracle: all 4 arms agree with the shipped API on every workload"); + println!(); + + // Table. + print!("{:<12}", "workload"); + for (name, _) in ARMS { + print!("{name:>16}"); + } + println!(); + println!("{}", "-".repeat(12 + 16 * ARMS.len())); + for (label, times) in &rows { + print!("{label:<12}"); + for t in times { + print!("{t:>16.2}"); + } + println!(); + } + println!(); + + // Anti-vacuity: arm A is the only arm whose work depends on prefix length, + // so its cost must rise from depth 0 to depth 5. If it does not, the + // early-exit loop is not being compiled as one and the A/B contrast is not + // measuring what this probe claims. + let (a0, a5) = (arm_a_by_depth[0], arm_a_by_depth[5]); + let binds = a5 > a0 * 1.05; + println!( + "arm A depth-0 {a0:.2} ns → depth-5 {a5:.2} ns ({:+.1}%) — the early-exit loop {}", + (a5 - a0) / a0 * 100.0, + if binds { + "does depend on prefix length, as it must" + } else { + "does NOT depend on prefix length — READ THE ASM before trusting any row above" + } + ); + println!(); + // This is a gate, not a remark: a flat slope means the A/B contrast is not + // measuring what this probe claims, so the table above must not be + // recorded. Fail the process rather than let an inattentive run bank it. + assert!( + binds, + "ANTI-VACUITY FAILED: arm A depth-5 ({a5:.2} ns) is not >5% above depth-0 \ + ({a0:.2} ns); the early-exit chain is not being compiled as one on this \ + target and every row above is invalid" + ); + println!("next: cargo asm --example facet_axis_lcp_probe arm_a_chain_loop"); + println!(" cargo asm --example facet_axis_lcp_probe arm_b_peek_u64"); + println!(" A scalar shift/mask vs B byte-loads is the whole question."); +} diff --git a/crates/lance-graph-contract/src/facet.rs b/crates/lance-graph-contract/src/facet.rs index 5d06949c4..e4a62c631 100644 --- a/crates/lance-graph-contract/src/facet.rs +++ b/crates/lance-graph-contract/src/facet.rs @@ -222,38 +222,31 @@ impl FacetCascade { [t[0].lo, t[1].lo, t[2].lo, t[3].lo, t[4].lo, t[5].lo] } - /// Byte mask selecting the `hi` byte of every tier in the LE `u128` facet - /// (bytes 5, 7, … 15; the classid occupies 0..4, tier `t` sits at `4 + 2t`). - const HI_BYTES: u128 = Self::tier_byte_mask(1); - /// Byte mask selecting the `lo` byte of every tier (bytes 4, 6, … 14). - const LO_BYTES: u128 = Self::tier_byte_mask(0); - - const fn tier_byte_mask(axis_off: u32) -> u128 { - let mut m = 0u128; - let mut t = 0; - while t < 6 { - m |= 0xFF << (8 * (4 + 2 * t + axis_off)); - t += 1; - } - m - } - - /// Shared coarse→fine prefix length (0..=6) along one axis, read straight - /// off the single-register facet — no chain gather, no re-fold. + /// Shared coarse→fine prefix length (0..=6) along one axis — the early-exit + /// byte chain fold, comparing tier `t`'s axis byte straight out of each + /// facet's backing bytes. /// - /// The facet's `u128` already holds both axes formatted by position - /// (`"{0}{1}" -f hi,lo` per tier, tier 0 lowest), so the `-f` was done once, - /// at mint. An axis prefix is the whole-facet xor masked to that axis's - /// bytes, then `trailing_zeros / 16` past the 4 classid bytes — the same - /// readout [`shared_prefix_tiles`](Self::shared_prefix_tiles) uses for the - /// whole facet. `xor == 0` under the mask ⇔ all six bytes agree. - const fn shared_axis(x: u128, axis: u128) -> u8 { - let x = x & axis; - if x == 0 { - 6 - } else { - ((x.trailing_zeros() - 32) / 16) as u8 + /// **This is a PEEK, not a mask** (`E-THREE-CARRIERS-THREE-FOLDS-1`). The + /// cascade is byte-addressed: tier `t`'s axis byte sits at the compile-time + /// constant offset `4 + 2t` (lo) or `5 + 2t` (hi), so each step lowers to a + /// `movzbl` plus a `cmp` against the other facet's memory and a `jne` that + /// exits at the first divergence. Nothing is gathered — LLVM never + /// materializes the `[u8; 6]`. The single-register masked readout (`xor`, + /// mask, `tzcnt`, offset-correct) performs the SAME byte loads, then pays + /// to reassemble them and cannot exit early; measured 2026-09-17 by + /// `examples/facet_axis_lcp_probe.rs` at **3.64 ns vs 1.72 ns** on 64K + /// random pairs, and slower at every divergence depth 0..5. + /// + /// The masked form is retained as this fold's test oracle + /// (`masked_axis_oracle`), the same license a raw intrinsic gets under + /// `#[cfg(test)]`. This is the pre-#1242 `shared6` verbatim — restored, not + /// rewritten, so the probe's arm A and the shipped path are one function. + const fn shared6(a: [u8; 6], b: [u8; 6]) -> u8 { + let mut n = 0u8; + while (n as usize) < 6 && a[n as usize] == b[n as usize] { + n += 1; } + n } /// `hi`-chain distance: `6 − shared hi-prefix` — locality along the `hi` hierarchy, @@ -261,7 +254,7 @@ impl FacetCascade { #[inline] #[must_use] pub const fn hi_distance(self, other: Self) -> u8 { - 6 - Self::shared_axis(self.as_u128() ^ other.as_u128(), Self::HI_BYTES) + 6 - Self::shared6(self.hi_chain(), other.hi_chain()) } /// `lo`-chain distance: `6 − shared lo-prefix` — locality along the orthogonal `lo` @@ -269,7 +262,7 @@ impl FacetCascade { #[inline] #[must_use] pub const fn lo_distance(self, other: Self) -> u8 { - 6 - Self::shared_axis(self.as_u128() ^ other.as_u128(), Self::LO_BYTES) + 6 - Self::shared6(self.lo_chain(), other.lo_chain()) } /// Number of fully-matching low **tiles** (0..=8, classid tiles 0–1 first, then the @@ -699,23 +692,40 @@ mod tests { assert_eq!(h.row_match_mask(f), 0b1110); } - /// The masked single-register axis readout against the byte loop it replaced, - /// at every divergence position and on the identical case. A mask off by one - /// byte (hi/lo swapped, or the classid bytes included) or a missing - /// identical-clamp fails one of these rows. Disable-verified 2026-09-16: - /// swapping `HI_BYTES`/`LO_BYTES` fails `hi flip at tier 0`. + /// The shipped byte-chain fold against the **masked single-register oracle** + /// it is measured faster than, at every divergence position and on the + /// identical case. + /// + /// Direction reversed 2026-09-17 (`E-THREE-CARRIERS-THREE-FOLDS-1`): the + /// masked readout is now the `#[cfg(test)]` oracle and the chain is what + /// ships, so this stays a real differential rather than becoming a + /// restatement of the implementation. The `−32` in the oracle is the + /// classid's 32 bits sitting below the tiers in the LE `u128`: the mask + /// removes the classid's *bits*, never its *offset*. Disable-verified + /// 2026-09-16 in its prior direction: swapping the hi/lo masks fails + /// `hi flip at tier 0`; it still does. #[test] fn folded_axis_prefix_matches_the_loop_at_every_position() { - const fn looped(a: [u8; 6], b: [u8; 6]) -> u8 { - let mut n = 0u8; - while (n as usize) < 6 && a[n as usize] == b[n as usize] { - n += 1; + /// The retired single-register readout, kept as the oracle: xor the whole + /// facet, mask to one axis's six bytes, `tzcnt`, subtract the classid's + /// 32-bit offset, divide by the 16-bit tier stride. + const fn masked_axis_oracle(a: u128, b: u128, axis_off: u32) -> u8 { + let mut mask = 0u128; + let mut t = 0; + while t < 6 { + mask |= 0xFF << (8 * (4 + 2 * t + axis_off)); + t += 1; + } + let x = (a ^ b) & mask; + if x == 0 { + 6 + } else { + ((x.trailing_zeros() - 32) / 16) as u8 } - n } let f = FacetCascade::from_bytes(&sample()); let base = sample(); - // identical: both axes fully shared (the xor == 0 clamp). + // identical: both axes fully shared (the oracle's xor == 0 clamp). assert_eq!(f.hi_distance(f), 0); assert_eq!(f.lo_distance(f), 0); // flip exactly tier `t`'s hi byte, then its lo byte: prefix must be `t` on @@ -726,8 +736,9 @@ mod tests { b[4 + 2 * t + axis_off] ^= 0x80; let g = FacetCascade::from_bytes(&b); let (sh, sl) = (6 - f.hi_distance(g) as usize, 6 - f.lo_distance(g) as usize); - assert_eq!(sh, looped(f.hi_chain(), g.hi_chain()) as usize, "hi t={t}"); - assert_eq!(sl, looped(f.lo_chain(), g.lo_chain()) as usize, "lo t={t}"); + let (xf, xg) = (f.as_u128(), g.as_u128()); + assert_eq!(sh, masked_axis_oracle(xf, xg, 1) as usize, "hi t={t}"); + assert_eq!(sl, masked_axis_oracle(xf, xg, 0) as usize, "lo t={t}"); if is_hi { assert_eq!((sh, sl), (t, 6), "hi flip at tier {t}"); } else {