hurray_core/layout/mod.rs
1//! Layout descriptors for the Hurray tensor interchange format.
2//!
3//! Every tensor descriptor includes a **layout tag** (`uint8`) and a
4//! layout-specific payload that together describe how elements are arranged in
5//! memory. This module provides the [`LayoutDescriptor`] enum and all per-layout
6//! payload structs.
7//!
8//! ## Layout tag space
9//!
10//! | Range | Allocation |
11//! |-------|-----------|
12//! | `0x00` | Reserved (permanently invalid) |
13//! | `0x01`–`0x0B` | Core named layouts (Tier 1), including the Composite / Virtual head (`0x0B`, ADR-027) |
14//! | `0x0C`–`0x3F` | Reserved for future specification versions |
15//! | `0x40` | Hilbert curve layout (Tier 2) |
16//! | `0x41`–`0x7F` | Reserved for future specification versions |
17//! | `0x80`–`0xEF` | Reserved for future specification versions |
18//! | `0xF0`–`0xFE` | Implementation-private extension layouts |
19//! | `0xFF` | Reserved (permanently invalid) |
20//!
21//! ## Strict vs permissive mode
22//!
23//! The [`LayoutDescriptor`] enum's named variants (everything except
24//! [`LayoutDescriptor::Unknown`]) correspond to tags this implementation
25//! understands. A decoder operating in **strict mode** must reject any tag not
26//! covered by a named variant; a decoder operating in **permissive mode** may
27//! wrap unrecognised tags in [`LayoutDescriptor::Unknown`].
28//!
29//! [`LayoutDescriptor::tag`] and [`LayoutDescriptor::buffer_count`] work on all
30//! variants including `Unknown`.
31//!
32//! See `docs/spec/memory-layout.md` for the normative definition.
33
34pub mod addressing;
35pub mod block_paged;
36pub mod col_major;
37pub mod composite;
38pub mod coo;
39pub mod csc;
40pub mod csf;
41pub mod csr;
42pub mod hilbert;
43pub mod morton;
44pub mod private_extension;
45pub mod row_major;
46pub mod strided;
47pub mod tiled;
48pub mod unknown;
49
50pub use addressing::{byte_address_from_element_offset, ElementAddress};
51pub use block_paged::{BlockPagedLayout, BlockTableIndexType, KvRole};
52pub use composite::{CombineOp, CompositeLayout, CompositionRule};
53pub use coo::CooLayout;
54pub use csc::CscLayout;
55pub use csf::CsfLayout;
56pub use csr::CsrLayout;
57pub use hilbert::HilbertLayout;
58pub use morton::MortonLayout;
59pub use private_extension::{
60 PrivateExtensionLayout, PRIVATE_LAYOUT_TAG_MAX, PRIVATE_LAYOUT_TAG_MIN,
61};
62pub use strided::StridedLayout;
63pub use tiled::{InnerStrides, OuterStrides, TiledLayout, MAX_TILED_DEPTH};
64pub use unknown::UnknownLayout;
65
66use std::num::NonZeroU8;
67
68use crate::{Error, Result, Shape};
69
70// ── Tag constants (public, for use in Layer 4 encoding) ──────────────────────
71
72/// Layout tag for row-major (C-order) layout.
73pub const TAG_ROW_MAJOR: u8 = 0x01;
74/// Layout tag for column-major (Fortran-order) layout.
75pub const TAG_COL_MAJOR: u8 = 0x02;
76/// Layout tag for strided layout.
77pub const TAG_STRIDED: u8 = 0x03;
78/// Layout tag for tiled / blocked layout.
79pub const TAG_TILED: u8 = 0x04;
80/// Layout tag for Morton (Z-order) layout.
81pub const TAG_MORTON: u8 = 0x05;
82/// Layout tag for COO (Coordinate) sparse layout.
83pub const TAG_COO: u8 = 0x06;
84/// Layout tag for CSR (Compressed Sparse Row) layout.
85pub const TAG_CSR: u8 = 0x07;
86/// Layout tag for CSC (Compressed Sparse Column) layout.
87pub const TAG_CSC: u8 = 0x08;
88/// Layout tag for CSF (Compressed Sparse Fiber) layout.
89pub const TAG_CSF: u8 = 0x09;
90/// Layout tag for block-paged indirect layout.
91pub const TAG_BLOCK_PAGED: u8 = 0x0A;
92/// Layout tag for the composite / virtual head (ADR-027). See `docs/spec/layouts/composite.md`.
93pub const TAG_COMPOSITE: u8 = 0x0B;
94/// Layout tag for Hilbert curve layout.
95pub const TAG_HILBERT: u8 = 0x40;
96
97// ── Layout tag classification helpers ────────────────────────────────────────
98
99/// Returns `true` if `tag` is permanently invalid (`0x00` or `0xFF`).
100///
101/// # Examples
102///
103/// ```
104/// use hurray_core::layout::is_invalid_tag;
105///
106/// assert!(is_invalid_tag(0x00));
107/// assert!(is_invalid_tag(0xFF));
108/// assert!(!is_invalid_tag(0x01));
109/// ```
110#[inline]
111pub fn is_invalid_tag(tag: u8) -> bool {
112 tag == 0x00 || tag == 0xFF
113}
114
115/// Returns `true` if `tag` falls in a range reserved for future specification
116/// versions (`0x0C`–`0x3F`, `0x41`–`0x7F`, `0x80`–`0xEF`).
117///
118/// Tag `0x0B` (composite / virtual head, ADR-027) is now a named Tier-1 tag and is
119/// therefore no longer reserved — it moved out of this range when Composite was
120/// implemented.
121///
122/// # Examples
123///
124/// ```
125/// use hurray_core::layout::is_reserved_tag;
126///
127/// assert!(!is_reserved_tag(0x0A)); // block-paged — named tag
128/// assert!(!is_reserved_tag(0x0B)); // composite / virtual head — named tag
129/// assert!(is_reserved_tag(0x0C));
130/// assert!(is_reserved_tag(0x3F));
131/// assert!(is_reserved_tag(0x41));
132/// assert!(is_reserved_tag(0xEF));
133/// assert!(!is_reserved_tag(0x01));
134/// assert!(!is_reserved_tag(0x40));
135/// ```
136#[inline]
137pub fn is_reserved_tag(tag: u8) -> bool {
138 matches!(tag, 0x0C..=0x3F | 0x41..=0x7F | 0x80..=0xEF)
139}
140
141/// Returns `true` if `tag` is in the private-extension range (`0xF0`–`0xFE`).
142///
143/// # Examples
144///
145/// ```
146/// use hurray_core::layout::is_private_tag;
147///
148/// assert!(is_private_tag(0xF0));
149/// assert!(is_private_tag(0xFE));
150/// assert!(!is_private_tag(0xFF));
151/// assert!(!is_private_tag(0x01));
152/// ```
153#[inline]
154pub fn is_private_tag(tag: u8) -> bool {
155 matches!(tag, PRIVATE_LAYOUT_TAG_MIN..=PRIVATE_LAYOUT_TAG_MAX)
156}
157
158/// Returns `true` if `tag` has a named [`LayoutDescriptor`] variant in this crate.
159///
160/// The complement of "unknown": a named tag carries structure this implementation
161/// knows how to check, so it MUST NOT be wrapped in
162/// [`UnknownLayout`](UnknownLayout), which has neither a buffer count nor shape
163/// constraints.
164///
165/// # Examples
166///
167/// ```
168/// use hurray_core::layout::is_named_tag;
169///
170/// assert!(is_named_tag(0x01)); // row-major
171/// assert!(is_named_tag(0x0B)); // composite / virtual head
172/// assert!(is_named_tag(0x40)); // Hilbert
173/// assert!(!is_named_tag(0x0C)); // reserved for a future version
174/// assert!(!is_named_tag(0xF0)); // private extension
175/// ```
176#[inline]
177pub fn is_named_tag(tag: u8) -> bool {
178 matches!(
179 tag,
180 TAG_ROW_MAJOR
181 | TAG_COL_MAJOR
182 | TAG_STRIDED
183 | TAG_TILED
184 | TAG_MORTON
185 | TAG_COO
186 | TAG_CSR
187 | TAG_CSC
188 | TAG_CSF
189 | TAG_BLOCK_PAGED
190 | TAG_COMPOSITE
191 | TAG_HILBERT
192 )
193}
194
195/// Validates a layout tag in **strict mode**, returning the appropriate error
196/// for tags that are invalid, reserved, or private.
197///
198/// Named (known) tags return `Ok(())`. Unknown tags in allocated-but-unassigned
199/// ranges return [`Error::UnknownLayoutTag`].
200///
201/// # Errors
202///
203/// | Tag range | Error |
204/// |-----------|-------|
205/// | `0x00`, `0xFF` | [`Error::InvalidLayoutTag`] |
206/// | `0x0C`–`0x3F`, `0x41`–`0x7F`, `0x80`–`0xEF` | [`Error::ReservedLayoutTag`] |
207/// | `0xF0`–`0xFE` | [`Error::PrivateLayoutTag`] |
208/// | Any other unrecognised value | [`Error::UnknownLayoutTag`] |
209///
210/// # Examples
211///
212/// ```
213/// use hurray_core::{Error, layout::validate_layout_tag_strict};
214///
215/// assert!(validate_layout_tag_strict(0x01).is_ok());
216/// assert!(validate_layout_tag_strict(0x09).is_ok()); // CSF
217/// assert!(validate_layout_tag_strict(0x0B).is_ok()); // composite / virtual head
218/// assert!(matches!(validate_layout_tag_strict(0x00), Err(Error::InvalidLayoutTag(0x00))));
219/// assert!(matches!(validate_layout_tag_strict(0xFF), Err(Error::InvalidLayoutTag(0xFF))));
220/// assert!(matches!(validate_layout_tag_strict(0x0C), Err(Error::ReservedLayoutTag(0x0C))));
221/// assert!(matches!(validate_layout_tag_strict(0xF0), Err(Error::PrivateLayoutTag(0xF0))));
222/// ```
223pub fn validate_layout_tag_strict(tag: u8) -> Result<()> {
224 match tag {
225 0x00 | 0xFF => Err(Error::InvalidLayoutTag(tag)),
226 t if is_reserved_tag(t) => Err(Error::ReservedLayoutTag(tag)),
227 t if is_private_tag(t) => Err(Error::PrivateLayoutTag(tag)),
228 // One list of named tags, in is_named_tag, so strict validation and the
229 // unknown-layout constructor cannot come to disagree about what is named.
230 t if is_named_tag(t) => Ok(()),
231 _ => Err(Error::UnknownLayoutTag(tag)),
232 }
233}
234
235// ── LayoutDescriptor ─────────────────────────────────────────────────────────
236
237/// The memory layout of a tensor's data buffer.
238///
239/// Every variant corresponds to a layout tag value defined in the Hurray spec
240/// (`docs/spec/memory-layout.md`). The discriminant IS the tag byte; call
241/// [`LayoutDescriptor::tag`] to retrieve it.
242///
243/// The enum is `#[non_exhaustive]` so that future spec versions can add new
244/// named layouts without breaking existing `match` arms in downstream crates.
245///
246/// ## Construction
247///
248/// - `RowMajor`, `ColMajor`, and `Morton` are unit variants — construct them
249/// directly (`LayoutDescriptor::RowMajor`). No separate constructor is needed
250/// because they carry no configurable fields.
251/// - All other variants wrap a payload struct. Construct the struct via its own
252/// `new()` method (which validates invariants), then wrap it.
253/// - `Unknown` is the permissive-mode fallback and is not produced by any
254/// named-variant path.
255///
256/// ## Examples
257///
258/// ```
259/// use hurray_core::layout::{LayoutDescriptor, StridedLayout, CooLayout};
260///
261/// // Unit variant — no constructor needed.
262/// let rm = LayoutDescriptor::RowMajor;
263/// assert_eq!(rm.tag(), 0x01);
264///
265/// // Strided layout with explicit strides.
266/// let strided = LayoutDescriptor::Strided(StridedLayout::new(vec![4, 1]));
267/// assert_eq!(strided.tag(), 0x03);
268///
269/// // Sparse COO layout.
270/// let coo = LayoutDescriptor::Coo(CooLayout::new(42, true));
271/// assert_eq!(coo.tag(), 0x06);
272/// assert_eq!(coo.buffer_count().map(|n| n.get()), Some(2));
273/// ```
274#[derive(Debug, Clone, PartialEq, Eq, Hash)]
275#[non_exhaustive]
276pub enum LayoutDescriptor {
277 /// Row-major (C-order) layout. Tag `0x01`.
278 RowMajor,
279
280 /// Column-major (Fortran-order) layout. Tag `0x02`.
281 ColMajor,
282
283 /// Strided layout with explicit per-dimension strides. Tag `0x03`.
284 Strided(StridedLayout),
285
286 /// Tiled / blocked layout. Tag `0x04`.
287 ///
288 /// Boxed because [`TiledLayout`] is recursive (inner tiles) and would
289 /// otherwise make the enum size unbounded.
290 Tiled(Box<TiledLayout>),
291
292 /// Morton (Z-order curve) layout. Tag `0x05`.
293 Morton(MortonLayout),
294
295 /// COO (Coordinate) sparse layout. Tag `0x06`. Buffer count = 2.
296 Coo(CooLayout),
297
298 /// CSR (Compressed Sparse Row) layout. Tag `0x07`. Buffer count = 3.
299 Csr(CsrLayout),
300
301 /// CSC (Compressed Sparse Column) layout. Tag `0x08`. Buffer count = 3.
302 Csc(CscLayout),
303
304 /// CSF (Compressed Sparse Fiber) layout. Tag `0x09`. Buffer count = `2·rank+1`.
305 ///
306 /// The rank-N generalisation of CSR/CSC, storing a sparse tensor as a tree of
307 /// `rank` levels. Only valid for rank ≥ 3. Buffer count is rank-dependent because
308 /// each level contributes one `pos` and one `crd` buffer plus a single `values` buffer.
309 ///
310 /// See `docs/spec/layouts/csf.md`.
311 Csf(CsfLayout),
312
313 /// Block-paged indirect layout. Tag `0x0A`. Buffer count = 3 (+ optional quant buffers).
314 ///
315 /// Stores a KV-cache tensor whose paged axis is divided into fixed-size pages
316 /// drawn from a shared page pool, with a block table mapping logical page
317 /// positions to physical page IDs. Designed for PagedAttention KV caches.
318 ///
319 /// See `docs/spec/layouts/block-paged.md`.
320 BlockPaged(BlockPagedLayout),
321
322 /// Composite / Virtual head. Tag `0x0B`. Owns no data (`buffer_count = 0`).
323 ///
324 /// The head presents a single logical `shape`/`type_tag` view over an ordered
325 /// set of **member** tensor descriptors bound by forward stream adjacency (see
326 /// [`crate::composite::CompositeTensor`]). This is a new addressing category,
327 /// **Virtual**, alongside Dense, Sparse, and Indirect.
328 ///
329 /// See `docs/spec/layouts/composite.md`.
330 Composite(CompositeLayout),
331
332 /// Hilbert curve layout. Tag `0x40`.
333 Hilbert(HilbertLayout),
334
335 /// Implementation-private extension layout. Tags `0xF0`–`0xFE`.
336 PrivateExtension(PrivateExtensionLayout),
337
338 /// Unrecognized layout accepted in permissive mode.
339 ///
340 /// A strict-mode reader MUST NOT produce this variant. A permissive-mode
341 /// reader MAY produce it for tags not covered by any named variant above,
342 /// but MUST NOT dereference or interpret the associated tensor data buffer.
343 Unknown(UnknownLayout),
344}
345
346impl LayoutDescriptor {
347 /// Returns the wire tag byte for this layout descriptor.
348 ///
349 /// The tag is the discriminant used in the binary encoding. It is always a
350 /// single `uint8` value.
351 ///
352 /// # Examples
353 ///
354 /// ```
355 /// use hurray_core::layout::{CooLayout, CsfLayout, LayoutDescriptor, UnknownLayout};
356 ///
357 /// assert_eq!(LayoutDescriptor::RowMajor.tag(), 0x01);
358 /// assert_eq!(LayoutDescriptor::ColMajor.tag(), 0x02);
359 /// assert_eq!(LayoutDescriptor::Coo(CooLayout::new(0, false)).tag(), 0x06);
360 /// assert_eq!(LayoutDescriptor::Csf(CsfLayout::new(0, vec![0, 1, 2])).tag(), 0x09);
361 ///
362 /// // Unknown passthrough (with a reserved tag outside the named set).
363 /// let u = LayoutDescriptor::Unknown(UnknownLayout::new(0x0C, vec![]).unwrap());
364 /// assert_eq!(u.tag(), 0x0C);
365 /// ```
366 #[inline]
367 pub fn tag(&self) -> u8 {
368 match self {
369 Self::RowMajor => TAG_ROW_MAJOR,
370 Self::ColMajor => TAG_COL_MAJOR,
371 Self::Strided(_) => TAG_STRIDED,
372 Self::Tiled(_) => TAG_TILED,
373 Self::Morton(_) => TAG_MORTON,
374 Self::Coo(_) => TAG_COO,
375 Self::Csr(_) => TAG_CSR,
376 Self::Csc(_) => TAG_CSC,
377 Self::Csf(_) => TAG_CSF,
378 Self::BlockPaged(_) => TAG_BLOCK_PAGED,
379 Self::Composite(_) => TAG_COMPOSITE,
380 Self::Hilbert(_) => TAG_HILBERT,
381 Self::PrivateExtension(p) => p.tag,
382 Self::Unknown(u) => u.tag,
383 }
384 }
385
386 /// Returns the number of data buffers this layout requires, or `None` if the
387 /// count is either not statically known ([`LayoutDescriptor::Unknown`],
388 /// [`LayoutDescriptor::PrivateExtension`]) or is a virtual "known zero"
389 /// ([`LayoutDescriptor::Composite`] — use [`LayoutDescriptor::is_virtual`] to
390 /// tell the two apart).
391 ///
392 /// Dense layouts always require 1 buffer. Sparse layouts require a fixed
393 /// number of component buffers as defined per format:
394 ///
395 /// | Layout | Buffer count |
396 /// |--------|-------------|
397 /// | RowMajor, ColMajor, Strided, Tiled, Morton, Hilbert | 1 |
398 /// | COO | 2 |
399 /// | CSR, CSC, BlockPaged | 3 |
400 /// | CSF | `2·rank+1` (rank-dependent; embedded in `mode_order.len()`) |
401 /// | Composite | `0`, not `NonZeroU8`-representable — reported as `None` |
402 /// | PrivateExtension, Unknown | `None` (genuinely unknown) |
403 ///
404 /// > **Note:** This is the layout's **minimum** buffer count, not the total size
405 /// > of the tensor descriptor's buffer table. Quantization-parameter buffers are
406 /// > counted separately and appended after the layout's own buffers (so a quantized
407 /// > block-paged tensor has `3 + n` buffers, matching the spec's `buffer_count >= 3`).
408 ///
409 /// # Examples
410 ///
411 /// ```
412 /// use std::num::NonZeroU8;
413 /// use hurray_core::layout::{CooLayout, CsrLayout, CsfLayout, LayoutDescriptor, UnknownLayout};
414 ///
415 /// assert_eq!(LayoutDescriptor::RowMajor.buffer_count(), NonZeroU8::new(1));
416 /// assert_eq!(
417 /// LayoutDescriptor::Coo(CooLayout::new(0, false)).buffer_count(),
418 /// NonZeroU8::new(2),
419 /// );
420 /// assert_eq!(
421 /// LayoutDescriptor::Csr(CsrLayout::new(0)).buffer_count(),
422 /// NonZeroU8::new(3),
423 /// );
424 /// // CSF rank-3: 2*3+1 = 7 buffers.
425 /// assert_eq!(
426 /// LayoutDescriptor::Csf(CsfLayout::new(0, vec![0, 1, 2])).buffer_count(),
427 /// NonZeroU8::new(7),
428 /// );
429 /// let u = LayoutDescriptor::Unknown(UnknownLayout::new(0x0C, vec![]).unwrap());
430 /// assert_eq!(u.buffer_count(), None);
431 /// ```
432 #[inline]
433 pub fn buffer_count(&self) -> Option<NonZeroU8> {
434 match self {
435 // Dense layouts: exactly one data buffer.
436 Self::RowMajor
437 | Self::ColMajor
438 | Self::Strided(_)
439 | Self::Tiled(_)
440 | Self::Morton(_)
441 | Self::Hilbert(_) => NonZeroU8::new(1),
442
443 // COO: values + indices.
444 Self::Coo(_) => NonZeroU8::new(2),
445
446 // CSR / CSC / BlockPaged: values + index array + pointer array.
447 Self::Csr(_) | Self::Csc(_) | Self::BlockPaged(_) => NonZeroU8::new(3),
448
449 // CSF: 2·rank+1 buffers (values + rank pos arrays + rank crd arrays).
450 // rank ≤ 64 (data-model.md), so 2·64+1 = 129 fits in u8 (max 255).
451 // Saturate at u8::MAX if mode_order is somehow empty (would be caught
452 // by validate_against_shape before any buffer-table use).
453 Self::Csf(c) => {
454 // 2·rank+1 with rank = mode_order.len(); rank ≥ 3 after validation.
455 // Saturate rather than truncate so a malformed (pre-validation) rank
456 // can never silently wrap to a small count.
457 let count = (2 * c.mode_order.len() + 1).min(u8::MAX as usize) as u8;
458 NonZeroU8::new(count)
459 }
460
461 // Composite: a virtual head owns exactly zero buffers, but "known zero" isn't
462 // representable via NonZeroU8 — callers MUST use `is_virtual()` to distinguish
463 // this case from PrivateExtension/Unknown's genuinely-unknown count.
464 Self::Composite(_) => None,
465
466 // Private and unknown: buffer requirements are not known statically.
467 Self::PrivateExtension(_) | Self::Unknown(_) => None,
468 }
469 }
470
471 /// Returns `true` if this layout is **virtual** (owns no data buffer).
472 ///
473 /// Only [`LayoutDescriptor::Composite`] is virtual. This lets callers
474 /// distinguish "buffer count is `None` because the layout is virtual (a
475 /// known zero, per spec)" from "buffer count is `None` because it is not
476 /// statically known" ([`LayoutDescriptor::Unknown`],
477 /// [`LayoutDescriptor::PrivateExtension`]) — both report `None` from
478 /// [`LayoutDescriptor::buffer_count`], but only the former is genuinely zero.
479 ///
480 /// # Examples
481 ///
482 /// ```
483 /// use hurray_core::layout::{CompositeLayout, CompositionRule, LayoutDescriptor};
484 ///
485 /// let composite = LayoutDescriptor::Composite(
486 /// CompositeLayout::new(CompositionRule::Group, 0).unwrap(),
487 /// );
488 /// assert!(composite.is_virtual());
489 /// assert!(!LayoutDescriptor::RowMajor.is_virtual());
490 /// ```
491 #[inline]
492 pub fn is_virtual(&self) -> bool {
493 matches!(self, Self::Composite(_))
494 }
495
496 /// Validates layout-specific constraints against the provided tensor shape.
497 ///
498 /// This method is called by Layer 4 (tensor descriptor) to check that the
499 /// layout descriptor is consistent with the tensor's rank and dimension
500 /// sizes. Constructors do not call this method because shape is not
501 /// available at layout-descriptor construction time.
502 ///
503 /// ## Checked constraints
504 ///
505 /// | Layout | Constraint |
506 /// |--------|-----------|
507 /// | `Strided` | `strides.len() == shape.rank()` |
508 /// | `Tiled` | `tile_shape.len() == shape.rank()`; each `tile_shape[k] > 0` |
509 /// | `Morton` | `morton_bits.len() == shape.rank()`; `shape[k] <= 2^morton_bits[k]` for all static dims |
510 /// | `Hilbert` | `hilbert_rank == shape.rank()`; `hilbert_rank >= 2`; each static dim == `2^hilbert_order` |
511 /// | `Coo` | `shape.rank() >= 1` (no scalar sparse tensors) |
512 /// | `Csr` | `shape.rank() == 2` |
513 /// | `Csc` | `shape.rank() == 2` |
514 /// | `Csf` | `shape.rank() >= 3`; `mode_order.len() == shape.rank()`; `mode_order` is a permutation of `0..rank` |
515 /// | `Unknown` | always `Ok(())` |
516 ///
517 /// # Errors
518 ///
519 /// Returns [`Error::InvalidLayout`] if any constraint is violated.
520 ///
521 /// # Examples
522 ///
523 /// ```
524 /// use hurray_core::{Shape, layout::{LayoutDescriptor, StridedLayout}};
525 ///
526 /// let shape = Shape::new(vec![3, 4]).unwrap();
527 ///
528 /// // Correct rank.
529 /// let ok = LayoutDescriptor::Strided(StridedLayout::new(vec![4, 1]));
530 /// assert!(ok.validate_against_shape(&shape).is_ok());
531 ///
532 /// // Wrong rank.
533 /// let bad = LayoutDescriptor::Strided(StridedLayout::new(vec![1]));
534 /// assert!(bad.validate_against_shape(&shape).is_err());
535 /// ```
536 pub fn validate_against_shape(&self, shape: &Shape) -> Result<()> {
537 match self {
538 Self::RowMajor | Self::ColMajor => {
539 // No layout-specific constraints beyond what the tensor descriptor checks.
540 Ok(())
541 }
542
543 Self::Strided(s) => {
544 if s.strides.len() != shape.rank() {
545 return Err(Error::InvalidLayout(format!(
546 "strided layout: strides.len() ({}) != shape.rank() ({})",
547 s.strides.len(),
548 shape.rank()
549 )));
550 }
551 // Spec: MUST NOT be used for rank-0 (scalar) tensors.
552 if shape.rank() == 0 {
553 return Err(Error::InvalidLayout(
554 "strided layout cannot be used for rank-0 (scalar) tensors".to_string(),
555 ));
556 }
557 Ok(())
558 }
559
560 Self::Tiled(t) => {
561 if t.tile_shape.len() != shape.rank() {
562 return Err(Error::InvalidLayout(format!(
563 "tiled layout: tile_shape.len() ({}) != shape.rank() ({})",
564 t.tile_shape.len(),
565 shape.rank()
566 )));
567 }
568 // Spec: MUST NOT be used for rank-0 tensors.
569 if shape.rank() == 0 {
570 return Err(Error::InvalidLayout(
571 "tiled layout cannot be used for rank-0 (scalar) tensors".to_string(),
572 ));
573 }
574 // tile_shape values > 0 already enforced by TiledLayout::new.
575 Ok(())
576 }
577
578 Self::Morton(m) => {
579 if m.morton_bits.len() != shape.rank() {
580 return Err(Error::InvalidLayout(format!(
581 "Morton layout: morton_bits.len() ({}) != shape.rank() ({})",
582 m.morton_bits.len(),
583 shape.rank()
584 )));
585 }
586 // Validate shape[k] <= 2^morton_bits[k] for all static (non-DYNAMIC) dims.
587 for (k, (&bits, &dim)) in m.morton_bits.iter().zip(shape.dims().iter()).enumerate()
588 {
589 if dim == crate::shape::DYNAMIC {
590 continue;
591 }
592 // 2^bits saturates at u64::MAX if bits >= 64.
593 let max_dim = if bits >= 64 { u64::MAX } else { 1u64 << bits };
594 if dim > max_dim {
595 return Err(Error::InvalidLayout(format!(
596 "Morton layout: shape[{k}] ({dim}) > 2^morton_bits[{k}] ({max_dim})"
597 )));
598 }
599 }
600 Ok(())
601 }
602
603 Self::Coo(_) => {
604 if shape.rank() == 0 {
605 return Err(Error::InvalidLayout(
606 "COO layout cannot be used for rank-0 (scalar) tensors".to_string(),
607 ));
608 }
609 Ok(())
610 }
611
612 Self::Csr(_) => {
613 if shape.rank() != 2 {
614 return Err(Error::InvalidLayout(format!(
615 "CSR layout requires rank 2, got rank {}",
616 shape.rank()
617 )));
618 }
619 Ok(())
620 }
621
622 Self::Csc(_) => {
623 if shape.rank() != 2 {
624 return Err(Error::InvalidLayout(format!(
625 "CSC layout requires rank 2, got rank {}",
626 shape.rank()
627 )));
628 }
629 Ok(())
630 }
631
632 Self::Csf(c) => {
633 let rank = shape.rank();
634 // Spec csf.md §Validity Constraints: rank MUST be >= 3.
635 if rank < 3 {
636 return Err(Error::InvalidLayout(format!(
637 "CSF layout requires rank >= 3, got rank {rank}"
638 )));
639 }
640 // mode_order length must equal rank.
641 if c.mode_order.len() != rank {
642 return Err(Error::InvalidLayout(format!(
643 "CSF layout: mode_order.len() ({}) != shape.rank() ({rank})",
644 c.mode_order.len()
645 )));
646 }
647 // mode_order must be a permutation of 0..rank.
648 // Use a presence-bitmap approach: O(rank), no allocation for rank ≤ 64.
649 let mut seen = 0u64;
650 for (level, &dim) in c.mode_order.iter().enumerate() {
651 if dim as usize >= rank {
652 return Err(Error::InvalidLayout(format!(
653 "CSF layout: mode_order[{level}]={dim} out of range [0, {rank})"
654 )));
655 }
656 let bit = 1u64 << dim;
657 if seen & bit != 0 {
658 return Err(Error::InvalidLayout(format!(
659 "CSF layout: mode_order contains duplicate value {dim}"
660 )));
661 }
662 seen |= bit;
663 }
664 Ok(())
665 }
666
667 Self::BlockPaged(bp) => {
668 // Spec: block-paged is defined only for rank-3 tensors.
669 if shape.rank() != 3 {
670 return Err(Error::InvalidLayout(format!(
671 "block-paged layout requires rank 3, got rank {}",
672 shape.rank()
673 )));
674 }
675 // Spec: paged_axis MUST be 0 in this version.
676 if bp.paged_axis != 0 {
677 return Err(Error::InvalidLayout(format!(
678 "block-paged layout: paged_axis must be 0, got {}",
679 bp.paged_axis
680 )));
681 }
682 // Spec: page_size MUST be >= 1.
683 if bp.page_size < 1 {
684 return Err(Error::InvalidLayout(
685 "block-paged layout: page_size must be >= 1".to_string(),
686 ));
687 }
688 // kv_role and block_table_index_type are strongly typed enums:
689 // all wire-representable values are valid here. No additional
690 // check needed — invalid wire bytes are rejected at decode time.
691 Ok(())
692 }
693
694 Self::Composite(c) => {
695 // Spec (composite.md § Head Descriptor): partition and overlay
696 // heads present a coverage/box-addressed index space, so a DYNAMIC
697 // dimension (unresolved size) would make coverage math ill-defined.
698 // Group has no spatial semantics, so it is unaffected. This check has
699 // the composition rule in hand here (unlike a bare shape check), so it
700 // belongs at this layer rather than being deferred to the cross-member
701 // validator in `crate::composite`, which only sees whole descriptors.
702 let needs_static_shape = !matches!(c.rule, CompositionRule::Group);
703 if needs_static_shape && shape.has_dynamic() {
704 return Err(Error::InvalidLayout(
705 "composite head: partition/overlay composition requires a fully \
706 static shape (no DYNAMIC dimension)"
707 .to_string(),
708 ));
709 }
710 Ok(())
711 }
712
713 Self::Hilbert(h) => {
714 if h.hilbert_rank as usize != shape.rank() {
715 return Err(Error::InvalidLayout(format!(
716 "Hilbert layout: hilbert_rank ({}) != shape.rank() ({})",
717 h.hilbert_rank,
718 shape.rank()
719 )));
720 }
721 // hilbert_rank >= 2 already enforced by HilbertLayout::new.
722 // Each static dim must equal 2^hilbert_order.
723 let expected_dim = 1u64.checked_shl(h.hilbert_order).ok_or_else(|| {
724 Error::InvalidLayout(format!(
725 "Hilbert layout: hilbert_order ({}) would require dimension > u64::MAX",
726 h.hilbert_order
727 ))
728 })?;
729 for (k, &dim) in shape.dims().iter().enumerate() {
730 if dim == crate::shape::DYNAMIC {
731 continue;
732 }
733 if dim != expected_dim {
734 return Err(Error::InvalidLayout(format!(
735 "Hilbert layout: shape[{k}] ({dim}) != 2^hilbert_order ({expected_dim})"
736 )));
737 }
738 }
739 Ok(())
740 }
741
742 // Private-extension: shape constraints are implementation-defined.
743 Self::PrivateExtension(_) => Ok(()),
744
745 // Unknown: permissive mode — accept without validation.
746 Self::Unknown(_) => Ok(()),
747 }
748 }
749
750 /// Returns the linear element offset for a multi-dimensional logical index.
751 ///
752 /// Dispatches to the layout-specific [`addressing::ElementAddress`] implementation.
753 /// For multi-buffer (sparse) layouts, returns [`Error::LayoutRequiresMultiBuffer`] —
754 /// use the layout-specific addressing method instead.
755 ///
756 /// # Examples
757 ///
758 /// ```
759 /// use hurray_core::layout::LayoutDescriptor;
760 /// use hurray_core::Shape;
761 ///
762 /// let shape = Shape::new(vec![3, 4]).unwrap();
763 ///
764 /// // Row-major: element [1, 2] → offset 1*4 + 2 = 6.
765 /// assert_eq!(LayoutDescriptor::RowMajor.element_offset(&[1, 2], &shape).unwrap(), 6);
766 ///
767 /// // Column-major: element [1, 2] → offset 1*1 + 2*3 = 7.
768 /// assert_eq!(LayoutDescriptor::ColMajor.element_offset(&[1, 2], &shape).unwrap(), 7);
769 /// ```
770 pub fn element_offset(&self, index: &[u64], shape: &Shape) -> crate::Result<u64> {
771 use addressing::ElementAddress;
772 match self {
773 Self::RowMajor => addressing::row_major::element_offset(index, shape),
774 Self::ColMajor => addressing::col_major::element_offset(index, shape),
775 Self::Strided(s) => s.element_offset(index, shape),
776 Self::Tiled(t) => t.element_offset(index, shape),
777 Self::Morton(m) => m.element_offset(index, shape),
778 Self::Hilbert(h) => h.element_offset(index, shape),
779 Self::Coo(_) => Err(crate::Error::LayoutRequiresMultiBuffer {
780 layout_tag: TAG_COO,
781 }),
782 Self::Csr(_) => Err(crate::Error::LayoutRequiresMultiBuffer {
783 layout_tag: TAG_CSR,
784 }),
785 Self::Csc(_) => Err(crate::Error::LayoutRequiresMultiBuffer {
786 layout_tag: TAG_CSC,
787 }),
788 // CSF is sparse multi-buffer; use addressing::csf::element_offset with index buffers.
789 Self::Csf(_) => Err(crate::Error::LayoutRequiresMultiBuffer {
790 layout_tag: TAG_CSF,
791 }),
792 // BlockPaged is indirect (multi-buffer); use addressing::block_paged directly.
793 Self::BlockPaged(_) => Err(crate::Error::LayoutRequiresMultiBuffer {
794 layout_tag: TAG_BLOCK_PAGED,
795 }),
796 // Composite is virtual (zero buffers, not "many buffers") — a distinct failure
797 // mode from LayoutRequiresMultiBuffer, so it gets its own error rather than
798 // overloading that variant's "use the layout-specific method instead" framing.
799 // Addressing a composite means resolving to a member first; see
800 // `crate::composite::CompositeTensor`.
801 Self::Composite(_) => Err(crate::Error::LayoutIsVirtual {
802 layout_tag: TAG_COMPOSITE,
803 }),
804 Self::PrivateExtension(p) => Err(crate::Error::PrivateLayoutTag(p.tag)),
805 Self::Unknown(u) => Err(crate::Error::UnknownLayoutTag(u.tag)),
806 }
807 }
808}
809
810// ── Tests ─────────────────────────────────────────────────────────────────────
811
812#[cfg(test)]
813mod tests {
814 use super::*;
815
816 // ── tag() ─────────────────────────────────────────────────────────────────
817
818 #[test]
819 fn tag_values_match_spec() {
820 assert_eq!(LayoutDescriptor::RowMajor.tag(), 0x01);
821 assert_eq!(LayoutDescriptor::ColMajor.tag(), 0x02);
822 assert_eq!(
823 LayoutDescriptor::Strided(StridedLayout::new(vec![1])).tag(),
824 0x03
825 );
826 assert_eq!(
827 LayoutDescriptor::Tiled(Box::new(
828 TiledLayout::new(vec![4], 0x01, 0x01, None, None, None).unwrap()
829 ))
830 .tag(),
831 0x04
832 );
833 assert_eq!(
834 LayoutDescriptor::Morton(MortonLayout::new(vec![2]).unwrap()).tag(),
835 0x05
836 );
837 assert_eq!(
838 LayoutDescriptor::Coo(CooLayout {
839 nnz: 0,
840 is_sorted: false
841 })
842 .tag(),
843 0x06
844 );
845 assert_eq!(LayoutDescriptor::Csr(CsrLayout { nnz: 0 }).tag(), 0x07);
846 assert_eq!(LayoutDescriptor::Csc(CscLayout { nnz: 0 }).tag(), 0x08);
847 assert_eq!(
848 LayoutDescriptor::Csf(CsfLayout::new(0, vec![0, 1, 2])).tag(),
849 0x09
850 );
851 assert_eq!(
852 LayoutDescriptor::Hilbert(HilbertLayout::new(2, 2).unwrap()).tag(),
853 0x40
854 );
855 }
856
857 // ── buffer_count() ────────────────────────────────────────────────────────
858
859 #[test]
860 fn dense_layouts_have_buffer_count_1() {
861 let dense = [
862 LayoutDescriptor::RowMajor,
863 LayoutDescriptor::ColMajor,
864 LayoutDescriptor::Strided(StridedLayout::new(vec![4, 1])),
865 LayoutDescriptor::Tiled(Box::new(
866 TiledLayout::new(vec![4, 4], 0x01, 0x01, None, None, None).unwrap(),
867 )),
868 LayoutDescriptor::Morton(MortonLayout::new(vec![2, 2]).unwrap()),
869 LayoutDescriptor::Hilbert(HilbertLayout::new(2, 2).unwrap()),
870 ];
871 for layout in &dense {
872 assert_eq!(
873 layout.buffer_count(),
874 NonZeroU8::new(1),
875 "expected buffer_count=1 for {:?}",
876 layout.tag()
877 );
878 }
879 }
880
881 #[test]
882 fn coo_has_buffer_count_2() {
883 assert_eq!(
884 LayoutDescriptor::Coo(CooLayout {
885 nnz: 0,
886 is_sorted: false
887 })
888 .buffer_count(),
889 NonZeroU8::new(2)
890 );
891 }
892
893 #[test]
894 fn csr_has_buffer_count_3() {
895 assert_eq!(
896 LayoutDescriptor::Csr(CsrLayout { nnz: 0 }).buffer_count(),
897 NonZeroU8::new(3)
898 );
899 }
900
901 #[test]
902 fn csc_has_buffer_count_3() {
903 assert_eq!(
904 LayoutDescriptor::Csc(CscLayout { nnz: 0 }).buffer_count(),
905 NonZeroU8::new(3)
906 );
907 }
908
909 #[test]
910 fn private_extension_buffer_count_is_none() {
911 let layout = LayoutDescriptor::PrivateExtension(
912 PrivateExtensionLayout::new(0xF0, 0, vec![]).unwrap(),
913 );
914 assert!(layout.buffer_count().is_none());
915 }
916
917 #[test]
918 fn unknown_buffer_count_is_none() {
919 // Use a tag in the reserved range (0x0A is now the last named tag, block-paged).
920 let layout = LayoutDescriptor::Unknown(UnknownLayout {
921 tag: 0x0C,
922 raw_bytes: vec![],
923 });
924 assert!(layout.buffer_count().is_none());
925 }
926
927 // ── validate_layout_tag_strict ────────────────────────────────────────────
928
929 #[test]
930 fn known_tags_pass_strict_validation() {
931 for tag in [
932 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x40,
933 ] {
934 assert!(
935 validate_layout_tag_strict(tag).is_ok(),
936 "tag 0x{tag:02X} should be valid"
937 );
938 }
939 }
940
941 #[test]
942 fn invalid_sentinels_rejected() {
943 assert!(matches!(
944 validate_layout_tag_strict(0x00),
945 Err(Error::InvalidLayoutTag(0x00))
946 ));
947 assert!(matches!(
948 validate_layout_tag_strict(0xFF),
949 Err(Error::InvalidLayoutTag(0xFF))
950 ));
951 }
952
953 #[test]
954 fn reserved_tags_rejected() {
955 // 0x0A is block-paged and 0x0B is the composite / virtual head (ADR-027)
956 // — both named tags, not reserved. 0x0C is now the lower boundary.
957 for tag in [0x0C_u8, 0x3F, 0x41, 0x7F, 0x80, 0xEF] {
958 assert!(
959 matches!(
960 validate_layout_tag_strict(tag),
961 Err(Error::ReservedLayoutTag(_))
962 ),
963 "tag 0x{tag:02X} should be ReservedLayoutTag"
964 );
965 }
966 }
967
968 #[test]
969 fn private_tags_rejected_in_strict_mode() {
970 for tag in [0xF0_u8, 0xF5, 0xFE] {
971 assert!(
972 matches!(
973 validate_layout_tag_strict(tag),
974 Err(Error::PrivateLayoutTag(_))
975 ),
976 "tag 0x{tag:02X} should be PrivateLayoutTag"
977 );
978 }
979 }
980
981 // ── validate_against_shape ────────────────────────────────────────────────
982
983 #[test]
984 fn row_major_valid_for_any_rank() {
985 let shape = Shape::new(vec![3, 4]).unwrap();
986 assert!(LayoutDescriptor::RowMajor
987 .validate_against_shape(&shape)
988 .is_ok());
989 assert!(LayoutDescriptor::ColMajor
990 .validate_against_shape(&Shape::scalar())
991 .is_ok());
992 }
993
994 #[test]
995 fn strided_valid_when_strides_len_matches_rank() {
996 let shape = Shape::new(vec![3, 4]).unwrap();
997 let layout = LayoutDescriptor::Strided(StridedLayout::new(vec![4, 1]));
998 assert!(layout.validate_against_shape(&shape).is_ok());
999 }
1000
1001 #[test]
1002 fn strided_invalid_when_strides_len_mismatches_rank() {
1003 let shape = Shape::new(vec![3, 4]).unwrap();
1004 let layout = LayoutDescriptor::Strided(StridedLayout::new(vec![1]));
1005 assert!(matches!(
1006 layout.validate_against_shape(&shape),
1007 Err(Error::InvalidLayout(_))
1008 ));
1009 }
1010
1011 #[test]
1012 fn strided_invalid_for_scalar() {
1013 let layout = LayoutDescriptor::Strided(StridedLayout::new(vec![]));
1014 assert!(matches!(
1015 layout.validate_against_shape(&Shape::scalar()),
1016 Err(Error::InvalidLayout(_))
1017 ));
1018 }
1019
1020 #[test]
1021 fn csr_valid_for_rank_2() {
1022 let shape = Shape::new(vec![4, 5]).unwrap();
1023 let layout = LayoutDescriptor::Csr(CsrLayout { nnz: 0 });
1024 assert!(layout.validate_against_shape(&shape).is_ok());
1025 }
1026
1027 #[test]
1028 fn csr_invalid_for_rank_3() {
1029 let shape = Shape::new(vec![2, 3, 4]).unwrap();
1030 let layout = LayoutDescriptor::Csr(CsrLayout { nnz: 0 });
1031 assert!(matches!(
1032 layout.validate_against_shape(&shape),
1033 Err(Error::InvalidLayout(_))
1034 ));
1035 }
1036
1037 #[test]
1038 fn coo_invalid_for_scalar() {
1039 let layout = LayoutDescriptor::Coo(CooLayout {
1040 nnz: 0,
1041 is_sorted: false,
1042 });
1043 assert!(matches!(
1044 layout.validate_against_shape(&Shape::scalar()),
1045 Err(Error::InvalidLayout(_))
1046 ));
1047 }
1048
1049 #[test]
1050 fn coo_valid_for_rank_1() {
1051 let shape = Shape::new(vec![10]).unwrap();
1052 let layout = LayoutDescriptor::Coo(CooLayout {
1053 nnz: 3,
1054 is_sorted: true,
1055 });
1056 assert!(layout.validate_against_shape(&shape).is_ok());
1057 }
1058
1059 #[test]
1060 fn hilbert_valid_for_4x4_order2() {
1061 let shape = Shape::new(vec![4, 4]).unwrap();
1062 let layout = LayoutDescriptor::Hilbert(HilbertLayout::new(2, 2).unwrap());
1063 assert!(layout.validate_against_shape(&shape).is_ok());
1064 }
1065
1066 #[test]
1067 fn hilbert_invalid_when_dim_not_power_of_two_order() {
1068 let shape = Shape::new(vec![3, 4]).unwrap(); // 3 != 2^2
1069 let layout = LayoutDescriptor::Hilbert(HilbertLayout::new(2, 2).unwrap());
1070 assert!(matches!(
1071 layout.validate_against_shape(&shape),
1072 Err(Error::InvalidLayout(_))
1073 ));
1074 }
1075
1076 #[test]
1077 fn morton_invalid_when_shape_exceeds_bits() {
1078 // morton_bits=[1,1] → max dim = 2^1 = 2; shape[0]=3 > 2.
1079 let shape = Shape::new(vec![3, 2]).unwrap();
1080 let layout = LayoutDescriptor::Morton(MortonLayout::new(vec![1, 1]).unwrap());
1081 assert!(matches!(
1082 layout.validate_against_shape(&shape),
1083 Err(Error::InvalidLayout(_))
1084 ));
1085 }
1086
1087 #[test]
1088 fn unknown_always_passes_validation() {
1089 // 0x0A is now block-paged; use a reserved-range tag for Unknown.
1090 let layout = LayoutDescriptor::Unknown(UnknownLayout {
1091 tag: 0x0C,
1092 raw_bytes: vec![],
1093 });
1094 assert!(layout.validate_against_shape(&Shape::scalar()).is_ok());
1095 assert!(layout
1096 .validate_against_shape(&Shape::new(vec![100, 200]).unwrap())
1097 .is_ok());
1098 }
1099}