diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 17b66d3b7..8e036aec3 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,12 @@ +## 2026-10-04 — Register128 slab reading + bounded power sums (branch `ccr-1d39fce9-gdgy6k`, unmerged, D-LXC-29) + +### Current Contract Inventory — net delta (`register128.rs`, `hotplug.rs`, `canonical_node.rs`) + +- `register128::{Register128, REGISTER_BYTES, RegisterRails, RegisterLanes}` — 16 raw bytes, no classid; lanes produced only by binding. +- `hotplug::SlabReading::Register128 = 1`; `ResolvedReading::bind_register128`; `ActivationDrift::{NotRegister128, RegisterRailAbsent}`. +- `canonical_node::ValueTenant::{Register0, Register1}` — 16 B each at row [252,268) / [268,284), in `ValueSchema::Full` only; `BoardAggregates` reservation re-based 16 → 18. No `ENVELOPE_LAYOUT_VERSION` bump. +- Kernels live in ndarray (`masked_group_bounded_*_u8*`, `widen_bounded_*`, `fold_bounded_*_tiles`). Entry `entries/2026-10-04-register128-bounded-power-sums.md`. + ## 2026-10-04 — SPOG × slab declaration resolves through hotplug (branch `ccr-f6094d67-h6ulb3`, unmerged) ### Current Contract Inventory — net delta (`hotplug.rs`, no new registry) diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 44ad6e802..1b949ab0d 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -53,6 +53,7 @@ Plan: `.claude/plans/deepnsm-v2-lexical-evidence-consumer-v1.md`. Convergence br | **D-LXC-27** | gender of unseen German nouns (`ud_gender_eval`): compound head, DeReKo lemma, `frostem` stem, nominalised infinitive | In PR | HDT unseen nouns: compound head covers 83.7 % at 98.1 % PASS; stem 88.0 % PASS; lemma 98.4 % on 4.7 %; infinitive 91.5 % KILL; cascade 89.9 % coverage at 97.7 %. GSD weaker (compound 88.8 % KILL). Entry `2026-10-04-deepnsm-v2-unseen-noun-gender.md` | | **D-LXC-28** | DeReKo-2014 profile across genres (`dereko_profile`); TEKAMOLO order by position in edited novels vs Bible translations (`tekamolo_order`) | In PR | ADJD about 3.4 % of tokens in novels vs about 1 % in every Bible; order: fresh novels 58.9 % vs Bibles 47.1 %, intervals overlap, KILL (z ≈ 2.1); density Te/Mo 3–5× lower in Bibles (robust). Entry `2026-10-04-deepnsm-v2-dereko-and-tekamolo-order-across-genres.md` | | **D-LXC-29** | 6 vs 8 rails of u8:u8 per 16-byte tenant (classid already in the row key), measured on KJV basin membership (`toc_hydrate` TENANT CAPACITY) | In PR | basins per verse median 3 / p95 6 / max 12; one tenant fits 95.66 % (6 rails) vs 99.41 % (8 rails); overflow 1,106 → 149; exact-set group nodes do not deduplicate (1,099 of 1,106 unique). Layout change OPEN: needs an envelope audit and an operator ruling. Entry `2026-10-04-deepnsm-v2-tenant-rails-6-vs-8.md` | +| **D-LXC-29-R** | Register128 slab reading (tag 1) + two classid-free value-slab rails; bounded u8 power sums (tile ≤ 65,536) widen losslessly to `PowerSums` / `CrossPowerSums` | In PR | 4 disable runs red; jc end-to-end == wide path; bounded 1.26–1.32× uni / 1.58–1.71× bi faster (AVX-512, ns/row). OPEN: zero-copy strided row writes, production call site, declaration storage. Entry `2026-10-04-register128-bounded-power-sums.md` | ## D-RPT — ReportPlan / zero-copy pivot / report-as-OGAR-projection (2026-09-23) diff --git a/.claude/board/entries/2026-10-04-register128-bounded-power-sums.md b/.claude/board/entries/2026-10-04-register128-bounded-power-sums.md new file mode 100644 index 000000000..8ea85ca2a --- /dev/null +++ b/.claude/board/entries/2026-10-04-register128-bounded-power-sums.md @@ -0,0 +1,86 @@ +# 2026-10-04 — Register128: classid-free 128-bit register + bounded power sums as its first consumer (D-LXC-29-R) + +## DECISION (operator ruling, 2026-10-04) + +- **DECISION:** alternative 2, carried to a live consumer. A slab may declare the physical reading `SlabReading::Register128` (tag 1). Its bytes are 128 raw bits with no classid inside. +- **SCOPE:** + - The concept comes from the SPOG context, resolved through `Activation::resolve_for_context` and handed back by `RegisterLanes::concept()`. + - Facet96 (tag 0) is untouched, and none of its bytes are reused. + - `TurbovecResidue` is not used. + - #1323's logical APIs are unchanged. +- **BASIS:** "classid-free" means not embedded in the payload; it does not mean semantically untyped. +- **REVISIT WHEN:** a consumer needs more than two rails, or the rails collide with a future tenant. + +## What landed + +**lance-graph contract (`58c1c72`)** +- `SlabReading::Register128 = 1`; tags 2, 3, 0x80 and 0xFF still fail closed. +- `ValueTenant::Register0` and `ValueTenant::Register1`, each 16 bytes: + - row bytes [252,268) and [268,284), value offsets 220 and 236; + - appended after `EpisodicBasin` and included in `ValueSchema::Full` only; + - the `BoardAggregates` reservation re-bases from ordinal 16 to 18. +- `register128.rs`: + - `Register128([u8;16])`, read as 4 LE `u32` words; + - `RegisterRails::{One, Two}`; + - `RegisterLanes`, which is `Copy`, holds only numbers, and offers `get`/`set` per rail. +- `ResolvedReading::bind_register128(rails)`: + - checked once per population; + - refuses a non-Register128 slab (`NotRegister128`); + - refuses a schema without the rails (`RegisterRailAbsent`). +- **`ENVELOPE_LAYOUT_VERSION` is NOT bumped.** Appended tenants are layout-preserving (same precedent as earlier appends), and a bump would make every existing Facet96 slab fail closed. + +**ndarray (`eeb911b`, plus bench)** +- `BOUNDED_TILE_ROWS = 65,536`, with a compile-time proof that `255² · 2^16 < 2^32`. +- `masked_group_bounded_power_sums_u8{,_via,_pair}`: + - register layout `[n, Σx, Σx², reserved]`; + - word 3 is never read or written. +- `masked_group_bounded_cross_power_sums_u8{,_via,_pair}`, on two rails: + - rail0 = `[n, Σx, Σx², ·]`, identical to the univariate register; + - rail1 = `[Σy, Σy², Σxy, ·]`; + - so `n` is stored once. +- All six go through the existing `group_walk`, so lane, via and pair semantics and the drop rules are those of the `i32` kernels. +- `widen_bounded_{,cross_}power_sums` give the exact `PowerSums` / `CrossPowerSums`. +- `fold_bounded_{,cross_}power_sums_tiles` cut a population into tiles of at most 2^16 rows and `checked_merge` each tile. A tile is refused whole if any group's merge would overflow. +- A tile over the bound returns `TileTooLarge` before any write. + +**jc end-to-end (`e723858`)** +- The context is resolved and both rails bound once. +- A four-tile population is folded through ndarray. +- Each tile's registers are stored per group into `NodeRow` rails, then read back, widened and merged. The result equals the wide `i32` path, univariate and bivariate. +- A Facet96 or undeclared slab never binds. + +## Proofs (each disable-verified red, then restored) + +| claim | test | disable | +|---|---|---| +| rails only for a Register128 slab + Full schema | `register_rails_are_granted_only_to_a_register128_slab` | slab check removed; rail-presence check removed | +| concept from context, never payload | `a_register_takes_its_concept_from_the_context_never_the_payload` | — | +| 65,536 × 255 fits exactly | `the_full_bound_at_u8_max_fits_exactly` | — | +| 65,537 rows refused, nothing written | `one_row_past_the_bound_is_refused_before_any_write` | bound guard removed → red | +| narrow → widen exact, lane/via/pair | `narrow_then_widen_…`, `bivariate_narrow_then_widen_…` | widen word swapped → red (3 tests) | +| tiles + checked_merge == whole | `partitioned_tiles_merge_to_the_whole` | merge replaced by overwrite → red | +| overflowing tile commits nothing | `an_overflowing_merge_commits_nothing_of_the_tile` | check fused into commit loop → red | + +## MEASURED (AVX-512 host, `avx512f=true`, release, 16 groups, median of 31 runs, ns/row) + +| case | wide i32 | bounded u8 | ratio | +|---|---|---|---| +| univariate tile=4096 | 1.448 | 1.094 | 1.32 | +| bivariate tile=4096 | 3.032 | 1.921 | 1.58 | +| univariate tile=16384 | 1.445 | 1.095 | 1.32 | +| bivariate tile=16384 | 3.010 | 1.906 | 1.58 | +| univariate tile=65536 | 1.432 | 1.095 | 1.31 | +| bivariate tile=65536 | 3.264 | 1.911 | 1.71 | +| univariate tiled n=1,048,699 | 1.514 | 1.205 | 1.26 | +| bivariate tiled n=1,048,699 | 3.477 | 2.133 | 1.63 | + +- These are single-host, single-session numbers. AVX2 and NEON were not measured. +- Input is 1 byte per lane against 4 for the wide path. +- The wide path is timed on pre-widened `i32` lanes, so the conversion cost the bounded path avoids is not included. + +## OPEN + +- **Zero-copy strided writes:** the kernel writes a compact `&mut [[u8;16]]` working set (one register per group). These are stored to rows per group via `RegisterLanes::set`, not strided into `NodeRow` in place. +- **No production call site yet:** the mask-risc terminal for the bounded fold is not wired. +- **Declaration storage:** where `SlabDeclaration` lives in the metadata envelope, and the writer that persists it. +- **Unmeasured tiers:** AVX2, NEON and wasm timings. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 39dbe7344..61f3ecaff 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,12 +25,13 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -211 entries, 2026-08-06 .. 2026-10-04. +212 entries, 2026-08-06 .. 2026-10-04. | date | entry id | finding | file | |---|---|---|---| | 2026-10-04 | `D-LXC-22` | | [2026-10-04-wordnet-clam-chaoda-scope-and-grammar-read-params.md](2026-10-04-wordnet-clam-chaoda-scope-and-grammar-read-params.md) | | 2026-10-04 | `D-HPS-1` | | [2026-10-04-spog-slab-hotplug-resolution.md](2026-10-04-spog-slab-hotplug-resolution.md) | +| 2026-10-04 | `D-LXC-29-R` | | [2026-10-04-register128-bounded-power-sums.md](2026-10-04-register128-bounded-power-sums.md) | | 2026-10-04 | `D-LXC-25` | | [2026-10-04-deepnsm-v2-wechsel-lane-quorum.md](2026-10-04-deepnsm-v2-wechsel-lane-quorum.md) | | 2026-10-04 | `D-LXC-27` | | [2026-10-04-deepnsm-v2-unseen-noun-gender.md](2026-10-04-deepnsm-v2-unseen-noun-gender.md) | | 2026-10-04 | `D-LXC-29` | | [2026-10-04-deepnsm-v2-tenant-rails-6-vs-8.md](2026-10-04-deepnsm-v2-tenant-rails-6-vs-8.md) | diff --git a/crates/jc/tests/register128_bounded_stats.rs b/crates/jc/tests/register128_bounded_stats.rs new file mode 100644 index 000000000..3873ee9d2 --- /dev/null +++ b/crates/jc/tests/register128_bounded_stats.rs @@ -0,0 +1,204 @@ +//! D-LXC-29 end to end: a SPOG context resolves a `Register128` slab ONCE, +//! binds its rails ONCE, and bounded power sums fold tile by tile through +//! `ndarray::simd` into those rails — then widen losslessly to the exact +//! `PowerSums` / `CrossPowerSums` the wide `i32` kernels produce. +//! +//! jc is the one crate that already sees both sides (ndarray as a dependency, +//! the contract as a dev-dependency), so the cross-crate proof lives here and +//! no production crate gains a dependency. + +use lance_graph_contract::canonical_node::{NodeGuid, NodeRow, ReadMode, ValueSchema}; +use lance_graph_contract::hotplug::{Activation, ActivationDrift, SlabDeclaration, SlabReading}; +use lance_graph_contract::register128::{Register128, RegisterLanes, RegisterRails}; +use lance_graph_contract::soa_envelope::ENVELOPE_LAYOUT_VERSION; +use ndarray::simd::{ + fold_bounded_cross_power_sums_tiles, fold_bounded_power_sums_tiles, + masked_group_bounded_cross_power_sums_u8, masked_group_bounded_power_sums_u8, + masked_group_cross_power_sums_i32, masked_group_power_sums_i32, CrossPowerSums, PowerSums, + BOUNDED_TILE_ROWS, +}; + +const CONCEPT: u16 = 0x0901; +const GROUPS: usize = 7; + +fn activation() -> Activation { + Activation::new( + Vec::new(), + Vec::new(), + vec![(CONCEPT, ReadMode::PLUG_AND_PLAY_V3)], + ) +} + +fn register_slab() -> SlabDeclaration { + SlabDeclaration { + reading: SlabReading::Register128, + value_schema: ValueSchema::Full, + layout_version: ENVELOPE_LAYOUT_VERSION, + } +} + +/// One group-result row, addressed under the context's concept. The key's +/// identity is the group; the payload carries no classid. +fn group_row(group: usize) -> NodeRow { + NodeRow { + key: NodeGuid::new(u32::from(CONCEPT) << 16, 1, 2, 3, 0x66, group as u32), + edges: Default::default(), + value: [0; 480], + } +} + +struct Population { + n: usize, + mask: Vec, + keys: Vec, + xs: Vec, + ys: Vec, +} + +fn population() -> Population { + let n = 3 * BOUNDED_TILE_ROWS + 777; + let mut mask = vec![0u64; n.div_ceil(64)]; + for i in (0..n).filter(|i| i % 11 != 4) { + mask[i / 64] |= 1 << (i % 64); + } + let mut s = 0x9E37_79B9_7F4A_7C15u64; + let mut next = || { + s = s + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + (s >> 56) as u8 + }; + let xs: Vec = (0..n).map(|_| next()).collect(); + let ys: Vec = (0..n).map(|_| next()).collect(); + let keys = (0..n as u32) + .map(|i| (i.wrapping_mul(2_654_435_761) >> 11) % GROUPS as u32) + .collect(); + Population { + n, + mask, + keys, + xs, + ys, + } +} + +/// Resolution and binding run exactly once, before the population loop; the +/// loop sees only `RegisterLanes` (plain numbers) and byte lanes. +fn bind() -> RegisterLanes { + activation() + .resolve_for_context(CONCEPT, Some(®ister_slab())) + .expect("context resolves") + .bind_register128(RegisterRails::Two) + .expect("Register128 slab grants both rails") +} + +/// FAILS IF: the tiled bounded fold, stored per tile and per group into the +/// value-slab rails and read back, does not widen and merge to exactly the +/// wide `i32` path over the whole population — univariate (rail 0) and +/// bivariate (rails 0+1) alike. +#[test] +fn register_rails_carry_bounded_stats_that_widen_to_the_wide_path() { + let p = population(); + let lanes = bind(); + assert_eq!(lanes.concept(), CONCEPT, "concept comes from the context"); + + // Univariate: one row per (tile, group); the register is written per + // group, never per input row. + let mut tile_rows: Vec> = Vec::new(); + let mut regs = [[0u8; 16]; GROUPS]; + let mut out = [PowerSums::default(); GROUPS]; + fold_bounded_power_sums_tiles(p.n, &mut regs, &mut out, |t, regs| { + masked_group_bounded_power_sums_u8( + &p.mask[t.start / 64..], + &p.keys[t.clone()], + &p.xs[t], + regs, + )?; + let rows = (0..GROUPS) + .map(|g| { + let mut row = group_row(g); + assert!(lanes.set(&mut row, 0, Register128(regs[g]))); + row + }) + .collect(); + tile_rows.push(rows); + Ok(()) + }) + .unwrap(); + assert_eq!(tile_rows.len(), 4, "three full tiles and one partial"); + + let wx: Vec = p.xs.iter().map(|&x| i32::from(x)).collect(); + let wy: Vec = p.ys.iter().map(|&y| i32::from(y)).collect(); + let mut want = [PowerSums::default(); GROUPS]; + masked_group_power_sums_i32(&p.mask, &p.keys, &wx, &mut want); + assert_eq!(out, want, "tiled driver == whole"); + + // Independently: re-read every stored rail, widen, merge. + let mut reread = [PowerSums::default(); GROUPS]; + for rows in &tile_rows { + for (g, row) in rows.iter().enumerate() { + let reg = lanes.get(row, 0).unwrap(); + let w = ndarray::simd::widen_bounded_power_sums(®.0); + reread[g] = reread[g].checked_merge(w).unwrap(); + } + } + assert_eq!(reread, want, "rails read back == whole"); + assert!(want.iter().map(|w| w.n).sum::() > BOUNDED_TILE_ROWS as u64); + assert!(want.iter().all(|w| w.n > 0), "every group populated"); + + // Bivariate: rail 0 = [n, Σx, Σx², ·], rail 1 = [Σy, Σy², Σxy, ·]. + let (mut r0, mut r1) = ([[0u8; 16]; GROUPS], [[0u8; 16]; GROUPS]); + let mut cross = [CrossPowerSums::default(); GROUPS]; + let mut stored: Vec = Vec::new(); + fold_bounded_cross_power_sums_tiles(p.n, &mut r0, &mut r1, &mut cross, |t, a, b| { + masked_group_bounded_cross_power_sums_u8( + &p.mask[t.start / 64..], + &p.keys[t.clone()], + &p.xs[t.clone()], + &p.ys[t], + a, + b, + )?; + for g in 0..GROUPS { + let mut row = group_row(g); + assert!(lanes.set(&mut row, 0, Register128(a[g]))); + assert!(lanes.set(&mut row, 1, Register128(b[g]))); + stored.push(row); + } + Ok(()) + }) + .unwrap(); + let mut want_cross = [CrossPowerSums::default(); GROUPS]; + masked_group_cross_power_sums_i32(&p.mask, &p.keys, &wx, &wy, &mut want_cross); + assert_eq!(cross, want_cross, "bivariate tiled driver == whole"); + + let mut reread = [CrossPowerSums::default(); GROUPS]; + for (k, row) in stored.iter().enumerate() { + let (a, b) = (lanes.get(row, 0).unwrap(), lanes.get(row, 1).unwrap()); + let g = k % GROUPS; + let w = ndarray::simd::widen_bounded_cross_power_sums(&a.0, &b.0); + reread[g] = reread[g].checked_merge(w).unwrap(); + } + assert_eq!(reread, want_cross, "bivariate rails read back == whole"); +} + +/// FAILS IF: a population whose slab declares Facet96 (or nothing) can be +/// bound as registers. The fold never runs on such a slab. +#[test] +fn a_facet96_slab_is_never_bound_as_registers() { + let a = activation(); + let facet = SlabDeclaration { + reading: SlabReading::Facet96, + ..register_slab() + }; + for slab in [Some(&facet), None] { + let r = a.resolve_for_context(CONCEPT, slab).unwrap(); + assert!(matches!( + r.bind_register128(RegisterRails::One), + Err(ActivationDrift::NotRegister128 { + concept: CONCEPT, + .. + }) + )); + } +} diff --git a/crates/lance-graph-contract/src/canonical_node.rs b/crates/lance-graph-contract/src/canonical_node.rs index fab12147d..de68f4250 100644 --- a/crates/lance-graph-contract/src/canonical_node.rs +++ b/crates/lance-graph-contract/src/canonical_node.rs @@ -1076,6 +1076,26 @@ pub enum ValueTenant { /// Zero-fallback: an all-zero lane reads as *no basin promoted* (subject 0 /// with an empty `[0,0)` range), never as a basin over nothing. EpisodicBasin = 15, + /// **Register128 rail 0** (`D-LXC-29`) — a 128-bit working register with + /// NO classid inside it. + /// + /// The register is content-blind: 16 raw bytes, read as + /// [`Register128`](crate::register128::Register128). Its semantic identity + /// is the SPOG context the population was resolved under + /// ([`ResolvedReading`](crate::hotplug::ResolvedReading)), never a classid in + /// the payload — the difference from the Facet96 lanes (`Tekamolo`, + /// `CausalWitness`), whose first 4 bytes ARE a classid. The bytes are only + /// read as a register when the slab declares + /// [`SlabReading::Register128`](crate::hotplug::SlabReading::Register128); + /// [`ResolvedReading::bind_register128`](crate::hotplug::ResolvedReading::bind_register128) + /// refuses any other slab. + /// + /// Zero-fallback: an all-zero register is an empty accumulator. + Register0 = 16, + /// **Register128 rail 1** — the second, independent 128-bit working + /// register, for readings that need more than 128 bits (e.g. bivariate + /// statistics). Same contract as [`Register0`](Self::Register0). + Register1 = 17, } impl ValueTenant { @@ -1237,6 +1257,25 @@ pub const VALUE_TENANTS: &[ColumnDescriptor] = &[ elems_per_row: 32, row_offset: 220, }, + // ── Register128 rails (D-LXC-29): two classid-free 16 B working registers + // appended after EpisodicBasin at [252,268) and [268,284) (value-slab + // [220,236) and [236,252)); additive, reserve-don't-reclaim, + // layout-preserving (Full now ends 284 ≤ 512, NODE_ROW_STRIDE unchanged → + // no ENVELOPE_LAYOUT_VERSION bump). These mints take discriminants 16 and + // 17, so the BoardAggregates reservation RE-BASES to 18 by the same + // ordinal rule as above; its offset stays derived, never a literal. + ColumnDescriptor { + name_id: ValueTenant::Register0 as u16, + kind: ColumnKind::U8, + elems_per_row: 16, + row_offset: 252, + }, + ColumnDescriptor { + name_id: ValueTenant::Register1 as u16, + kind: ColumnKind::U8, + elems_per_row: 16, + row_offset: 268, + }, ]; // Compile-time canon: VALUE_TENANTS is discriminant-ordered, contiguous within the @@ -1349,6 +1388,8 @@ impl ValueSchema { // the `Full covers every tenant` compile assert requires it — // that assert is what caught this mint before any test ran. ValueTenant::EpisodicBasin as u8, + ValueTenant::Register0 as u8, + ValueTenant::Register1 as u8, ]), } } @@ -2608,8 +2649,8 @@ mod tests { assert!(prev_end <= NODE_ROW_STRIDE); assert_eq!( prev_end - VALUE_SLAB_ROW_OFFSET, - 220, - "current Full carve uses 220 of 480 B (kanban×Rubicon 8 + autopoiesis triangle 3×12=36 + TEKAMOLO facet 16 + CausalWitness facet 16 + episodic-basin rail 32)" + 252, + "current Full carve uses 252 of 480 B (kanban×Rubicon 8 + autopoiesis triangle 3×12=36 + TEKAMOLO facet 16 + CausalWitness facet 16 + episodic-basin rail 32 + Register128 rails 2×16)" ); assert!(prev_end - VALUE_SLAB_ROW_OFFSET <= VALUE_SLAB_LEN); } @@ -2691,11 +2732,11 @@ mod tests { // Cognitive 58 + Kanban 8 = 66 (triangle + TEKAMOLO + CausalWitness + // episodic basin NOT in Cognitive — entity classes keep their carve); // Full 120 + 3×12 triangle + 16 TEKAMOLO facet + 16 CausalWitness facet - // + 32 episodic-basin rail = 220 (all additive — reserve-don't-reclaim, - // still ≤ 480, stride unchanged). + // + 32 episodic-basin rail + 2×16 Register128 rails = 252 (all additive — + // reserve-don't-reclaim, still ≤ 480, stride unchanged). assert_eq!(ValueSchema::Cognitive.tenant_bytes(), 66); assert_eq!(ValueSchema::Compressed.tenant_bytes(), 56); - assert_eq!(ValueSchema::Full.tenant_bytes(), 220); + assert_eq!(ValueSchema::Full.tenant_bytes(), 252); for s in [ ValueSchema::Bootstrap, ValueSchema::Cognitive, @@ -2812,8 +2853,8 @@ mod tests { VALUE_TENANTS.len(), "Full read-mode materialises every value tenant" ); - assert_eq!(rm.value_schema.tenant_bytes(), 220); - // The slab has room (220 ≤ 480) and the choice never grows the stride. + assert_eq!(rm.value_schema.tenant_bytes(), 252); + // The slab has room (252 ≤ 480) and the choice never grows the stride. assert!(rm.value_schema.tenant_bytes() <= VALUE_SLAB_LEN); assert!(rm.is_layout_preserving()); } diff --git a/crates/lance-graph-contract/src/hotplug.rs b/crates/lance-graph-contract/src/hotplug.rs index fb970b6f6..2f649e162 100644 --- a/crates/lance-graph-contract/src/hotplug.rs +++ b/crates/lance-graph-contract/src/hotplug.rs @@ -201,6 +201,16 @@ pub enum SlabReading { /// ([`crate::facet::FacetCascade`]). Declared only by a slab whose bytes /// really are that facet. Facet96 = 0, + /// The 128-bit working register with NO classid in the payload + /// (`D-LXC-29`, [`crate::register128::Register128`]), held in the + /// [`ValueTenant::Register0`](crate::canonical_node::ValueTenant::Register0) / + /// [`ValueTenant::Register1`](crate::canonical_node::ValueTenant::Register1) + /// rails. The register's semantic identity is the SPOG context this + /// declaration is resolved under, not its bytes. Declaring it does not + /// change how Facet96 lanes are read; it is what + /// [`ResolvedReading::bind_register128`] requires before granting the + /// register rails. + Register128 = 1, } impl SlabReading { @@ -213,6 +223,7 @@ impl SlabReading { pub const fn from_tag(tag: u8) -> Result { match tag { 0 => Ok(SlabReading::Facet96), + 1 => Ok(SlabReading::Register128), other => Err(ActivationDrift::UnknownSlabReading(other)), } } @@ -320,6 +331,46 @@ impl Activation { } } +impl ResolvedReading { + /// Bind the register rails of this population — the one place a + /// Register128 reading is checked, done ONCE per population, never per + /// row. + /// + /// Grants `rails` only when the slab declared + /// [`SlabReading::Register128`] AND its value schema materialises every + /// rail requested. The returned [`RegisterLanes`](crate::register128::RegisterLanes) + /// carry the concept this reading was resolved under; that concept, not + /// anything in the register bytes, is the registers' semantic identity. + /// + /// # Errors + /// + /// - [`ActivationDrift::NotRegister128`] when the slab declared another + /// reading (e.g. Facet96) or nothing: an undeclared slab is never + /// assumed to hold registers. + /// - [`ActivationDrift::RegisterRailAbsent`] when the value schema does not + /// materialise a requested rail. + pub fn bind_register128( + &self, + rails: crate::register128::RegisterRails, + ) -> Result { + if self.slab != Some(SlabReading::Register128) { + return Err(ActivationDrift::NotRegister128 { + concept: self.concept, + slab: self.slab, + }); + } + for &tenant in rails.tenants() { + if !self.read_mode.value_schema.has(tenant) { + return Err(ActivationDrift::RegisterRailAbsent { + concept: self.concept, + tenant: tenant as u8, + }); + } + } + Ok(crate::register128::RegisterLanes::new(self.concept, rails)) + } +} + /// Why an activation failed — each arm is one named bang. #[derive(Debug, Clone, PartialEq, Eq)] pub enum ActivationDrift { @@ -348,6 +399,23 @@ pub enum ActivationDrift { /// A slab's metadata envelope carries a reading tag this build does not /// implement. Never assumed to be [`SlabReading::Facet96`]. UnknownSlabReading(u8), + /// [`ResolvedReading::bind_register128`] was asked for register rails of + /// a population whose slab did not declare [`SlabReading::Register128`]. + NotRegister128 { + /// The concept the reading was resolved under. + concept: u16, + /// What the slab declared instead (`None`: nothing). + slab: Option, + }, + /// The resolved value schema does not materialise a requested register + /// rail. + RegisterRailAbsent { + /// The concept the reading was resolved under. + concept: u16, + /// The missing [`ValueTenant`](crate::canonical_node::ValueTenant) + /// discriminant. + tenant: u8, + }, /// A slab was written under an envelope layout version this build does /// not implement. SlabLayoutVersion { @@ -426,6 +494,15 @@ impl core::fmt::Display for ActivationDrift { f, "slab declares reading tag {tag}, which this build does not implement" ), + Self::NotRegister128 { concept, slab } => write!( + f, + "concept 0x{concept:04X}: slab declares {slab:?}, not Register128; \ + no register rails are granted" + ), + Self::RegisterRailAbsent { concept, tenant } => write!( + f, + "concept 0x{concept:04X}: value schema does not materialise register tenant {tenant}" + ), Self::SlabLayoutVersion { slab, expected } => write!( f, "slab written under envelope layout v{slab}, this build reads v{expected}" @@ -671,7 +748,8 @@ mod tests { #[test] fn an_unsupported_physical_reading_fails_closed() { assert_eq!(SlabReading::from_tag(0), Ok(SlabReading::Facet96)); - for tag in [1u8, 2, 0x80, 0xFF] { + assert_eq!(SlabReading::from_tag(1), Ok(SlabReading::Register128)); + for tag in [2u8, 3, 0x80, 0xFF] { assert_eq!( SlabReading::from_tag(tag), Err(ActivationDrift::UnknownSlabReading(tag)) @@ -806,6 +884,98 @@ mod tests { .iter() .all(|k| crate::spog_tenants::graph_of(*k) == resolved.concept)); } + + /// A Register128 declaration at the current layout. + fn reg_decl(value_schema: ValueSchema) -> SlabDeclaration { + SlabDeclaration { + reading: SlabReading::Register128, + value_schema, + layout_version: ENVELOPE_LAYOUT_VERSION, + } + } + + /// FAILS IF: the register rails are granted without a Register128 + /// declaration (Facet96, or no declaration at all), or for a schema + /// that does not materialise them. Silence twin: Register128 + Full + /// grants both rails. + #[test] + fn register_rails_are_granted_only_to_a_register128_slab() { + use crate::canonical_node::ValueTenant; + use crate::register128::RegisterRails; + let a = act(); + let granted = a + .resolve_for_context(0x0901, Some(®_decl(ValueSchema::Full))) + .unwrap(); + assert_eq!(granted.slab, Some(SlabReading::Register128)); + let lanes = granted.bind_register128(RegisterRails::Two).unwrap(); + assert_eq!(lanes.rails(), RegisterRails::Two); + assert!(lanes.rail_range(1).is_some()); + + let facet = a + .resolve_for_context(0x0901, Some(&decl(ValueSchema::Full))) + .unwrap(); + let undeclared = a.resolve_for_context(0x0901, None).unwrap(); + for (r, slab) in [(facet, Some(SlabReading::Facet96)), (undeclared, None)] { + assert_eq!( + r.bind_register128(RegisterRails::One), + Err(ActivationDrift::NotRegister128 { + concept: 0x0901, + slab + }) + ); + } + + let narrow = a + .resolve_for_context(0x0901, Some(®_decl(ValueSchema::Cognitive))) + .unwrap(); + assert_eq!( + narrow.bind_register128(RegisterRails::One), + Err(ActivationDrift::RegisterRailAbsent { + concept: 0x0901, + tenant: ValueTenant::Register0 as u8, + }) + ); + } + + /// FAILS IF: a register's semantic identity is read from its bytes. + /// + /// The same register payload — here deliberately holding the OTHER + /// concept's id in its first word, the shape a Facet96 classid would + /// take — is bound under two contexts. The concept follows the + /// context both times, and the bytes come back unchanged: nothing in + /// the binding or the resolution reads the payload. + #[test] + fn a_register_takes_its_concept_from_the_context_never_the_payload() { + use crate::register128::{Register128, RegisterRails}; + let a = act(); + let d = reg_decl(ValueSchema::Full); + let payload = Register128::from_words([0x0902_0000, 7, 8, 9]); + // Rail 1, never rail 0: `register128::tests::register_writes_are_counted_per_tenant` + // pins an exact Register0 count under `tenant-counters`, and tests run in parallel. + for (concept, other) in [(0x0901u16, 0x0902u16), (0x0902, 0x0901)] { + let mut row = NodeRow { + key: key(concept, 1), + edges: Default::default(), + value: [0; 480], + }; + let lanes = a + .resolve_tenant_reading(row.key, Some(&d)) + .unwrap() + .bind_register128(RegisterRails::Two) + .unwrap(); + assert!(lanes.set(&mut row, 1, payload)); + assert_eq!(lanes.concept(), concept); + assert_ne!(lanes.concept(), other); + assert_eq!(lanes.get(&row, 1), Some(payload)); + // Resolving again after the write is unchanged. + let again = a + .resolve_tenant_reading(row.key, Some(&d)) + .unwrap() + .bind_register128(RegisterRails::Two) + .unwrap(); + assert_eq!(again, lanes); + } + } } /// The drift class the retired `COUNT_FUSE` guarded: a concept the diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 25bfc93be..9dc929b09 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -142,6 +142,7 @@ pub mod proprioception; pub mod qualia; pub mod rail_geometry; pub mod rbac; +pub mod register128; pub mod tekamolo_facet; pub use qualia::{ axis_index, axis_label, qualia_to_state, QualiaI4_16D, QualiaVector, AXIS_LABELS, MIDPOINT, diff --git a/crates/lance-graph-contract/src/register128.rs b/crates/lance-graph-contract/src/register128.rs new file mode 100644 index 000000000..007197646 --- /dev/null +++ b/crates/lance-graph-contract/src/register128.rs @@ -0,0 +1,298 @@ +//! `register128` — the 128-bit working register with no classid inside it +//! (`D-LXC-29`). +//! +//! # What it is +//! +//! 16 raw bytes in a value-slab rail ([`ValueTenant::Register0`], +//! [`ValueTenant::Register1`]). The register is content-blind: the bytes say +//! nothing about what they mean. +//! +//! # Where its meaning comes from +//! +//! The semantic identity of a register is the SPOG context its population was +//! resolved under, never something stored in the payload: +//! +//! - the concept (classid) comes from [`crate::spog_tenants::graph_of`] of the +//! row key, resolved through [`crate::hotplug::Activation::resolve_for_context`]; +//! - the node is the row's own [`NodeGuid`](crate::canonical_node::NodeGuid); +//! - rung / thought layers stay the sparse alpha mechanism +//! ([`crate::alpha`]); a register is not a dense per-layer expansion. +//! +//! This is the difference from the Facet96 lanes (`Tekamolo`, +//! `CausalWitness`): their first four bytes ARE a classid. Facet96 is not +//! touched by this module and keeps its meaning. +//! +//! # Binding, once +//! +//! A slab whose metadata declares +//! [`SlabReading::Register128`](crate::hotplug::SlabReading::Register128) is +//! bound by [`ResolvedReading::bind_register128`](crate::hotplug::ResolvedReading::bind_register128), +//! which checks the declaration and the value schema ONCE and returns +//! [`RegisterLanes`]. The hot path reads and writes registers through those +//! lanes; it never resolves a reading, looks up a class or touches a label. +//! +//! # Readings of the bytes +//! +//! Which reading a consumer applies (for example bounded statistics, whose +//! word layout lives with the fold in `ndarray::simd`) is the consumer's +//! choice under its bound context. [`Register128::words`] is the only +//! interpretation this crate provides: four little-endian `u32` words. + +use crate::canonical_node::{NodeRow, ValueTenant}; + +/// One 128-bit working register: 16 raw little-endian bytes, no classid. +/// +/// # Examples +/// +/// ``` +/// use lance_graph_contract::register128::Register128; +/// +/// let r = Register128::from_words([1, 2, 3, 0]); +/// assert_eq!(r.words(), [1, 2, 3, 0]); +/// assert_eq!(r.0[..4], 1u32.to_le_bytes()); +/// ``` +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Register128(pub [u8; 16]); + +/// Width of one register in bytes, equal to its value-slab rail. +pub const REGISTER_BYTES: usize = 16; + +impl Register128 { + /// The empty register. + pub const ZERO: Self = Self([0; REGISTER_BYTES]); + + /// The register as four little-endian `u32` words. + #[must_use] + pub const fn words(&self) -> [u32; 4] { + let b = &self.0; + [ + u32::from_le_bytes([b[0], b[1], b[2], b[3]]), + u32::from_le_bytes([b[4], b[5], b[6], b[7]]), + u32::from_le_bytes([b[8], b[9], b[10], b[11]]), + u32::from_le_bytes([b[12], b[13], b[14], b[15]]), + ] + } + + /// A register from four `u32` words, stored little-endian. + #[must_use] + pub const fn from_words(w: [u32; 4]) -> Self { + let mut b = [0u8; REGISTER_BYTES]; + let mut i = 0; + while i < 4 { + let le = w[i].to_le_bytes(); + b[4 * i] = le[0]; + b[4 * i + 1] = le[1]; + b[4 * i + 2] = le[2]; + b[4 * i + 3] = le[3]; + i += 1; + } + Self(b) + } +} + +/// How many register rails a reading needs. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RegisterRails { + /// [`ValueTenant::Register0`] only. + One, + /// [`ValueTenant::Register0`] and [`ValueTenant::Register1`]. + Two, +} + +impl RegisterRails { + /// The tenants this many rails occupy, in rail order. + #[must_use] + pub const fn tenants(self) -> &'static [ValueTenant] { + match self { + Self::One => &[ValueTenant::Register0], + Self::Two => &[ValueTenant::Register0, ValueTenant::Register1], + } + } +} + +/// The register rails of one bound population, produced ONCE by +/// [`ResolvedReading::bind_register128`](crate::hotplug::ResolvedReading::bind_register128). +/// +/// Holds plain numbers: the concept the population was resolved under, and +/// how many rails the binding granted. `Copy`, no references, no strings: it +/// is meant to be handed to the population loop. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RegisterLanes { + concept: u16, + rails: RegisterRails, +} + +impl RegisterLanes { + /// Built only by the binding, which has already checked the slab. + pub(crate) const fn new(concept: u16, rails: RegisterRails) -> Self { + Self { concept, rails } + } + + /// The SPOG concept this population was resolved under. The semantic + /// identity of every register in it; never read from a payload. + #[must_use] + pub const fn concept(&self) -> u16 { + self.concept + } + + /// The rails this binding granted. + #[must_use] + pub const fn rails(&self) -> RegisterRails { + self.rails + } + + /// The value-slab byte range of `rail`, or `None` if this binding did not + /// grant it. + #[must_use] + pub const fn rail_range(&self, rail: usize) -> Option> { + let tenants = self.rails.tenants(); + if rail >= tenants.len() { + return None; + } + let start = tenants[rail].value_offset(); + Some(start..start + REGISTER_BYTES) + } + + /// Read `rail` of `row`, or `None` if this binding did not grant it. + #[must_use] + pub fn get(&self, row: &NodeRow, rail: usize) -> Option { + let r = self.rail_range(rail)?; + let mut b = [0u8; REGISTER_BYTES]; + b.copy_from_slice(&row.value[r]); + Some(Register128(b)) + } + + /// Write `rail` of `row`. Returns `false`, writing nothing, if this + /// binding did not grant the rail. A successful write is counted against + /// the rail's tenant, like every other tenant setter + /// ([`crate::tenant_counter::tenant_update`], a no-op unless the + /// `tenant-counters` feature is on). + #[must_use] + pub fn set(&self, row: &mut NodeRow, rail: usize, reg: Register128) -> bool { + match self.rail_range(rail) { + Some(r) => { + row.value[r].copy_from_slice(®.0); + crate::tenant_counter::tenant_update(self.rails.tenants()[rail]); + true + } + None => false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::canonical_node::VALUE_TENANTS; + + /// The register width is the declared rail width, for both rails. + #[test] + fn the_register_is_exactly_one_declared_rail() { + for t in [ValueTenant::Register0, ValueTenant::Register1] { + assert_eq!( + VALUE_TENANTS[t as usize].col_bytes_per_row(), + REGISTER_BYTES + ); + assert_eq!(t.byte_len(), REGISTER_BYTES); + } + assert_eq!(core::mem::size_of::(), 16); + } + + /// Words are little-endian and round-trip, with every word distinct so a + /// swapped pair cannot pass. + #[test] + fn words_round_trip_little_endian() { + let w = [0x0403_0201, 0x0807_0605, 0x0C0B_0A09, 0x100F_0E0D]; + let r = Register128::from_words(w); + assert_eq!(r.0, core::array::from_fn::(|i| i as u8 + 1)); + assert_eq!(r.words(), w); + } + + /// FAILS IF: the two rails overlap each other or any other tenant, or do + /// not append exactly where `EpisodicBasin` ends (field isolation for a + /// layout that gains lanes, `I-LEGACY-API-FEATURE-GATED`). + #[test] + fn the_rails_touch_no_other_tenant() { + let range = |t: ValueTenant| { + let d = VALUE_TENANTS[t as usize]; + let s = d.row_offset as usize; + s..s + d.col_bytes_per_row() + }; + assert_eq!(range(ValueTenant::Register0), 252..268); + assert_eq!(range(ValueTenant::Register1), 268..284); + assert_eq!( + range(ValueTenant::EpisodicBasin).end, + 252, + "additive, not reclaiming" + ); + for rail in [ValueTenant::Register0, ValueTenant::Register1] { + let r = range(rail); + for other in VALUE_TENANTS { + if other.name_id == rail as u16 { + continue; + } + let (os, oe) = ( + other.row_offset as usize, + other.row_offset as usize + other.col_bytes_per_row(), + ); + assert!( + r.end <= os || oe <= r.start, + "{rail:?} overlaps tenant {}", + other.name_id + ); + } + } + } + + /// FAILS IF: writing a rail touches any other byte of the row, or a + /// one-rail binding can reach rail 1. + #[test] + fn writing_a_rail_leaves_every_other_byte_alone() { + let lanes = RegisterLanes::new(0x0901, RegisterRails::Two); + let mut row = NodeRow { + key: crate::canonical_node::NodeGuid::new(0x0901_0000, 1, 2, 3, 0x66, 7), + edges: Default::default(), + value: core::array::from_fn(|i| (i % 251) as u8), + }; + let before = row.value; + let reg = Register128::from_words([u32::MAX, 1, 2, 3]); + assert!(lanes.set(&mut row, 1, reg)); + assert_eq!(lanes.get(&row, 1), Some(reg)); + let r1 = lanes.rail_range(1).unwrap(); + for (i, (a, b)) in before.iter().zip(row.value.iter()).enumerate() { + if !r1.contains(&i) { + assert_eq!(a, b, "byte {i} outside rail 1 changed"); + } + } + let one = RegisterLanes::new(0x0901, RegisterRails::One); + assert_eq!(one.get(&row, 1), None); + assert!(!one.set(&mut row, 1, Register128::ZERO)); + assert_eq!( + lanes.get(&row, 1), + Some(reg), + "a refused write wrote nothing" + ); + } + + /// FAILS IF: a successful register write is not counted against its + /// tenant, or a refused one is. This is the only test in the crate that + /// writes `Register0` (every other register test writes rail 1), so the + /// delta is exact even with tests running in parallel. Keep it that way. + #[cfg(feature = "tenant-counters")] + #[test] + fn register_writes_are_counted_per_tenant() { + use crate::tenant_counter::tenant_count; + let mut row = NodeRow { + key: crate::canonical_node::NodeGuid::new(0x0901_0000, 1, 2, 3, 0x66, 9), + edges: Default::default(), + value: [0; 480], + }; + let before = tenant_count(ValueTenant::Register0); + let one = RegisterLanes::new(0x0901, RegisterRails::One); + assert!(one.set(&mut row, 0, Register128::from_words([1, 2, 3, 0]))); + assert!(one.set(&mut row, 0, Register128::ZERO)); + assert!(!one.set(&mut row, 1, Register128::ZERO), "refused"); + assert_eq!(tenant_count(ValueTenant::Register0), before + 2); + } +}