diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 5d1b5eedc..e7d55d470 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -1,3 +1,7 @@ +## 2026-10-08 — Register128 signed readings on branch `ccr-b2e415d9-4jfvyk-register-readings` (unmerged) + +- Contract delta (additive): `hotplug::SlabReading::{RegisterI4x32(law), RegisterI8x16(law)}` (tags 2..=7, `to_tag`), `Activation::{with_register_laws, register_law_for}`, `ResolvedReading::bind_signed_register(&activation, rails)`, `ActivationDrift::{NotSignedRegister, NoRegisterLawFor, RegisterLawMismatch}`; `register128::{RegisterCarving, RegisterLaw, SignedRegisterLanes, SignedRegisterRefusal}`; `atoms::I4x32::{from_bytes, as_bytes}`. No layout change. Entry: `entries/2026-10-08-register128-signed-readings.md`. + ## 2026-10-08 — HHTL / NARS / Moore value tenants on branch `ccr-b2e415d9-4jfvyk-tenants` (unmerged) - Contract delta (additive): `ValueTenant::{Nars16x8 = 18, MoorePalettePairs = 19, MooreNars16 = 20}` at row [284,332), in `ValueSchema::Full` only; new module `contract::moore_tenant::{MooreSlot, MooreNars16, DirectionReading, DirectionRefusal, MooreRefusal, MooreTenantView, MooreTenantMut}`. BoardAggregates reservation re-based to 21. Entry: `entries/2026-10-08-hhtl-nars-moore-value-tenants.md`. diff --git a/.claude/board/entries/2026-10-08-register128-signed-readings.md b/.claude/board/entries/2026-10-08-register128-signed-readings.md new file mode 100644 index 000000000..44aae9c6c --- /dev/null +++ b/.claude/board/entries/2026-10-08-register128-signed-readings.md @@ -0,0 +1,78 @@ +# 2026-10-08 — Register128 signed readings (32×i4, 16×i8) with a per-family law + +**Status:** CURRENT-CONTRACT, TEST-PINNED. Branch +`ccr-b2e415d9-4jfvyk-register-readings`, commits `14831543` and `96304b88` +(law authority, after Codex P2 on #1410). + +## What landed + +The carrier is unchanged: the `Register0` / `Register1` rails (16 B each), no +classid in the payload, no layout change. + +- **Carving and recorded law, declared by the slab.** + `SlabReading::RegisterI4x32(law)` (32 × i4, dim `2k` = low nibble of byte + `k`, the `atoms::I4x32` layout) and `SlabReading::RegisterI8x16(law)` (16 × + i8, dim `k` = byte `k`). Envelope tags 2..=7 (`to_tag` inverts `from_tag`). + The law is the one the writer actually wrote with. +- **Concept law, declared by the authority.** `Activation::with_register_laws` + / `register_law_for` (fail-closed: `NoRegisterLawFor`). +- **Binding checks agreement.** `ResolvedReading::bind_signed_register(&activation, + rails)` takes no law; it refuses `RegisterLawMismatch` when the slab's law and + the concept's differ. A writer and a reader of one population therefore hold + the same law. The returned `SignedRegisterLanes` carry concept, rails, carving + and law. `RegisterLaw::{RelativeOffset, AxisPosition, Support}`. +- **Checked on every access.** `read_i4x32` / `read_i8x16` / `write_i4x32` / + `write_i8x16` take the law the caller expects and refuse, in order, an absent + rail, a different carving, a different law. An i4 outside `-8..=7` is refused, + never saturated. A refused write leaves the row unchanged. +- **Separation from the unsigned reading.** `bind_register128` (4 × u32 words) + is unchanged and refuses the signed carvings; `bind_signed_register` refuses + `Register128`, `Facet96` and undeclared slabs. +- **EpistemicState5 has no law here.** It stays on CE64 bits 59..63. + +## Inventory of the 24×i4 families on `main` (2026-10-08) + +| family | on `main` | law it would bind | +|---|---|---| +| Temporal / anaphora | `CausalWitness` tenant 14, Facet96 read as G24N4 (24 i4 window pointers). EXPERIMENTAL. The only resident 24×i4 lane. | `RelativeOffset` | +| Evidence (Tarski) | Probe only: `probe_four_plane_causal_medium.rs` (G24N4 as support/falsifier). | `Support` | +| Taxonomy axes | Fixture only: `entries/2026-10-04-coresearch-evidence-stance-dependence.md` (Mammal: `terrestrial`, `placental`). | `AxisPosition` | +| Episodic basin | `EpisodicBasin` tenant 15: 32 B of references, not i4. | none (not a signed family) | +| 32×i4 carrier | `atoms::I4x32`; now also the i4 carving's layout. | — | +| 16×i8 | `ndarray` SIMD `I8x16` only. | — | + +`CausalWitness` is NOT moved onto a register by this change: it is a Facet96 +lane with its own classid, and re-homing it is a separate decision. + +## Tests and disable runs + +Hotplug: `signed_rails_need_a_signed_carving_and_an_agreeing_law`; tag test now +accepts 2 and 3. Register128: round trip of every i4 value in every dim with the +nibble layout pinned; i8 dim-k-is-byte-k; the same bytes differ under the two +carvings; law mismatch (all 3×3 pairs, both carvings) refused and writes +nothing; carving mismatch refused; out-of-range i4 in the last dim refused +before writing; writes stay inside their rail. + +| disable | caught by | +|---|---| +| law check removed | `a_different_law_is_refused_and_writes_nothing` | +| carving check removed | `a_different_carving_is_refused` | +| rail check removed | `signed_writes_stay_inside_their_rail` | +| i4 range check removed | `an_out_of_range_i4_is_refused_before_writing` | +| binding accepts `Register128` as i4 | `signed_rails_need_a_signed_carving_and_an_agreeing_law` | +| binding swaps the carvings | same | +| binding skips the schema check | same | +| law mismatch check removed | `signed_rails_need_a_signed_carving_and_an_agreeing_law` | +| concept law ignored (slab law used) | same | +| `to_tag` shifts the I8x16 tags | `an_unsupported_physical_reading_fails_closed` | +| `from_tag` drops the law | same | + +## OPEN + +- No OGAR authority populates `with_register_laws` yet (`lance-graph-ogar` + builds its `Activation` without laws), so in production every concept still + refuses to bind signed registers. That is fail-closed, not a gap in the check. +- Lane or reading per family is undecided until measured: 32×i4 vs 16×i8 on + KJV anaphora and the mammal fixture. The rule "outside the local window → + basin edge" is not changed by `RelativeOffset` under `I8x16`. +- No production writer or reader. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 4d72ca7c0..d1a137647 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,10 +25,11 @@ 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. -270 entries, 2026-08-06 .. 2026-10-08. +271 entries, 2026-08-06 .. 2026-10-08. | date | entry id | finding | file | |---|---|---|---| +| 2026-10-08 | `register128-signed-readings` | | [2026-10-08-register128-signed-readings.md](2026-10-08-register128-signed-readings.md) | | 2026-10-08 | `rbac-nested-scope-path` | | [2026-10-08-rbac-nested-scope-path.md](2026-10-08-rbac-nested-scope-path.md) | | 2026-10-08 | `rbac-membership-scope` | | [2026-10-08-rbac-membership-scope.md](2026-10-08-rbac-membership-scope.md) | | 2026-10-08 | `rbac-hotplug-socket` | | [2026-10-08-rbac-hotplug-socket.md](2026-10-08-rbac-hotplug-socket.md) | diff --git a/.claude/v3/soa_layout/tenants.md b/.claude/v3/soa_layout/tenants.md index 080661fa7..1ff465246 100644 --- a/.claude/v3/soa_layout/tenants.md +++ b/.claude/v3/soa_layout/tenants.md @@ -57,7 +57,7 @@ Offsets are FULL-ROW; subtract 32 for slab-relative. | 14 | CausalWitness | U8 × 16 | 16 B | [204,220) | CausalWitness facet lane — 16 B content-blind V3 4+12 facet read as **G24N4** (24 signed i4 loci, a lane shape name, never a `CascadeShape` variant); each nibble is a context pointer (signed ±8 window offset), not a strength. Slots 16..24 reserved-zero. **Status: EXPERIMENTAL — not in the operator-locked §3 catalogue** (per its own doc-comment) | | 15 | EpisodicBasin | U8 × 32 | 32 B | [220,252) | **D-ACR-6 rail** — a promoted basin as REFERENCES: `subject` u16 · `member_count` u16 · `self_code` 12 B (Cam96 centroid) · `version_from`/`version_to` u64. Members are reached by following `(subject, [from,to))` into the triple stream, never inlined — the fat-concept guard `§3a` names. Width is NOT stored (recomputable through the references) | | 16 | Register0 | U8 × 16 | 16 B | [252,268) | **D-LXC-29** 128-bit working register, NO classid in the payload; read as `Register128` only when the slab declares `SlabReading::Register128` (`ResolvedReading::bind_register128` refuses otherwise). All-zero = empty accumulator | -| 17 | Register1 | U8 × 16 | 16 B | [268,284) | Second independent `Register128` rail, same contract as Register0 | +| 17 | Register1 | U8 × 16 | 16 B | [268,284) | Second independent `Register128` rail, same contract as Register0. Both rails can instead be declared signed: `SlabReading::RegisterI4x32(law)` / `RegisterI8x16(law)`, bound by `bind_signed_register`, which requires the slab's recorded `RegisterLaw` to equal the concept's (2026-10-08) | | 18 | Nars16x8 | U16 × 8 (LE) | 16 B | [284,300) | One `u16` per Moore slot (`NW,N,NE,W,E,SW,S,SE`). Layout RATIFIED; the `u16` reading is CANDIDATE and NOT declared by this tenant. Never shares bytes or a reading with MooreNars16 | | 19 | MoorePalettePairs | U8 × 16 | 16 B | [300,316) | One Palette256 `(u8:u8)` pair per Moore slot: byte `2i` = first operand, `2i+1` = second, addressing a 256×256 law as `(first << 8) \| second` (the #1336 `PairAddress` orientation). Two separate bytes, never one widened `u16` | | 20 | MooreNars16 | U16 × 8 (LE) | 16 B | [316,332) | One `MooreNars16` per Moore slot: `Pearl3 \| Energy4 \| Plasticity3 \| Polarity1 \| Epi5` (bits 0..2, 3..6, 7..9, 10, 11..15). Direction = `(slot, polarity)`, never the S/P/O sign triple (`DirectionReading::require_sign_triple` refuses it); one witness for all eight lanes, held by the owner, not in these bytes (`MooreTenantMut::lift_ce64` refuses mixed W). Epi5 24..31 reserved. Layout RATIFIED, semantics CANDIDATE | diff --git a/crates/lance-graph-contract/src/atoms.rs b/crates/lance-graph-contract/src/atoms.rs index 285ee4059..6799d8e96 100644 --- a/crates/lance-graph-contract/src/atoms.rs +++ b/crates/lance-graph-contract/src/atoms.rs @@ -79,6 +79,19 @@ impl I4x32 { /// The all-zero style vector (every lane neutral). pub const ZERO: Self = Self { bytes: [0u8; 16] }; + /// Wrap 16 already-packed bytes. No conversion: dim `2k` is the low + /// nibble of byte `k`, dim `2k+1` the high nibble. + #[must_use] + pub const fn from_bytes(bytes: [u8; 16]) -> Self { + Self { bytes } + } + + /// The 16 packed bytes, in the layout [`from_bytes`](Self::from_bytes) takes. + #[must_use] + pub const fn as_bytes(&self) -> &[u8; 16] { + &self.bytes + } + /// Pack 32 signed dims into the i4 CAM carrier, saturating to `[−8, 7]`. /// /// The carrier is a **sparse, deterministic 32×CAM address** (128-bit) — the non-zero diff --git a/crates/lance-graph-contract/src/hotplug.rs b/crates/lance-graph-contract/src/hotplug.rs index 2f649e162..e5a494d44 100644 --- a/crates/lance-graph-contract/src/hotplug.rs +++ b/crates/lance-graph-contract/src/hotplug.rs @@ -129,6 +129,11 @@ pub struct Activation { /// now guarantees is that *asking* for an absent one bangs instead of /// quietly yielding a V1 tail. read_modes: Vec<(u16, crate::canonical_node::ReadMode)>, + /// `(concept id, RegisterLaw)` — what a signed register population of + /// each concept means. Empty unless the authority declares laws with + /// [`with_register_laws`](Activation::with_register_laws); a concept + /// with no declared law cannot bind signed registers at all. + register_laws: Vec<(u16, crate::register128::RegisterLaw)>, } impl Activation { @@ -145,9 +150,36 @@ impl Activation { concepts, capabilities, read_modes, + register_laws: Vec::new(), } } + /// Declare the register law of each concept. The authority side of + /// [`ResolvedReading::bind_signed_register`]: the binder never chooses a + /// law, it is resolved here and checked against the law the slab + /// recorded. + #[must_use] + pub fn with_register_laws(mut self, laws: Vec<(u16, crate::register128::RegisterLaw)>) -> Self { + self.register_laws = laws; + self + } + + /// The register law declared for `concept`. Fails closed, like + /// [`read_mode_for`](Activation::read_mode_for): there is no default law. + /// + /// # Errors + /// + /// [`ActivationDrift::NoRegisterLawFor`] if `concept` has no declared law. + pub fn register_law_for( + &self, + concept: u16, + ) -> Result { + self.register_laws + .iter() + .find_map(|(c, l)| (*c == concept).then_some(*l)) + .ok_or(ActivationDrift::NoRegisterLawFor(concept)) + } + /// The reading for one hot-plugged concept — the ONLY lookup, and it /// fails closed. /// @@ -188,9 +220,9 @@ impl Activation { /// A physical reading a slab declares for itself. /// -/// The variants are exactly the readings THIS build implements. A future -/// reading (a classid-free 128-bit register and its carvings) lands as a new -/// variant; which slabs use it is decided by the slab's own declaration, never +/// The variants are exactly the readings THIS build implements. A new +/// reading (as the classid-free 128-bit register and its signed carvings +/// did) lands as a new variant; which slabs use it is decided by the slab's own declaration, never /// by a per-concept table. The on-wire tag is a `u8` in the slab's metadata /// envelope, decoded by [`SlabReading::from_tag`], which refuses any tag this /// build does not implement. @@ -211,6 +243,18 @@ pub enum SlabReading { /// [`ResolvedReading::bind_register128`] requires before granting the /// register rails. Register128 = 1, + /// The same register rails, written as 32 signed `i4` values: dim `2k` + /// is the low nibble of byte `k`, dim `2k+1` the high nibble (the + /// [`crate::atoms::I4x32`] layout), under the recorded + /// [`RegisterLaw`](crate::register128::RegisterLaw): the law the writer + /// actually wrote with. Bound by + /// [`ResolvedReading::bind_signed_register`], never by + /// [`ResolvedReading::bind_register128`]. + RegisterI4x32(crate::register128::RegisterLaw), + /// The same register rails, written as 16 signed `i8` values, one per + /// byte, under the recorded law. Bound by + /// [`ResolvedReading::bind_signed_register`]. + RegisterI8x16(crate::register128::RegisterLaw), } impl SlabReading { @@ -224,9 +268,47 @@ impl SlabReading { match tag { 0 => Ok(SlabReading::Facet96), 1 => Ok(SlabReading::Register128), + 2..=7 => { + let carving = (tag - 2) / 3; + let law = match (tag - 2) % 3 { + 0 => crate::register128::RegisterLaw::RelativeOffset, + 1 => crate::register128::RegisterLaw::AxisPosition, + _ => crate::register128::RegisterLaw::Support, + }; + if carving == 0 { + Ok(SlabReading::RegisterI4x32(law)) + } else { + Ok(SlabReading::RegisterI8x16(law)) + } + } other => Err(ActivationDrift::UnknownSlabReading(other)), } } + + /// The envelope tag, the inverse of [`from_tag`](Self::from_tag). + /// + /// | tag | reading | + /// |---|---| + /// | 0 | `Facet96` | + /// | 1 | `Register128` | + /// | 2, 3, 4 | `RegisterI4x32` with `RelativeOffset`, `AxisPosition`, `Support` | + /// | 5, 6, 7 | `RegisterI8x16` with `RelativeOffset`, `AxisPosition`, `Support` | + #[must_use] + pub const fn to_tag(self) -> u8 { + const fn law_index(law: crate::register128::RegisterLaw) -> u8 { + match law { + crate::register128::RegisterLaw::RelativeOffset => 0, + crate::register128::RegisterLaw::AxisPosition => 1, + crate::register128::RegisterLaw::Support => 2, + } + } + match self { + SlabReading::Facet96 => 0, + SlabReading::Register128 => 1, + SlabReading::RegisterI4x32(law) => 2 + law_index(law), + SlabReading::RegisterI8x16(law) => 5 + law_index(law), + } + } } /// What a slab's metadata envelope DECLARES about its own, already written @@ -369,6 +451,74 @@ impl ResolvedReading { } Ok(crate::register128::RegisterLanes::new(self.concept, rails)) } + + /// Bind the register rails of this population as SIGNED values. Checked + /// ONCE per population, like [`bind_register128`](Self::bind_register128). + /// + /// The carving and the law each have one authority, and the binder + /// chooses neither: + /// - the slab declares the carving AND the law its bytes were written + /// under ([`SlabReading::RegisterI4x32`] / [`SlabReading::RegisterI8x16`]); + /// - `activation` declares the law of this concept + /// ([`Activation::register_law_for`]). + /// + /// The two must agree. A writer and a reader that bind the same + /// population therefore always hold the same law, and every read and + /// write on the returned lanes checks the law it is asked for against it. + /// + /// # Errors + /// + /// - [`ActivationDrift::NotSignedRegister`] when the slab declared any + /// other reading, including the unsigned [`SlabReading::Register128`], + /// or nothing. + /// - [`ActivationDrift::NoRegisterLawFor`] when the concept has no + /// declared law. + /// - [`ActivationDrift::RegisterLawMismatch`] when the slab's recorded law + /// differs from the concept's. + /// - [`ActivationDrift::RegisterRailAbsent`] when the value schema does + /// not materialise a requested rail. + pub fn bind_signed_register( + &self, + activation: &Activation, + rails: crate::register128::RegisterRails, + ) -> Result { + let (carving, slab_law) = match self.slab { + Some(SlabReading::RegisterI4x32(law)) => { + (crate::register128::RegisterCarving::I4x32, law) + } + Some(SlabReading::RegisterI8x16(law)) => { + (crate::register128::RegisterCarving::I8x16, law) + } + other => { + return Err(ActivationDrift::NotSignedRegister { + concept: self.concept, + slab: other, + }) + } + }; + let concept_law = activation.register_law_for(self.concept)?; + if concept_law != slab_law { + return Err(ActivationDrift::RegisterLawMismatch { + concept: self.concept, + concept_law, + slab_law, + }); + } + 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::SignedRegisterLanes::new( + self.concept, + rails, + carving, + concept_law, + )) + } } /// Why an activation failed — each arm is one named bang. @@ -407,6 +557,29 @@ pub enum ActivationDrift { /// What the slab declared instead (`None`: nothing). slab: Option, }, + /// [`ResolvedReading::bind_signed_register`] was asked for signed + /// register rails of a population whose slab did not declare a signed + /// carving ([`SlabReading::RegisterI4x32`] or + /// [`SlabReading::RegisterI8x16`]). + NotSignedRegister { + /// The concept the reading was resolved under. + concept: u16, + /// What the slab declared instead (`None`: nothing). + slab: Option, + }, + /// The authority declared no register law for this concept, so its + /// signed registers cannot be bound. + NoRegisterLawFor(u16), + /// The law a slab recorded for its signed registers differs from the + /// law the authority declares for the concept. + RegisterLawMismatch { + /// The concept the reading was resolved under. + concept: u16, + /// The law the authority declares. + concept_law: crate::register128::RegisterLaw, + /// The law the slab recorded. + slab_law: crate::register128::RegisterLaw, + }, /// The resolved value schema does not materialise a requested register /// rail. RegisterRailAbsent { @@ -499,6 +672,23 @@ impl core::fmt::Display for ActivationDrift { "concept 0x{concept:04X}: slab declares {slab:?}, not Register128; \ no register rails are granted" ), + Self::NotSignedRegister { concept, slab } => write!( + f, + "concept 0x{concept:04X}: slab declares {slab:?}, not a signed register \ + carving; no signed register rails are granted" + ), + Self::NoRegisterLawFor(concept) => write!( + f, + "concept 0x{concept:04X}: no register law declared; signed registers are not bound" + ), + Self::RegisterLawMismatch { + concept, + concept_law, + slab_law, + } => write!( + f, + "concept 0x{concept:04X}: slab recorded {slab_law:?}, concept declares {concept_law:?}" + ), Self::RegisterRailAbsent { concept, tenant } => write!( f, "concept 0x{concept:04X}: value schema does not materialise register tenant {tenant}" @@ -749,7 +939,23 @@ mod tests { fn an_unsupported_physical_reading_fails_closed() { assert_eq!(SlabReading::from_tag(0), Ok(SlabReading::Facet96)); assert_eq!(SlabReading::from_tag(1), Ok(SlabReading::Register128)); - for tag in [2u8, 3, 0x80, 0xFF] { + for tag in 0u8..=7 { + let r = SlabReading::from_tag(tag).unwrap(); + assert_eq!(r.to_tag(), tag, "{r:?}"); + } + assert_eq!( + SlabReading::from_tag(3), + Ok(SlabReading::RegisterI4x32( + crate::register128::RegisterLaw::AxisPosition + )) + ); + assert_eq!( + SlabReading::from_tag(7), + Ok(SlabReading::RegisterI8x16( + crate::register128::RegisterLaw::Support + )) + ); + for tag in [8u8, 9, 0x80, 0xFF] { assert_eq!( SlabReading::from_tag(tag), Err(ActivationDrift::UnknownSlabReading(tag)) @@ -937,6 +1143,108 @@ mod tests { ); } + /// FAILS IF: signed rails are granted to a slab without a signed + /// carving, the carving or law is taken from anywhere but the slab + /// and the authority, a slab law that differs from the concept's is + /// accepted, a concept without a law binds, or a rail the schema + /// lacks is granted. + #[test] + fn signed_rails_need_a_signed_carving_and_an_agreeing_law() { + use crate::canonical_node::ValueTenant; + use crate::register128::{RegisterCarving, RegisterLaw, RegisterRails}; + const LAWS: [RegisterLaw; 3] = [ + RegisterLaw::RelativeOffset, + RegisterLaw::AxisPosition, + RegisterLaw::Support, + ]; + let carved = |reading, value_schema| SlabDeclaration { + reading, + value_schema, + layout_version: ENVELOPE_LAYOUT_VERSION, + }; + for concept_law in LAWS { + // 0x0901 declares `concept_law`; 0x0902 declares nothing. + let a = act().with_register_laws(vec![(0x0901, concept_law)]); + for slab_law in LAWS { + for (reading, carving) in [ + (SlabReading::RegisterI4x32(slab_law), RegisterCarving::I4x32), + (SlabReading::RegisterI8x16(slab_law), RegisterCarving::I8x16), + ] { + let r = a + .resolve_for_context(0x0901, Some(&carved(reading, ValueSchema::Full))) + .unwrap(); + let bound = r.bind_signed_register(&a, RegisterRails::Two); + if slab_law == concept_law { + let lanes = bound.unwrap(); + assert_eq!(lanes.carving(), carving); + assert_eq!(lanes.law(), concept_law); + assert_eq!(lanes.concept(), 0x0901); + } else { + assert_eq!( + bound, + Err(ActivationDrift::RegisterLawMismatch { + concept: 0x0901, + concept_law, + slab_law, + }) + ); + } + assert_eq!( + r.bind_register128(RegisterRails::One), + Err(ActivationDrift::NotRegister128 { + concept: 0x0901, + slab: Some(reading) + }), + "a signed slab is not an unsigned word register" + ); + let lawless = a + .resolve_for_context(0x0902, Some(&carved(reading, ValueSchema::Full))) + .unwrap(); + assert_eq!( + lawless.bind_signed_register(&a, RegisterRails::One), + Err(ActivationDrift::NoRegisterLawFor(0x0902)) + ); + } + } + let narrow = a + .resolve_for_context( + 0x0901, + Some(&carved( + SlabReading::RegisterI8x16(concept_law), + ValueSchema::Cognitive, + )), + ) + .unwrap(); + assert_eq!( + narrow.bind_signed_register(&a, RegisterRails::One), + Err(ActivationDrift::RegisterRailAbsent { + concept: 0x0901, + tenant: ValueTenant::Register0 as u8, + }) + ); + let words = a + .resolve_for_context(0x0901, Some(®_decl(ValueSchema::Full))) + .unwrap(); + 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 [ + (words, Some(SlabReading::Register128)), + (facet, Some(SlabReading::Facet96)), + (undeclared, None), + ] { + assert_eq!( + r.bind_signed_register(&a, RegisterRails::One), + Err(ActivationDrift::NotSignedRegister { + concept: 0x0901, + slab + }) + ); + } + } + } + /// FAILS IF: a register's semantic identity is read from its bytes. /// /// The same register payload — here deliberately holding the OTHER diff --git a/crates/lance-graph-contract/src/register128.rs b/crates/lance-graph-contract/src/register128.rs index 007197646..74f67517a 100644 --- a/crates/lance-graph-contract/src/register128.rs +++ b/crates/lance-graph-contract/src/register128.rs @@ -181,8 +181,479 @@ impl RegisterLanes { } } +/// How the bytes of a signed register were written. Declared by the slab +/// ([`SlabReading::RegisterI4x32`](crate::hotplug::SlabReading::RegisterI4x32), +/// [`SlabReading::RegisterI8x16`](crate::hotplug::SlabReading::RegisterI8x16)), +/// never chosen by a reader. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RegisterCarving { + /// 32 signed `i4`, range `-8..=7`. Dim `2k` is the low nibble of byte + /// `k`, dim `2k+1` the high nibble ([`crate::atoms::I4x32`]). + I4x32, + /// 16 signed `i8`, range `-128..=127`, dim `k` in byte `k`. + I8x16, +} + +impl RegisterCarving { + /// Number of dimensions in one register. + #[must_use] + pub const fn dims(self) -> usize { + match self { + Self::I4x32 => 32, + Self::I8x16 => 16, + } + } +} + +/// What the signed values of a bound population mean. Two authorities, which +/// must agree: the concept's law, declared by the +/// [`Activation`](crate::hotplug::Activation) +/// ([`register_law_for`](crate::hotplug::Activation::register_law_for)), and the +/// law the slab recorded when its bytes were written +/// ([`SlabReading::RegisterI4x32`](crate::hotplug::SlabReading::RegisterI4x32)). +/// The binder chooses neither. Checked again on every read and write. +/// +/// Each law is a different kind of quantity, so a value written under one +/// is not a value under another even when the bytes are identical. That is +/// the mixing an earlier 24×i4 accumulation got wrong, and why no law here +/// converts into another. +/// +/// Epistemic state is deliberately absent: it is defined on `CausalEdge64` +/// bits 59..63 (`EpistemicState5`) and is not re-encoded as a signed value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RegisterLaw { + /// A relative address in a local window: `-3` means "three positions + /// back". An address, not a strength. A referent outside the window is + /// not encoded here; it is reached through a basin or graph edge. + RelativeOffset, + /// A signed position on a declared semantic axis (e.g. `terrestrial`: + /// fox `+7`, whale `-5`). Independent of the `is_a` taxonomy: a negative + /// position never removes a class membership. + AxisPosition, + /// Evidence stance: positive supports, zero is unresolved, negative + /// falsifies. + Support, +} + +/// Why a signed register read or write was refused. A refused write leaves +/// the row unchanged. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SignedRegisterRefusal { + /// The binding did not grant this rail. + RailAbsent { + /// The rail asked for. + rail: usize, + }, + /// The caller asked for a different carving than the slab declared. + CarvingMismatch { + /// The carving the slab declared. + bound: RegisterCarving, + /// The carving the caller asked for. + asked: RegisterCarving, + }, + /// The caller asked for a different law than the binding declared. + LawMismatch { + /// The law the population was bound under. + bound: RegisterLaw, + /// The law the caller asked for. + asked: RegisterLaw, + }, + /// A value does not fit the carving (an `i4` outside `-8..=7`). + OutOfRange { + /// The dimension holding it. + dim: usize, + /// The value. + value: i8, + }, +} + +/// The signed register rails of one bound population, produced ONCE by +/// [`ResolvedReading::bind_signed_register`](crate::hotplug::ResolvedReading::bind_signed_register). +/// +/// Carries the concept, the granted rails, the slab's carving and the +/// declared law. Reads decode one rail into an owned array of 16 or 32 +/// values; nothing else is copied. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct SignedRegisterLanes { + concept: u16, + rails: RegisterRails, + carving: RegisterCarving, + law: RegisterLaw, +} + +impl SignedRegisterLanes { + /// Built only by the binding, which has already checked the slab. + pub(crate) const fn new( + concept: u16, + rails: RegisterRails, + carving: RegisterCarving, + law: RegisterLaw, + ) -> Self { + Self { + concept, + rails, + carving, + law, + } + } + + /// The SPOG concept this population was resolved under. + #[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 carving the slab declared. + #[must_use] + pub const fn carving(&self) -> RegisterCarving { + self.carving + } + + /// The law this population was bound under. + #[must_use] + pub const fn law(&self) -> RegisterLaw { + self.law + } + + /// The value-slab byte range of `rail`, after checking rail, carving and + /// law in that order. + fn check( + &self, + rail: usize, + carving: RegisterCarving, + law: RegisterLaw, + ) -> Result, SignedRegisterRefusal> { + let tenants = self.rails.tenants(); + if rail >= tenants.len() { + return Err(SignedRegisterRefusal::RailAbsent { rail }); + } + if carving != self.carving { + return Err(SignedRegisterRefusal::CarvingMismatch { + bound: self.carving, + asked: carving, + }); + } + if law != self.law { + return Err(SignedRegisterRefusal::LawMismatch { + bound: self.law, + asked: law, + }); + } + let start = tenants[rail].value_offset(); + Ok(start..start + REGISTER_BYTES) + } + + /// Read `rail` as 32 signed `i4` under `law`. + /// + /// # Errors + /// + /// [`SignedRegisterRefusal`] when the rail is absent, the slab is not + /// carved as [`RegisterCarving::I4x32`], or `law` is not the bound law. + pub fn read_i4x32( + &self, + row: &NodeRow, + rail: usize, + law: RegisterLaw, + ) -> Result<[i8; 32], SignedRegisterRefusal> { + let r = self.check(rail, RegisterCarving::I4x32, law)?; + let mut b = [0u8; REGISTER_BYTES]; + b.copy_from_slice(&row.value[r]); + Ok(crate::atoms::I4x32::from_bytes(b).unpack()) + } + + /// Read `rail` as 16 signed `i8` under `law`. + /// + /// # Errors + /// + /// As [`read_i4x32`](Self::read_i4x32), for [`RegisterCarving::I8x16`]. + pub fn read_i8x16( + &self, + row: &NodeRow, + rail: usize, + law: RegisterLaw, + ) -> Result<[i8; 16], SignedRegisterRefusal> { + let r = self.check(rail, RegisterCarving::I8x16, law)?; + let bytes = &row.value[r]; + Ok(core::array::from_fn(|i| bytes[i] as i8)) + } + + /// Write `rail` as 32 signed `i4` under `law`. Every value must lie in + /// `-8..=7`; nothing is saturated, and nothing is written unless all + /// checks pass. + /// + /// # Errors + /// + /// As [`read_i4x32`](Self::read_i4x32), plus + /// [`SignedRegisterRefusal::OutOfRange`]. + pub fn write_i4x32( + &self, + row: &mut NodeRow, + rail: usize, + law: RegisterLaw, + values: &[i8; 32], + ) -> Result<(), SignedRegisterRefusal> { + let r = self.check(rail, RegisterCarving::I4x32, law)?; + if let Some((dim, &value)) = values + .iter() + .enumerate() + .find(|(_, v)| !(-8..=7).contains(*v)) + { + return Err(SignedRegisterRefusal::OutOfRange { dim, value }); + } + let packed = crate::atoms::I4x32::pack(values); + row.value[r].copy_from_slice(packed.as_bytes()); + crate::tenant_counter::tenant_update(self.rails.tenants()[rail]); + Ok(()) + } + + /// Write `rail` as 16 signed `i8` under `law`. + /// + /// # Errors + /// + /// As [`read_i8x16`](Self::read_i8x16). Nothing is written on refusal. + pub fn write_i8x16( + &self, + row: &mut NodeRow, + rail: usize, + law: RegisterLaw, + values: &[i8; 16], + ) -> Result<(), SignedRegisterRefusal> { + let r = self.check(rail, RegisterCarving::I8x16, law)?; + for (dst, &v) in row.value[r].iter_mut().zip(values.iter()) { + *dst = v as u8; + } + crate::tenant_counter::tenant_update(self.rails.tenants()[rail]); + Ok(()) + } +} + #[cfg(test)] mod tests { + fn patterned_row() -> NodeRow { + NodeRow { + key: crate::canonical_node::NodeGuid::new(0x0901_0000, 1, 2, 3, 0x66, 11), + edges: Default::default(), + value: core::array::from_fn(|i| (i % 251) as u8), + } + } + + const LAWS: [RegisterLaw; 3] = [ + RegisterLaw::RelativeOffset, + RegisterLaw::AxisPosition, + RegisterLaw::Support, + ]; + + /// FAILS IF: an i4 value does not round-trip, or the nibble layout + /// differs from `atoms::I4x32` (dim 2k low nibble of byte k). + #[test] + fn i4x32_round_trips_every_value_in_every_dim() { + let lanes = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I4x32, + RegisterLaw::AxisPosition, + ); + for v in -8i8..=7 { + for dim in 0..32 { + let mut values = [0i8; 32]; + values[dim] = v; + values[(dim + 1) % 32] = -v.clamp(-7, 7); + let mut row = patterned_row(); + lanes + .write_i4x32(&mut row, 1, RegisterLaw::AxisPosition, &values) + .unwrap(); + assert_eq!( + lanes.read_i4x32(&row, 1, RegisterLaw::AxisPosition), + Ok(values) + ); + let r = lanes + .check(1, RegisterCarving::I4x32, RegisterLaw::AxisPosition) + .unwrap(); + let byte = row.value[r.start + dim / 2]; + let nibble = if dim % 2 == 0 { byte & 0xF } else { byte >> 4 }; + assert_eq!(nibble, (v as u8) & 0xF, "dim {dim} nibble"); + } + } + } + + /// FAILS IF: an i8 value does not round-trip, or dim k is not byte k. + #[test] + fn i8x16_round_trips_and_dim_k_is_byte_k() { + let lanes = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I8x16, + RegisterLaw::RelativeOffset, + ); + for base in [-128i8, -100, -1, 0, 1, 77, 127] { + let values: [i8; 16] = + core::array::from_fn(|k| base.wrapping_add((k as i8).wrapping_mul(17))); + let mut row = patterned_row(); + lanes + .write_i8x16(&mut row, 1, RegisterLaw::RelativeOffset, &values) + .unwrap(); + assert_eq!( + lanes.read_i8x16(&row, 1, RegisterLaw::RelativeOffset), + Ok(values) + ); + let r = lanes.rails.tenants()[1].value_offset(); + for (k, &v) in values.iter().enumerate() { + assert_eq!(row.value[r + k] as i8, v); + } + } + } + + /// FAILS IF: the carving is not load-bearing: the same 16 bytes must + /// mean different values under the two carvings. + #[test] + fn the_same_bytes_differ_under_the_two_carvings() { + let mut row = patterned_row(); + let i8s = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I8x16, + RegisterLaw::Support, + ); + let i4s = SignedRegisterLanes { + carving: RegisterCarving::I4x32, + ..i8s + }; + let mut v8 = [0i8; 16]; + v8[0] = 0xF1u8 as i8; // -15 as i8; nibbles (+1, -1) as i4 + i8s.write_i8x16(&mut row, 1, RegisterLaw::Support, &v8) + .unwrap(); + let as4 = i4s.read_i4x32(&row, 1, RegisterLaw::Support).unwrap(); + assert_eq!(v8[0], -15); + assert_eq!((as4[0], as4[1]), (1, -1)); + } + + /// FAILS IF: a read or write under a law other than the bound one is + /// accepted, or a refused write changes the row. + #[test] + fn a_different_law_is_refused_and_writes_nothing() { + for bound in LAWS { + for asked in LAWS { + for carving in [RegisterCarving::I4x32, RegisterCarving::I8x16] { + let lanes = + SignedRegisterLanes::new(0x0901, RegisterRails::Two, carving, bound); + let mut row = patterned_row(); + let before = row.value; + let (read_ok, write) = match carving { + RegisterCarving::I4x32 => ( + lanes.read_i4x32(&row, 1, asked).is_ok(), + lanes.write_i4x32(&mut row, 1, asked, &[3; 32]), + ), + RegisterCarving::I8x16 => ( + lanes.read_i8x16(&row, 1, asked).is_ok(), + lanes.write_i8x16(&mut row, 1, asked, &[3; 16]), + ), + }; + if asked == bound { + assert!(read_ok && write.is_ok()); + } else { + assert!(!read_ok); + assert_eq!( + write, + Err(SignedRegisterRefusal::LawMismatch { bound, asked }) + ); + assert_eq!(row.value, before, "a refused write wrote"); + } + } + } + } + } + + /// FAILS IF: a reader can choose a carving other than the slab's. + #[test] + fn a_different_carving_is_refused() { + let lanes = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I4x32, + RegisterLaw::Support, + ); + let mut row = patterned_row(); + let before = row.value; + let refused = Err(SignedRegisterRefusal::CarvingMismatch { + bound: RegisterCarving::I4x32, + asked: RegisterCarving::I8x16, + }); + assert_eq!( + lanes.read_i8x16(&row, 1, RegisterLaw::Support).map(|_| ()), + refused + ); + assert_eq!( + lanes.write_i8x16(&mut row, 1, RegisterLaw::Support, &[1; 16]), + refused + ); + assert_eq!(row.value, before); + } + + /// FAILS IF: an out-of-range i4 is saturated or partly written instead + /// of refused. The bad value is in the LAST dim so a write that checks + /// as it goes would already have written the others. + #[test] + fn an_out_of_range_i4_is_refused_before_writing() { + let lanes = SignedRegisterLanes::new( + 0x0901, + RegisterRails::Two, + RegisterCarving::I4x32, + RegisterLaw::AxisPosition, + ); + for bad in [8i8, 9, 127, -9, -128] { + let mut values = [5i8; 32]; + values[31] = bad; + let mut row = patterned_row(); + let before = row.value; + assert_eq!( + lanes.write_i4x32(&mut row, 1, RegisterLaw::AxisPosition, &values), + Err(SignedRegisterRefusal::OutOfRange { + dim: 31, + value: bad + }) + ); + assert_eq!(row.value, before); + } + } + + /// FAILS IF: a one-rail binding reaches rail 1, or a write touches any + /// byte outside its rail. + #[test] + fn signed_writes_stay_inside_their_rail() { + let one = SignedRegisterLanes::new( + 0x0901, + RegisterRails::One, + RegisterCarving::I8x16, + RegisterLaw::Support, + ); + let mut row = patterned_row(); + assert_eq!( + one.write_i8x16(&mut row, 1, RegisterLaw::Support, &[1; 16]), + Err(SignedRegisterRefusal::RailAbsent { rail: 1 }) + ); + let two = SignedRegisterLanes { + rails: RegisterRails::Two, + ..one + }; + let before = row.value; + two.write_i8x16(&mut row, 1, RegisterLaw::Support, &[-1; 16]) + .unwrap(); + let r = two + .check(1, RegisterCarving::I8x16, RegisterLaw::Support) + .unwrap(); + for (i, (a, b)) in before.iter().zip(row.value.iter()).enumerate() { + if !r.contains(&i) { + assert_eq!(a, b, "byte {i} outside rail 1 changed"); + } + } + } + use super::*; use crate::canonical_node::VALUE_TENANTS;