hurray_core/layout/addressing/block_paged.rs
1//! Block-paged layout element lookup.
2//!
3//! Implements the element-address formula from
4//! `docs/spec/layouts/block-paged.md § Element Lookup`:
5//!
6//! ```text
7//! page_in_seq = t / page_size
8//! offset_in_page = t mod page_size
9//! phys_page = block_table[seq_ptr[s] + page_in_seq]
10//! flat = ((phys_page * page_size + offset_in_page) * num_heads + h) * head_dim + d
11//! ```
12//!
13//! The index buffers are passed by the caller as typed slices to keep this
14//! module free of byte-parsing — parsing belongs at the descriptor layer.
15
16use crate::Error;
17
18/// Computes the flat element offset into `page_pool` (buffer 0) for the
19/// element at token `t` of sequence `s`, head `h`, dimension `d`.
20///
21/// Returns `Err` if any index is out of bounds or if the `seq_ptr` / `block_table`
22/// invariants are violated. Never panics.
23///
24/// # Arguments
25///
26/// - `s` — sequence index (must be `< num_seqs`).
27/// - `t` — token index within the sequence (zero-based).
28/// - `h` — head index (must be `< num_heads`).
29/// - `d` — head-dimension index (must be `< head_dim`).
30/// - `page_size` — tokens per page (from the layout descriptor; `>= 1`).
31/// - `num_pages` — number of physical pages (from the layout descriptor).
32/// - `num_heads` — `shape[1]` of the tensor.
33/// - `head_dim` — `shape[2]` of the tensor.
34/// - `block_table` — slice of physical page IDs (buffer 1). Length must be >= `seq_ptr[num_seqs]`.
35/// - `seq_ptr` — offset array (buffer 2). Length must be `num_seqs + 1`.
36///
37/// # Errors
38///
39/// - [`Error::IndexOutOfRange`] — any index component is out of bounds.
40/// - [`Error::AddressOverflow`] — intermediate arithmetic overflowed `u64`.
41///
42/// # Examples
43///
44/// ```
45/// use hurray_core::layout::addressing::block_paged::element_offset_u32;
46///
47/// // From the spec example: page_size=4, num_pages=5, num_heads=2, head_dim=8.
48/// // Sequence 0 uses block_table[0..2] = [0, 1].
49/// // Token 4 is in page 1 (4/4=1), offset 0 (4%4=0).
50/// // phys_page = block_table[0 + 1] = 1.
51/// // flat = ((1*4 + 0)*2 + 0)*8 + 0 = (4*2)*8 = 64.
52/// let block_table: &[u32] = &[0, 1, 0]; // seq0: [0,1], seq1: [0]
53/// let seq_ptr: &[u32] = &[0, 2, 3]; // seq0 owns bt[0..2], seq1 owns bt[2..3]
54/// let flat = element_offset_u32(
55/// /*s=*/0, /*t=*/4, /*h=*/0, /*d=*/0,
56/// /*page_size=*/4, /*num_pages=*/5,
57/// /*num_heads=*/2, /*head_dim=*/8,
58/// block_table, seq_ptr,
59/// ).unwrap();
60/// assert_eq!(flat, 64);
61/// ```
62#[allow(clippy::too_many_arguments)]
63pub fn element_offset_u32(
64 s: u64,
65 t: u64,
66 h: u64,
67 d: u64,
68 page_size: u32,
69 num_pages: u64,
70 num_heads: u64,
71 head_dim: u64,
72 block_table: &[u32],
73 seq_ptr: &[u32],
74) -> crate::Result<u64> {
75 element_offset_impl(
76 s,
77 t,
78 h,
79 d,
80 page_size,
81 num_pages,
82 num_heads,
83 head_dim,
84 seq_ptr,
85 block_table,
86 )
87}
88
89/// Computes the flat element offset into `page_pool` (buffer 0) for the
90/// element at token `t` of sequence `s`, head `h`, dimension `d`,
91/// using 64-bit index buffers.
92///
93/// See [`element_offset_u32`] for the full documentation. The only difference
94/// is that `block_table` and `seq_ptr` carry `u64` entries rather than `u32`,
95/// enabling pools larger than 4 billion pages.
96///
97/// # Examples
98///
99/// ```
100/// use hurray_core::layout::addressing::block_paged::element_offset_u64;
101///
102/// let block_table: &[u64] = &[0u64, 1, 0];
103/// let seq_ptr: &[u64] = &[0u64, 2, 3];
104/// let flat = element_offset_u64(
105/// 0, 4, 0, 0, 4, 5, 2, 8, block_table, seq_ptr,
106/// ).unwrap();
107/// assert_eq!(flat, 64);
108/// ```
109#[allow(clippy::too_many_arguments)]
110pub fn element_offset_u64(
111 s: u64,
112 t: u64,
113 h: u64,
114 d: u64,
115 page_size: u32,
116 num_pages: u64,
117 num_heads: u64,
118 head_dim: u64,
119 block_table: &[u64],
120 seq_ptr: &[u64],
121) -> crate::Result<u64> {
122 element_offset_impl(
123 s,
124 t,
125 h,
126 d,
127 page_size,
128 num_pages,
129 num_heads,
130 head_dim,
131 seq_ptr,
132 block_table,
133 )
134}
135
136/// Shared inner implementation, generic over the index width.
137///
138/// `seq_ptr` has `num_seqs + 1` entries. `block_table` has `seq_ptr[num_seqs]` entries.
139/// Generic over `T: Copy + Into<u64>` so the `u32` and `u64` entry points share this
140/// logic without widening the index buffers into a temporary allocation per call —
141/// this is the KV-cache lookup hot path, so per-call heap traffic is avoided.
142#[allow(clippy::too_many_arguments)]
143fn element_offset_impl<T: Copy + Into<u64>>(
144 s: u64,
145 t: u64,
146 h: u64,
147 d: u64,
148 page_size: u32,
149 num_pages: u64,
150 num_heads: u64,
151 head_dim: u64,
152 seq_ptr: &[T],
153 block_table: &[T],
154) -> crate::Result<u64> {
155 // seq_ptr must have at least 1 element (even for an empty batch: [0]).
156 // The last element is seq_ptr[num_seqs].
157 let num_seqs = seq_ptr.len().saturating_sub(1) as u64;
158
159 // Validate sequence index.
160 if s >= num_seqs {
161 return Err(Error::IndexOutOfRange {
162 dim: 0,
163 index: s,
164 size: num_seqs,
165 });
166 }
167
168 // Validate head index.
169 if h >= num_heads {
170 return Err(Error::IndexOutOfRange {
171 dim: 1,
172 index: h,
173 size: num_heads,
174 });
175 }
176
177 // Validate dimension index.
178 if d >= head_dim {
179 return Err(Error::IndexOutOfRange {
180 dim: 2,
181 index: d,
182 size: head_dim,
183 });
184 }
185
186 let page_size_u64 = page_size as u64;
187
188 // Locate the block-table slice for sequence s.
189 // seq_ptr should be non-decreasing (storage invariant), but this function does
190 // not assume the caller validated it — checked_sub guards against a malformed
191 // (non-monotone) seq_ptr instead of panicking, honouring the "never panics" contract.
192 let bt_start: u64 = seq_ptr[s as usize].into();
193 let bt_end: u64 = seq_ptr[s as usize + 1].into();
194
195 // The sequence's logical page count for this sequence.
196 let seq_page_count = bt_end.checked_sub(bt_start).ok_or_else(|| {
197 Error::InvalidLayout(
198 "block-paged: seq_ptr is not non-decreasing (storage invariant violated)".to_string(),
199 )
200 })?;
201
202 // Token → page index within the sequence.
203 let page_in_seq = t / page_size_u64;
204 let offset_in_page = t % page_size_u64;
205
206 // Validate token: must be within the sequence's allocated page slots.
207 // (The spec allows trailing undefined slots in the last page; we
208 // allow t up to seq_page_count * page_size - 1 here, matching what a
209 // reader is permitted to access. The caller is responsible for not
210 // reading past the sequence's valid token count.)
211 if page_in_seq >= seq_page_count {
212 return Err(Error::IndexOutOfRange {
213 dim: 0,
214 index: t,
215 // saturating_mul: this is only the reported upper bound in an error path;
216 // a saturated value still conveys "out of range" without risking overflow.
217 size: seq_page_count.saturating_mul(page_size_u64),
218 });
219 }
220
221 // Resolve physical page via block table.
222 let bt_idx = bt_start
223 .checked_add(page_in_seq)
224 .ok_or(Error::AddressOverflow)?;
225
226 let bt_idx_usize = bt_idx as usize;
227 if bt_idx_usize >= block_table.len() {
228 // Storage invariant violation: block_table is too short.
229 return Err(Error::IndexOutOfRange {
230 dim: 0,
231 index: bt_idx,
232 size: block_table.len() as u64,
233 });
234 }
235
236 let phys_page: u64 = block_table[bt_idx_usize].into();
237
238 // Storage invariant: all physical page IDs must be < num_pages.
239 if phys_page >= num_pages {
240 return Err(Error::IndexOutOfRange {
241 dim: 0,
242 index: phys_page,
243 size: num_pages,
244 });
245 }
246
247 // Compute flat offset per spec:
248 // flat = ((phys_page * page_size + offset_in_page) * num_heads + h) * head_dim + d
249 let flat = phys_page
250 .checked_mul(page_size_u64)
251 .and_then(|v| v.checked_add(offset_in_page))
252 .and_then(|v| v.checked_mul(num_heads))
253 .and_then(|v| v.checked_add(h))
254 .and_then(|v| v.checked_mul(head_dim))
255 .and_then(|v| v.checked_add(d))
256 .ok_or(Error::AddressOverflow)?;
257
258 Ok(flat)
259}
260
261/// Validates the storage invariants for a block-paged tensor's index buffers.
262///
263/// Per `docs/spec/layouts/block-paged.md § Storage Invariants`:
264///
265/// 1. `seq_ptr[0] == 0`.
266/// 2. `seq_ptr` is non-decreasing.
267/// 3. `seq_ptr[num_seqs] == total_logical_pages` (equals `block_table.len()`).
268/// 4. Every `block_table[k] < num_pages`.
269///
270/// Both U32 and U64 variants are supported through the typed overloads below.
271/// This function operates on already-widened slices.
272///
273/// # Errors
274///
275/// Returns [`Error::InvalidLayout`] if any invariant is violated.
276///
277/// # Examples
278///
279/// ```
280/// use hurray_core::layout::addressing::block_paged::validate_index_buffers_u64;
281///
282/// let seq_ptr: &[u64] = &[0, 2, 3];
283/// let block_table: &[u64] = &[0, 1, 0];
284/// assert!(validate_index_buffers_u64(5, 2, seq_ptr, block_table).is_ok());
285/// ```
286pub fn validate_index_buffers_u64(
287 num_pages: u64,
288 num_seqs: u32,
289 seq_ptr: &[u64],
290 block_table: &[u64],
291) -> crate::Result<()> {
292 validate_index_buffers_impl(num_pages, num_seqs, seq_ptr, block_table)
293}
294
295/// Validates the storage invariants for block-paged index buffers using `u32` entries.
296///
297/// Widening wrapper over [`validate_index_buffers_u64`]. See that function for
298/// the full invariant list.
299///
300/// # Errors
301///
302/// Returns [`Error::InvalidLayout`] if any invariant is violated.
303///
304/// # Examples
305///
306/// ```
307/// use hurray_core::layout::addressing::block_paged::validate_index_buffers_u32;
308///
309/// let seq_ptr: &[u32] = &[0, 2, 3];
310/// let block_table: &[u32] = &[0, 1, 0];
311/// assert!(validate_index_buffers_u32(5, 2, seq_ptr, block_table).is_ok());
312/// ```
313pub fn validate_index_buffers_u32(
314 num_pages: u64,
315 num_seqs: u32,
316 seq_ptr: &[u32],
317 block_table: &[u32],
318) -> crate::Result<()> {
319 // Widen before delegating — the inner impl uses u64 throughout.
320 let sp: Vec<u64> = seq_ptr.iter().map(|&v| v as u64).collect();
321 let bt: Vec<u64> = block_table.iter().map(|&v| v as u64).collect();
322 validate_index_buffers_impl(num_pages, num_seqs, &sp, &bt)
323}
324
325fn validate_index_buffers_impl(
326 num_pages: u64,
327 num_seqs: u32,
328 seq_ptr: &[u64],
329 block_table: &[u64],
330) -> crate::Result<()> {
331 let expected_sp_len = (num_seqs as usize).saturating_add(1);
332 if seq_ptr.len() != expected_sp_len {
333 return Err(Error::InvalidLayout(format!(
334 "block-paged: seq_ptr length {} does not equal num_seqs + 1 = {}",
335 seq_ptr.len(),
336 expected_sp_len
337 )));
338 }
339
340 // Invariant 1: seq_ptr[0] == 0.
341 if seq_ptr[0] != 0 {
342 return Err(Error::InvalidLayout(format!(
343 "block-paged: seq_ptr[0] must be 0, got {}",
344 seq_ptr[0]
345 )));
346 }
347
348 // Invariant 2: seq_ptr is non-decreasing.
349 for i in 0..(seq_ptr.len() - 1) {
350 if seq_ptr[i] > seq_ptr[i + 1] {
351 return Err(Error::InvalidLayout(format!(
352 "block-paged: seq_ptr is not non-decreasing at index {}: seq_ptr[{}]={} > seq_ptr[{}]={}",
353 i,
354 i,
355 seq_ptr[i],
356 i + 1,
357 seq_ptr[i + 1]
358 )));
359 }
360 }
361
362 // Invariant 3: seq_ptr[num_seqs] == total_logical_pages == block_table.len().
363 let total_logical_pages = seq_ptr[num_seqs as usize];
364 if total_logical_pages != block_table.len() as u64 {
365 return Err(Error::InvalidLayout(format!(
366 "block-paged: seq_ptr[num_seqs]={} does not equal block_table.len()={}",
367 total_logical_pages,
368 block_table.len()
369 )));
370 }
371
372 // Invariant 4: every block_table[k] < num_pages.
373 for (k, &phys_page) in block_table.iter().enumerate() {
374 if phys_page >= num_pages {
375 return Err(Error::InvalidLayout(format!(
376 "block-paged: block_table[{k}]={phys_page} >= num_pages={num_pages}"
377 )));
378 }
379 }
380
381 Ok(())
382}
383
384#[cfg(test)]
385mod tests {
386 use super::*;
387 use crate::Error;
388
389 // ── Helpers ──────────────────────────────────────────────────────────────
390
391 /// The spec worked example (§ Example):
392 /// layer 3, key role, 2 seqs, page_size=4, num_pages=5, num_heads=2, head_dim=8.
393 /// seq_ptr = [0, 2, 3]
394 /// block_table = [0, 1, 0] ← seq 1 aliases page 0 (prefix sharing)
395 ///
396 /// Flat-address formula (spec §Element Lookup):
397 /// flat = ((phys_page * page_size + offset_in_page) * num_heads + h) * head_dim + d
398 fn spec_example_u32() -> (&'static [u32], &'static [u32]) {
399 let block_table: &[u32] = &[0, 1, 0];
400 let seq_ptr: &[u32] = &[0, 2, 3];
401 (block_table, seq_ptr)
402 }
403
404 fn spec_example_u64() -> (&'static [u64], &'static [u64]) {
405 let block_table: &[u64] = &[0, 1, 0];
406 let seq_ptr: &[u64] = &[0, 2, 3];
407 (block_table, seq_ptr)
408 }
409
410 // ── element_offset_u32: spec worked example ───────────────────────────────
411
412 /// Spec §Example: token 0 of seq 0, head 0, dim 0.
413 /// page_in_seq=0, offset=0, phys_page=block_table[0]=0.
414 /// flat = ((0*4 + 0)*2 + 0)*8 + 0 = 0.
415 #[test]
416 fn element_offset_u32_spec_example_seq0_t0_h0_d0() {
417 let (bt, sp) = spec_example_u32();
418 let flat = element_offset_u32(0, 0, 0, 0, 4, 5, 2, 8, bt, sp).unwrap();
419 assert_eq!(flat, 0);
420 }
421
422 /// Spec §Example: token 4 of seq 0, head 0, dim 0.
423 /// page_in_seq=1, offset=0, phys_page=block_table[1]=1.
424 /// flat = ((1*4 + 0)*2 + 0)*8 + 0 = 64.
425 #[test]
426 fn element_offset_u32_spec_example_seq0_t4_h0_d0() {
427 let (bt, sp) = spec_example_u32();
428 let flat = element_offset_u32(0, 4, 0, 0, 4, 5, 2, 8, bt, sp).unwrap();
429 assert_eq!(flat, 64);
430 }
431
432 /// Spec §Example: token 5 of seq 0, head 1, dim 7 (last valid element).
433 /// page_in_seq=1, offset=1, phys_page=1.
434 /// flat = ((1*4 + 1)*2 + 1)*8 + 7 = ((5)*2 + 1)*8 + 7 = 11*8 + 7 = 95.
435 #[test]
436 fn element_offset_u32_spec_example_seq0_t5_h1_d7() {
437 let (bt, sp) = spec_example_u32();
438 let flat = element_offset_u32(0, 5, 1, 7, 4, 5, 2, 8, bt, sp).unwrap();
439 assert_eq!(flat, 95);
440 }
441
442 /// Spec §Prefix Sharing: seq 1 token 0 uses the same physical page as seq 0 token 0.
443 /// seq 1: block_table[sp[1]..sp[2]] = block_table[2..3] = [0].
444 /// page_in_seq=0, offset=0, phys_page=block_table[2]=0 (same as seq 0 page 0).
445 /// flat = ((0*4 + 0)*2 + 0)*8 + 0 = 0.
446 #[test]
447 fn element_offset_u32_prefix_sharing_seq1_same_physical_page_as_seq0() {
448 let (bt, sp) = spec_example_u32();
449 let flat_seq0 = element_offset_u32(0, 0, 0, 0, 4, 5, 2, 8, bt, sp).unwrap();
450 let flat_seq1 = element_offset_u32(1, 0, 0, 0, 4, 5, 2, 8, bt, sp).unwrap();
451 // Both sequences' token 0 resolve to the same physical page (page 0), same offset.
452 assert_eq!(
453 flat_seq0, flat_seq1,
454 "aliased pages must resolve to identical flat offsets"
455 );
456 assert_eq!(flat_seq1, 0);
457 }
458
459 /// Seq 1 has only 1 logical page (sp[1]=2, sp[2]=3). Token 4 would need a second page.
460 #[test]
461 fn element_offset_u32_token_beyond_sequence_page_count_is_error() {
462 let (bt, sp) = spec_example_u32();
463 let result = element_offset_u32(1, 4, 0, 0, 4, 5, 2, 8, bt, sp);
464 assert!(
465 matches!(result, Err(Error::IndexOutOfRange { .. })),
466 "token 4 in seq 1 (only 1 page) must be IndexOutOfRange"
467 );
468 }
469
470 /// Head index >= num_heads must be rejected.
471 #[test]
472 fn element_offset_u32_head_out_of_range_is_error() {
473 let (bt, sp) = spec_example_u32();
474 let result = element_offset_u32(0, 0, 2, 0, 4, 5, 2, 8, bt, sp);
475 assert!(matches!(result, Err(Error::IndexOutOfRange { dim: 1, .. })));
476 }
477
478 /// Dimension index >= head_dim must be rejected.
479 #[test]
480 fn element_offset_u32_dim_out_of_range_is_error() {
481 let (bt, sp) = spec_example_u32();
482 let result = element_offset_u32(0, 0, 0, 8, 4, 5, 2, 8, bt, sp);
483 assert!(matches!(result, Err(Error::IndexOutOfRange { dim: 2, .. })));
484 }
485
486 /// Sequence index >= num_seqs must be rejected.
487 #[test]
488 fn element_offset_u32_seq_out_of_range_is_error() {
489 let (bt, sp) = spec_example_u32();
490 let result = element_offset_u32(2, 0, 0, 0, 4, 5, 2, 8, bt, sp);
491 assert!(matches!(result, Err(Error::IndexOutOfRange { dim: 0, .. })));
492 }
493
494 /// A non-monotone `seq_ptr` (storage-invariant violation) must return an error,
495 /// not panic — `element_offset` does not assume the caller pre-validated the buffers.
496 #[test]
497 fn element_offset_u32_non_monotone_seq_ptr_is_error_not_panic() {
498 let block_table: &[u32] = &[0];
499 let seq_ptr: &[u32] = &[0, 2, 1]; // seq_ptr[2] < seq_ptr[1]
500 let result = element_offset_u32(1, 0, 0, 0, 4, 5, 2, 8, block_table, seq_ptr);
501 assert!(matches!(
502 result,
503 Err(Error::InvalidLayout(_) | Error::IndexOutOfRange { .. })
504 ));
505 }
506
507 // ── element_offset_u64 ────────────────────────────────────────────────────
508
509 /// Confirm element_offset_u64 produces the same result as u32 on the spec example.
510 #[test]
511 fn element_offset_u64_matches_u32_on_spec_example() {
512 let (bt, sp) = spec_example_u64();
513 let flat = element_offset_u64(0, 4, 0, 0, 4, 5, 2, 8, bt, sp).unwrap();
514 assert_eq!(flat, 64);
515 }
516
517 /// Prefix sharing is observable via u64 variant too.
518 #[test]
519 fn element_offset_u64_prefix_sharing_aliased_offsets_are_equal() {
520 let (bt, sp) = spec_example_u64();
521 let flat_seq0 = element_offset_u64(0, 0, 0, 0, 4, 5, 2, 8, bt, sp).unwrap();
522 let flat_seq1 = element_offset_u64(1, 0, 0, 0, 4, 5, 2, 8, bt, sp).unwrap();
523 assert_eq!(flat_seq0, flat_seq1);
524 }
525
526 // ── element_offset: last valid slot in the page pool ─────────────────────
527
528 /// Absolute last addressable element: page 4 (last page), slot 3, head 1, dim 7.
529 /// flat = ((4*4 + 3)*2 + 1)*8 + 7 = (19*2 + 1)*8 + 7 = 39*8 + 7 = 319.
530 /// page_pool has 5*4*2*8 = 320 elements; index 319 is the last.
531 #[test]
532 fn element_offset_u32_last_element_in_pool() {
533 // Use a seq that owns physical page 4 via its block table entry.
534 let block_table: &[u32] = &[4]; // seq 0 has 1 logical page → physical page 4
535 let seq_ptr: &[u32] = &[0, 1];
536 let flat = element_offset_u32(0, 3, 1, 7, 4, 5, 2, 8, block_table, seq_ptr).unwrap();
537 assert_eq!(flat, 319);
538 }
539
540 // ── validate_index_buffers_u32: valid inputs ──────────────────────────────
541
542 /// Spec §Storage Invariants (1)–(4): the spec example is valid.
543 #[test]
544 fn validate_index_buffers_u32_spec_example_is_valid() {
545 let seq_ptr: &[u32] = &[0, 2, 3];
546 let block_table: &[u32] = &[0, 1, 0];
547 assert!(
548 validate_index_buffers_u32(5, 2, seq_ptr, block_table).is_ok(),
549 "spec example index buffers must be valid"
550 );
551 }
552
553 /// Empty batch: num_seqs=0, seq_ptr=[0], block_table=[].
554 #[test]
555 fn validate_index_buffers_u32_empty_batch_is_valid() {
556 let seq_ptr: &[u32] = &[0];
557 let block_table: &[u32] = &[];
558 assert!(
559 validate_index_buffers_u32(5, 0, seq_ptr, block_table).is_ok(),
560 "empty batch must be valid"
561 );
562 }
563
564 /// Single-sequence batch with multiple pages.
565 #[test]
566 fn validate_index_buffers_u32_single_seq_multiple_pages_is_valid() {
567 let seq_ptr: &[u32] = &[0, 3];
568 let block_table: &[u32] = &[2, 0, 4];
569 assert!(validate_index_buffers_u32(5, 1, seq_ptr, block_table).is_ok());
570 }
571
572 // ── validate_index_buffers_u32: invariant 1 — seq_ptr[0] != 0 ────────────
573
574 /// Spec §Storage Invariant 1: seq_ptr[0] MUST be 0.
575 #[test]
576 fn validate_index_buffers_u32_rejects_seq_ptr_not_starting_at_zero() {
577 let seq_ptr: &[u32] = &[1, 3]; // seq_ptr[0] = 1, not 0
578 let block_table: &[u32] = &[0, 1, 2];
579 let result = validate_index_buffers_u32(5, 1, seq_ptr, block_table);
580 assert!(
581 matches!(result, Err(Error::InvalidLayout(_))),
582 "seq_ptr[0] != 0 must be InvalidLayout"
583 );
584 }
585
586 // ── validate_index_buffers_u32: invariant 2 — non-monotonic seq_ptr ──────
587
588 /// Spec §Storage Invariant 2: seq_ptr must be non-decreasing.
589 #[test]
590 fn validate_index_buffers_u32_rejects_non_monotonic_seq_ptr() {
591 let seq_ptr: &[u32] = &[0, 3, 2]; // seq_ptr[1] > seq_ptr[2]
592 let block_table: &[u32] = &[0, 1, 0];
593 let result = validate_index_buffers_u32(5, 2, seq_ptr, block_table);
594 assert!(
595 matches!(result, Err(Error::InvalidLayout(_))),
596 "non-monotonic seq_ptr must be InvalidLayout"
597 );
598 }
599
600 // ── validate_index_buffers_u32: invariant 3 — seq_ptr[num_seqs] mismatch ─
601
602 /// Spec §Storage Invariant 3: seq_ptr[num_seqs] must equal block_table.len().
603 #[test]
604 fn validate_index_buffers_u32_rejects_seq_ptr_total_mismatch_block_table_len() {
605 // seq_ptr says 4 total pages but block_table has 3 entries.
606 let seq_ptr: &[u32] = &[0, 2, 4];
607 let block_table: &[u32] = &[0, 1, 2]; // only 3 entries, not 4
608 let result = validate_index_buffers_u32(5, 2, seq_ptr, block_table);
609 assert!(
610 matches!(result, Err(Error::InvalidLayout(_))),
611 "seq_ptr[num_seqs] != block_table.len() must be InvalidLayout"
612 );
613 }
614
615 // ── validate_index_buffers_u32: invariant 4 — out-of-bounds page id ──────
616
617 /// Spec §Storage Invariant 4: every block_table[k] < num_pages.
618 #[test]
619 fn validate_index_buffers_u32_rejects_out_of_bounds_page_id() {
620 let seq_ptr: &[u32] = &[0, 2, 3];
621 let block_table: &[u32] = &[0, 5, 0]; // page 5 >= num_pages=5
622 let result = validate_index_buffers_u32(5, 2, seq_ptr, block_table);
623 assert!(
624 matches!(result, Err(Error::InvalidLayout(_))),
625 "block_table[k] >= num_pages must be InvalidLayout"
626 );
627 }
628
629 /// Page id equal to num_pages (exactly at boundary) must be rejected.
630 #[test]
631 fn validate_index_buffers_u32_rejects_page_id_equal_to_num_pages() {
632 let seq_ptr: &[u32] = &[0, 1];
633 let block_table: &[u32] = &[5]; // == num_pages (0-indexed: valid range is 0..4)
634 let result = validate_index_buffers_u32(5, 1, seq_ptr, block_table);
635 assert!(matches!(result, Err(Error::InvalidLayout(_))));
636 }
637
638 // ── validate_index_buffers_u64: basic coverage ────────────────────────────
639
640 #[test]
641 fn validate_index_buffers_u64_spec_example_is_valid() {
642 let seq_ptr: &[u64] = &[0, 2, 3];
643 let block_table: &[u64] = &[0, 1, 0];
644 assert!(validate_index_buffers_u64(5, 2, seq_ptr, block_table).is_ok());
645 }
646
647 #[test]
648 fn validate_index_buffers_u64_empty_batch_is_valid() {
649 let seq_ptr: &[u64] = &[0];
650 let block_table: &[u64] = &[];
651 assert!(validate_index_buffers_u64(5, 0, seq_ptr, block_table).is_ok());
652 }
653
654 #[test]
655 fn validate_index_buffers_u64_rejects_out_of_bounds_page_id() {
656 let seq_ptr: &[u64] = &[0, 1];
657 let block_table: &[u64] = &[5]; // == num_pages
658 let result = validate_index_buffers_u64(5, 1, seq_ptr, block_table);
659 assert!(matches!(result, Err(Error::InvalidLayout(_))));
660 }
661}