|
| 1 | +//! The **stored node** — where a function actually lives as bytes. |
| 2 | +//! |
| 3 | +//! # The claim this closes |
| 4 | +//! |
| 5 | +//! The surface rests on one sentence: *a program is V3 SoA rows, and the |
| 6 | +//! blocks/steps a user sees are a projection of those rows.* Every other |
| 7 | +//! layer — a block cast, a text projection, an editor address — is a |
| 8 | +//! projection **of** something. This module is the something. |
| 9 | +//! |
| 10 | +//! # The layout, and the slot that is deliberately empty |
| 11 | +//! |
| 12 | +//! ```text |
| 13 | +//! one function = one node = 512 bytes = 32 × 16-byte slots |
| 14 | +//! slot 0 key 16 B the canonical GUID |
| 15 | +//! slot 1 reserved 16 B ZEROED — the edge-block design is RETIRED |
| 16 | +//! slots 2..31 value slab 480 B 30 lanes × 12 B, carved by LaneShape |
| 17 | +//! ``` |
| 18 | +//! |
| 19 | +//! Slot 1 is **reserved, not reclaimed**. The edge-block design (12 in-family + |
| 20 | +//! 4 out-of-family slots) was retired, and the temptation is to hand its 16 |
| 21 | +//! bytes to the value slab and get two more calls. That is exactly the |
| 22 | +//! field-widening the substrate's canon forbids: capacity comes from the next |
| 23 | +//! cascade level, never from renegotiating this one. A zero tier means *not |
| 24 | +//! consulted*, never *compacted away* — so a later mint can wake slot 1 with no |
| 25 | +//! layout-version change, and [`FunctionNode::reserved_is_zeroed`] asserts it |
| 26 | +//! stayed empty. |
| 27 | +//! |
| 28 | +//! # The key is opaque here, on purpose |
| 29 | +//! |
| 30 | +//! [`FunctionNode::key`] is a caller-supplied `[u8; 16]`. This crate does |
| 31 | +//! **not** mint GUIDs: the canonical layout (classid · path tiers · tail) is |
| 32 | +//! the substrate's, each vocabulary's app prefix is an operator decision, and |
| 33 | +//! inventing either here would bake a guess into stored data. So the node |
| 34 | +//! round-trips a key it does not interpret, and what this module actually |
| 35 | +//! proves is the part it owns: **the value slab survives the trip |
| 36 | +//! byte-for-byte**. |
| 37 | +//! |
| 38 | +//! # Interleave, not concatenation |
| 39 | +//! |
| 40 | +//! The slab is **not** 360 body bytes followed by 120 spare. Each 16-byte lane |
| 41 | +//! is `classid(4) + payload(12)`, so a call's bytes sit at |
| 42 | +//! `(i / calls_per_lane) * 16 + 4 + (i % calls_per_lane) * bytes_per_call`. |
| 43 | +//! Writing the body as one contiguous run would produce a slab that looks |
| 44 | +//! plausible, reads back correctly through the same wrong function, and is |
| 45 | +//! wrong on the wire. [`FunctionBody::write_into_value_slab`] owns that |
| 46 | +//! arithmetic; this module composes it and tests the composition against the |
| 47 | +//! layout constants rather than against its own idea of them. |
| 48 | +
|
| 49 | +use crate::{FunctionBody, LaneShape, SLOT_STRIDE, VALUE_SLAB_LEN}; |
| 50 | + |
| 51 | +/// Bytes in one stored node. |
| 52 | +pub const NODE_BYTES: usize = 512; |
| 53 | + |
| 54 | +/// Byte offset of the key slot. |
| 55 | +pub const KEY_OFFSET: usize = 0; |
| 56 | +/// Bytes the key occupies (slot 0). |
| 57 | +pub const KEY_BYTES: usize = SLOT_STRIDE; |
| 58 | +/// Byte offset of the reserved slot (slot 1) — zeroed, never reclaimed. |
| 59 | +pub const RESERVED_OFFSET: usize = SLOT_STRIDE; |
| 60 | +/// Byte offset at which the value slab begins (slot 2). |
| 61 | +pub const VALUE_OFFSET: usize = 2 * SLOT_STRIDE; |
| 62 | + |
| 63 | +// The layout is derived, not asserted twice: if a stride or a slot count ever |
| 64 | +// changes, this fails to compile rather than silently storing a different |
| 65 | +// shape. |
| 66 | +const _: () = assert!(VALUE_OFFSET + VALUE_SLAB_LEN == NODE_BYTES); |
| 67 | +const _: () = assert!(KEY_BYTES == SLOT_STRIDE); |
| 68 | + |
| 69 | +/// One function, as it is stored. |
| 70 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 71 | +pub struct FunctionNode { |
| 72 | + /// The canonical GUID. **Opaque here** — this crate neither mints nor |
| 73 | + /// interprets it; see the module docs. |
| 74 | + pub key: [u8; 16], |
| 75 | + /// The calls. |
| 76 | + pub body: FunctionBody, |
| 77 | +} |
| 78 | + |
| 79 | +impl FunctionNode { |
| 80 | + /// A node holding `body` under `key`. |
| 81 | + #[must_use] |
| 82 | + pub fn new(key: [u8; 16], body: FunctionBody) -> Self { |
| 83 | + Self { key, body } |
| 84 | + } |
| 85 | + |
| 86 | + /// Serialize to the 512-byte block. |
| 87 | + /// |
| 88 | + /// `to_le_bytes` IS the wire format — no serde, no intermediate DTO (the |
| 89 | + /// no-serialization-in-the-hot-path rule). Slot 1 is written as zeroes. |
| 90 | + #[must_use] |
| 91 | + pub fn to_le_bytes(&self) -> [u8; NODE_BYTES] { |
| 92 | + let mut out = [0u8; NODE_BYTES]; |
| 93 | + out[KEY_OFFSET..KEY_OFFSET + KEY_BYTES].copy_from_slice(&self.key); |
| 94 | + // slot 1 stays zero — reserve, don't reclaim. |
| 95 | + let mut slab = [0u8; VALUE_SLAB_LEN]; |
| 96 | + self.body.write_into_value_slab(&mut slab); |
| 97 | + out[VALUE_OFFSET..].copy_from_slice(&slab); |
| 98 | + out |
| 99 | + } |
| 100 | + |
| 101 | + /// Read a node back. |
| 102 | + /// |
| 103 | + /// `shape` is not stored in the node: the lane carving is a property of the |
| 104 | + /// **class**, resolved through the key's classid, and duplicating it inside |
| 105 | + /// the value slab would be a second source of truth that could disagree |
| 106 | + /// with the first. The caller supplies what the ClassView says. |
| 107 | + #[must_use] |
| 108 | + pub fn from_le_bytes(bytes: &[u8; NODE_BYTES], shape: LaneShape) -> Self { |
| 109 | + let mut key = [0u8; 16]; |
| 110 | + key.copy_from_slice(&bytes[KEY_OFFSET..KEY_OFFSET + KEY_BYTES]); |
| 111 | + let mut slab = [0u8; VALUE_SLAB_LEN]; |
| 112 | + slab.copy_from_slice(&bytes[VALUE_OFFSET..]); |
| 113 | + Self { |
| 114 | + key, |
| 115 | + body: FunctionBody::read_from_value_slab(shape, &slab), |
| 116 | + } |
| 117 | + } |
| 118 | + |
| 119 | + /// Whether the reserved slot is still empty in a serialized node. |
| 120 | + /// |
| 121 | + /// The retired edge block's 16 bytes must stay zeroed so a later mint can |
| 122 | + /// wake them with no layout-version change. A caller that finds this false |
| 123 | + /// is looking at a node from a layout this code does not describe. |
| 124 | + #[must_use] |
| 125 | + pub fn reserved_is_zeroed(bytes: &[u8; NODE_BYTES]) -> bool { |
| 126 | + bytes[RESERVED_OFFSET..RESERVED_OFFSET + SLOT_STRIDE] |
| 127 | + .iter() |
| 128 | + .all(|b| *b == 0) |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +#[cfg(test)] |
| 133 | +mod tests { |
| 134 | + use super::*; |
| 135 | + use crate::{CLASSID_BYTES, Call, FnIndex}; |
| 136 | + |
| 137 | + fn key() -> [u8; 16] { |
| 138 | + // An arbitrary opaque key — this crate does not mint, so the test must |
| 139 | + // not model a minting scheme either. |
| 140 | + let mut k = [0u8; 16]; |
| 141 | + k[0..4].copy_from_slice(&0x1701_FF00_u32.to_le_bytes()); |
| 142 | + k[10..16].copy_from_slice(&[1, 2, 3, 4, 5, 6]); |
| 143 | + k |
| 144 | + } |
| 145 | + |
| 146 | + /// `1 + 2 * 3` under the stack discipline. |
| 147 | + fn expr_body(shape: LaneShape) -> FunctionBody { |
| 148 | + FunctionBody::from_calls( |
| 149 | + shape, |
| 150 | + &[ |
| 151 | + Call::with_value(FnIndex::NUMBER, 1), |
| 152 | + Call::with_value(FnIndex::NUMBER, 2), |
| 153 | + Call::with_value(FnIndex::NUMBER, 3), |
| 154 | + Call::new(FnIndex::MUL), |
| 155 | + Call::new(FnIndex::ADD), |
| 156 | + ], |
| 157 | + ) |
| 158 | + .unwrap() |
| 159 | + } |
| 160 | + |
| 161 | + #[test] |
| 162 | + fn a_stored_node_round_trips_byte_for_byte() { |
| 163 | + // THE claim: a program written to a row and read back is the same |
| 164 | + // program. Not "reads without error" — the same CALLS. |
| 165 | + let node = FunctionNode::new(key(), expr_body(LaneShape::Pairs)); |
| 166 | + let bytes = node.to_le_bytes(); |
| 167 | + let back = FunctionNode::from_le_bytes(&bytes, LaneShape::Pairs); |
| 168 | + |
| 169 | + assert_eq!(back.key, node.key); |
| 170 | + assert_eq!(back.body.len(), node.body.len()); |
| 171 | + assert_eq!(back.body.as_body_bytes(), node.body.as_body_bytes()); |
| 172 | + // …and serializing the recovered node reproduces the same 512 bytes, |
| 173 | + // which a lossy read would fail even if the calls happened to match. |
| 174 | + assert_eq!(back.to_le_bytes(), bytes); |
| 175 | + } |
| 176 | + |
| 177 | + #[test] |
| 178 | + fn a_different_program_stores_to_different_bytes() { |
| 179 | + // Anti-vacuity for the round-trip: a `to_le_bytes` that returned a |
| 180 | + // constant would round-trip perfectly and store nothing. |
| 181 | + let one_plus_two = FunctionBody::from_calls( |
| 182 | + LaneShape::Pairs, |
| 183 | + &[ |
| 184 | + Call::with_value(FnIndex::NUMBER, 1), |
| 185 | + Call::with_value(FnIndex::NUMBER, 2), |
| 186 | + Call::new(FnIndex::ADD), |
| 187 | + ], |
| 188 | + ) |
| 189 | + .unwrap(); |
| 190 | + let one_plus_three = FunctionBody::from_calls( |
| 191 | + LaneShape::Pairs, |
| 192 | + &[ |
| 193 | + Call::with_value(FnIndex::NUMBER, 1), |
| 194 | + Call::with_value(FnIndex::NUMBER, 3), |
| 195 | + Call::new(FnIndex::ADD), |
| 196 | + ], |
| 197 | + ) |
| 198 | + .unwrap(); |
| 199 | + let a = FunctionNode::new(key(), one_plus_two); |
| 200 | + let b = FunctionNode::new(key(), one_plus_three); |
| 201 | + assert_ne!(a.to_le_bytes(), b.to_le_bytes()); |
| 202 | + // …and the same program under a different KEY differs too, so the key |
| 203 | + // is genuinely stored rather than dropped. |
| 204 | + let mut k2 = key(); |
| 205 | + k2[15] = 99; |
| 206 | + let c = FunctionNode::new(k2, one_plus_two); |
| 207 | + assert_ne!(a.to_le_bytes(), c.to_le_bytes()); |
| 208 | + assert_eq!( |
| 209 | + FunctionNode::from_le_bytes(&c.to_le_bytes(), LaneShape::Pairs).key, |
| 210 | + k2 |
| 211 | + ); |
| 212 | + } |
| 213 | + |
| 214 | + #[test] |
| 215 | + fn the_reserved_slot_stays_zeroed() { |
| 216 | + // Reserve, don't reclaim. The retired edge block's 16 bytes are the |
| 217 | + // obvious place to steal two more calls from, and stealing them is the |
| 218 | + // field-widening the canon forbids. |
| 219 | + let node = FunctionNode::new(key(), expr_body(LaneShape::Pairs)); |
| 220 | + let bytes = node.to_le_bytes(); |
| 221 | + assert!(FunctionNode::reserved_is_zeroed(&bytes)); |
| 222 | + assert!(bytes[RESERVED_OFFSET..VALUE_OFFSET].iter().all(|b| *b == 0)); |
| 223 | + |
| 224 | + // Two-sided: the guard must be able to say NO, or "stayed zeroed" is |
| 225 | + // a function that returns true. |
| 226 | + let mut tampered = bytes; |
| 227 | + tampered[RESERVED_OFFSET + 7] = 1; |
| 228 | + assert!(!FunctionNode::reserved_is_zeroed(&tampered)); |
| 229 | + } |
| 230 | + |
| 231 | + #[test] |
| 232 | + fn the_body_is_interleaved_across_lanes_not_written_contiguously() { |
| 233 | + // The failure this catches is nasty precisely because it is |
| 234 | + // self-consistent: a contiguous write reads back fine through the same |
| 235 | + // wrong function, and is wrong on the wire. So the assertion is against |
| 236 | + // the LAYOUT — a call's bytes must land at its lane offset, past the |
| 237 | + // lane's 4 classid bytes. |
| 238 | + let node = FunctionNode::new(key(), expr_body(LaneShape::Pairs)); |
| 239 | + let bytes = node.to_le_bytes(); |
| 240 | + |
| 241 | + // First call (NUMBER:1) sits at lane 0, just past the classid. |
| 242 | + let first = VALUE_OFFSET + CLASSID_BYTES; |
| 243 | + assert_eq!(bytes[first], 0x46, "first call's function byte"); |
| 244 | + assert_eq!(bytes[first + 1], 1, "first call's value byte"); |
| 245 | + |
| 246 | + // Pairs packs 6 calls per 12-byte lane, so call 6 begins the SECOND |
| 247 | + // lane — 16 bytes on, not 12. A contiguous writer puts it at 12. |
| 248 | + let lane1 = VALUE_OFFSET + SLOT_STRIDE + CLASSID_BYTES; |
| 249 | + assert_eq!(lane1 - first, SLOT_STRIDE, "lanes are strided by 16"); |
| 250 | + // The classid gap really is a gap: those 4 bytes are untouched by the |
| 251 | + // body writer. |
| 252 | + let gap = VALUE_OFFSET + SLOT_STRIDE; |
| 253 | + assert!( |
| 254 | + bytes[gap..gap + CLASSID_BYTES].iter().all(|b| *b == 0), |
| 255 | + "the body must not write into a lane's classid bytes" |
| 256 | + ); |
| 257 | + } |
| 258 | + |
| 259 | + #[test] |
| 260 | + fn every_shape_round_trips_and_the_shape_is_not_stored() { |
| 261 | + // The shape is a class property resolved through the key, deliberately |
| 262 | + // not duplicated in the slab. So the same bytes read under a different |
| 263 | + // shape yield a DIFFERENT program — which is correct, and is why the |
| 264 | + // caller must supply what the ClassView says rather than guessing. |
| 265 | + for shape in [LaneShape::Pairs, LaneShape::Triples, LaneShape::Quads] { |
| 266 | + let node = FunctionNode::new(key(), expr_body(shape)); |
| 267 | + let bytes = node.to_le_bytes(); |
| 268 | + let back = FunctionNode::from_le_bytes(&bytes, shape); |
| 269 | + let a: Vec<Call> = back.body.calls().collect(); |
| 270 | + let b: Vec<Call> = node.body.calls().collect(); |
| 271 | + assert_eq!(a, b, "{shape:?} did not round-trip"); |
| 272 | + } |
| 273 | + // Two-sided: reading Pairs bytes as Quads must NOT silently agree, or |
| 274 | + // "the shape matters" is untested. |
| 275 | + let pairs = FunctionNode::new(key(), expr_body(LaneShape::Pairs)); |
| 276 | + let bytes = pairs.to_le_bytes(); |
| 277 | + let misread = FunctionNode::from_le_bytes(&bytes, LaneShape::Quads); |
| 278 | + let a: Vec<Call> = misread.body.calls().collect(); |
| 279 | + let b: Vec<Call> = pairs.body.calls().collect(); |
| 280 | + assert_ne!(a, b); |
| 281 | + } |
| 282 | + |
| 283 | + #[test] |
| 284 | + fn a_full_body_fills_the_slab_without_overrunning_the_node() { |
| 285 | + // The boundary: a program at exactly the shape's budget must still fit |
| 286 | + // in 512 bytes, and must not disturb the key or the reserved slot. |
| 287 | + let cap = LaneShape::Pairs.calls_per_function(); |
| 288 | + let calls: Vec<Call> = (0..cap) |
| 289 | + .map(|i| Call::with_value(FnIndex::NUMBER, (i % 250) as u8 + 1)) |
| 290 | + .collect(); |
| 291 | + let body = FunctionBody::from_calls(LaneShape::Pairs, &calls).unwrap(); |
| 292 | + assert_eq!(body.len(), cap); |
| 293 | + |
| 294 | + let node = FunctionNode::new(key(), body); |
| 295 | + let bytes = node.to_le_bytes(); |
| 296 | + assert_eq!(&bytes[..16], &key()); |
| 297 | + assert!(FunctionNode::reserved_is_zeroed(&bytes)); |
| 298 | + let back = FunctionNode::from_le_bytes(&bytes, LaneShape::Pairs); |
| 299 | + assert_eq!(back.body.len(), cap); |
| 300 | + assert_eq!(back.to_le_bytes(), bytes); |
| 301 | + } |
| 302 | +} |
0 commit comments