Skip to main content

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}