Skip to main content

hurray_core/descriptor/
mod.rs

1//! Tensor descriptor — binary encoding and decoding.
2//!
3//! The [`TensorDescriptor`] type is the top-level carrier for all metadata
4//! required to interpret a tensor's data buffer: element type, rank, shape,
5//! memory layout, buffer handles, and optional quantization, shard, statistics,
6//! and extension-type annotations.
7//!
8//! # Wire format
9//!
10//! Defined in `docs/spec/metadata.md`. Fixed header (20 bytes) followed by
11//! variable-length core fields, layout-specific payload, buffer table, and
12//! up to four optional sections selected by the `flags` bitmask.
13//!
14//! # Examples
15//!
16//! ```
17//! use hurray_core::{
18//!     BufferHandle, DeviceTag, ElementType, Shape, SyncMode, MIN_BUFFER_ALIGNMENT,
19//!     descriptor::TensorDescriptor,
20//!     layout::LayoutDescriptor,
21//! };
22//!
23//! // Build a float32 [3, 4] row-major tensor descriptor (the spec's worked example).
24//! let shape = Shape::new(vec![3, 4]).unwrap();
25//! let buffer = BufferHandle::new(192, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
26//! let desc = TensorDescriptor::new(
27//!     1, 0,
28//!     ElementType::Float32,
29//!     shape,
30//!     0,
31//!     LayoutDescriptor::RowMajor,
32//!     vec![buffer],
33//!     None, None, None, None,
34//! ).unwrap();
35//!
36//! let encoded = desc.encode().unwrap();
37//! assert_eq!(encoded.len(), 61); // spec worked example is 61 bytes
38//!
39//! let decoded = TensorDescriptor::decode(&encoded).unwrap();
40//! assert_eq!(decoded, desc);
41//! ```
42
43// ── Sub-modules ───────────────────────────────────────────────────────────────
44
45pub mod composite_member;
46pub(crate) mod cursor;
47mod decode;
48mod encode;
49pub mod ext_type;
50pub mod layout_codec;
51pub mod shard;
52pub mod statistics;
53
54// Internal type alias used by encode.rs / decode.rs to import TensorDescriptor
55// without a circular path.
56pub(crate) mod mod_types {
57    pub(crate) use super::{DescriptorFlags, TensorDescriptor, RESERVED_FLAGS_MASK};
58}
59
60// ── Public re-exports ─────────────────────────────────────────────────────────
61
62pub use composite_member::{CompositeMemberDescriptor, MemberRole};
63pub use ext_type::ExtensionTypeDescriptor;
64pub use shard::ShardDescriptor;
65pub use statistics::{Statistics, StatisticsMask};
66
67// ── Constants ─────────────────────────────────────────────────────────────────
68
69/// Magic bytes that open every Hurray tensor descriptor: ASCII `"HRRY"`.
70///
71/// ```
72/// use hurray_core::descriptor::MAGIC;
73/// assert_eq!(&MAGIC, b"HRRY");
74/// ```
75pub const MAGIC: [u8; 4] = *b"HRRY";
76
77/// Current major version of the descriptor format.
78pub const DESCRIPTOR_VERSION_MAJOR: u8 = 1;
79
80/// Current minor version of the descriptor format.
81pub const DESCRIPTOR_VERSION_MINOR: u8 = 0;
82
83/// Minimum valid value for the `descriptor_length` field (= fixed header size).
84pub(crate) const MIN_DESCRIPTOR_LEN: u32 = 20;
85
86/// Bitmask of all reserved flag bits (bits 5–31). Readers MUST reject descriptors
87/// with any reserved bit set.
88pub(crate) const RESERVED_FLAGS_MASK: u32 = !0x1F;
89
90// ── DescriptorFlags ───────────────────────────────────────────────────────────
91
92/// Descriptor flags bitmask (wire field `flags`, offset 10 in the fixed header).
93///
94/// Each bit selects an optional section that follows the buffer table. Bits 4–31
95/// are reserved and MUST be `0`.
96///
97/// # Examples
98///
99/// ```
100/// use hurray_core::descriptor::DescriptorFlags;
101///
102/// let f = DescriptorFlags(DescriptorFlags::HAS_QUANTIZATION | DescriptorFlags::HAS_SHARD);
103/// assert!(f.has_quantization());
104/// assert!(f.has_shard());
105/// assert!(!f.has_statistics());
106/// assert!(!f.has_extension_type());
107/// assert!(!f.has_composite_member());
108/// ```
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub struct DescriptorFlags(pub u32);
111
112impl DescriptorFlags {
113    /// Bit 0: quantization section is present.
114    pub const HAS_QUANTIZATION: u32 = 1 << 0;
115    /// Bit 1: shard section is present.
116    pub const HAS_SHARD: u32 = 1 << 1;
117    /// Bit 2: extension type section is present.
118    pub const HAS_EXTENSION_TYPE: u32 = 1 << 2;
119    /// Bit 3: statistics section is present.
120    pub const HAS_STATISTICS: u32 = 1 << 3;
121    /// Bit 4: Composite Member section is present. Set on the members of an
122    /// overlay composite; see `docs/spec/layouts/composite.md`.
123    pub const HAS_COMPOSITE_MEMBER: u32 = 1 << 4;
124
125    /// Returns `true` if the `HAS_QUANTIZATION` bit is set.
126    #[inline]
127    pub fn has_quantization(self) -> bool {
128        self.0 & Self::HAS_QUANTIZATION != 0
129    }
130
131    /// Returns `true` if the `HAS_SHARD` bit is set.
132    #[inline]
133    pub fn has_shard(self) -> bool {
134        self.0 & Self::HAS_SHARD != 0
135    }
136
137    /// Returns `true` if the `HAS_EXTENSION_TYPE` bit is set.
138    #[inline]
139    pub fn has_extension_type(self) -> bool {
140        self.0 & Self::HAS_EXTENSION_TYPE != 0
141    }
142
143    /// Returns `true` if the `HAS_STATISTICS` bit is set.
144    #[inline]
145    pub fn has_statistics(self) -> bool {
146        self.0 & Self::HAS_STATISTICS != 0
147    }
148
149    /// Returns `true` if the `HAS_COMPOSITE_MEMBER` bit is set.
150    #[inline]
151    pub fn has_composite_member(self) -> bool {
152        self.0 & Self::HAS_COMPOSITE_MEMBER != 0
153    }
154}
155
156// ── TensorDescriptor ──────────────────────────────────────────────────────────
157
158use crate::descriptor::composite_member::CompositeMemberDescriptor as CompositeMemberDesc;
159use crate::descriptor::ext_type::ExtensionTypeDescriptor as ExtDesc;
160use crate::descriptor::shard::ShardDescriptor as ShardDesc;
161use crate::descriptor::statistics::Statistics as Stats;
162use crate::quantization::QuantizationDescriptor;
163use crate::{BufferHandle, ElementType, Error, LayoutDescriptor, Result, Shape};
164
165/// The top-level tensor descriptor carrying all metadata required to interpret
166/// a tensor's data buffer.
167///
168/// Construct via [`TensorDescriptor::new`], encode to bytes with
169/// [`TensorDescriptor::encode`], and decode from bytes with
170/// [`TensorDescriptor::decode`].
171///
172/// Derives `PartialEq` but NOT `Eq` — the `statistics` field may contain `f64`
173/// values, and NaN semantics make `Eq` unsound for those fields.
174///
175/// # Examples
176///
177/// ```
178/// use hurray_core::{
179///     BufferHandle, DeviceTag, ElementType, Shape, SyncMode, MIN_BUFFER_ALIGNMENT,
180///     descriptor::TensorDescriptor,
181///     layout::LayoutDescriptor,
182/// };
183///
184/// let shape  = Shape::new(vec![3u64, 4]).unwrap();
185/// let buffer = BufferHandle::new(192, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
186/// let desc   = TensorDescriptor::new(
187///     1, 0,
188///     ElementType::Float32,
189///     shape,
190///     0,
191///     LayoutDescriptor::RowMajor,
192///     vec![buffer],
193///     None, None, None, None,
194/// ).unwrap();
195///
196/// assert_eq!(desc.version_major, 1);
197/// assert_eq!(desc.element_type, ElementType::Float32);
198/// assert_eq!(desc.shape.rank(), 2);
199/// ```
200#[derive(Debug, Clone, PartialEq)]
201pub struct TensorDescriptor {
202    /// Major format version stored in the wire header.
203    pub version_major: u8,
204    /// Minor format version stored in the wire header.
205    pub version_minor: u8,
206    /// Element type tag for this tensor's data.
207    pub element_type: ElementType,
208    /// Tensor shape (rank and dimension sizes).
209    pub shape: Shape,
210    /// Byte offset from the start of buffer 0 to logical element `[0,…,0]`.
211    pub byte_offset: u64,
212    /// Memory layout descriptor.
213    pub layout: LayoutDescriptor,
214    /// Buffer handles (at least one required).
215    pub buffers: Vec<BufferHandle>,
216    /// Raw quantization payload (opaque `quantization_descriptor` bytes).
217    ///
218    /// When present the bytes are stored verbatim; typed decode is not performed
219    /// in this layer (design decision: typed quantization decode is deferred to
220    /// a higher layer that has schema context).
221    pub quantization: Option<Vec<u8>>,
222    /// Shard annotation (this tensor is a sub-region of a larger parent).
223    pub shard: Option<ShardDesc>,
224    /// Advisory statistics about the tensor's data buffer.
225    pub statistics: Option<Stats>,
226    /// Extension type descriptor (present iff `type_tag` is in `0xF0`–`0xFE`).
227    pub extension_type: Option<ExtDesc>,
228    /// Composite Member section (this tensor's role within an enclosing overlay
229    /// composite). `None` by default; set via [`TensorDescriptor::with_composite_member`].
230    ///
231    /// Not a [`TensorDescriptor::new`] parameter — a composite head declares
232    /// `member_count` before its members exist, so member-role assignment is
233    /// necessarily a post-construction, opt-in step (see ADR-027).
234    pub composite_member: Option<CompositeMemberDesc>,
235}
236
237impl TensorDescriptor {
238    /// Creates a new [`TensorDescriptor`], validating invariants.
239    ///
240    /// # Errors
241    ///
242    /// - [`Error::EmptyBufferTable`] — `buffers` is empty (and `layout` is not the
243    ///   composite head, `layout_tag = 0x0B`, for which an empty buffer table is
244    ///   the *only* valid table — see [`Error::CompositeHeadHasBuffers`]).
245    /// - [`Error::CompositeHeadHasBuffers`] — `layout` is the composite head but
246    ///   `buffers` is non-empty.
247    /// - [`Error::CompositeHeadHasByteOffset`] — `layout` is the composite head but
248    ///   `byte_offset != 0`.
249    /// - [`Error::CompositeHeadHasQuantization`] — `layout` is the composite head but
250    ///   `quantization` is `Some`.
251    /// - [`Error::ExtensionTypeFlagMismatch`] — `extension_type` is `Some` but
252    ///   `element_type.tag()` is not in `0xF0`–`0xFE`, or vice-versa.
253    /// - [`Error::InvalidShape`] — `shard.parent_shape.len() != shape.rank()`.
254    /// - [`Error::InvalidLayout`] — a block-paged or CSF layout carries a `shard` descriptor
255    ///   (spec § Sharding).
256    /// - [`Error::InvalidQuantization`] — a block-paged layout carries a `quantization`
257    ///   descriptor incompatible with the paged layout (spec § Quantization Compatibility).
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use hurray_core::{
263    ///     BufferHandle, DeviceTag, ElementType, Shape, SyncMode, MIN_BUFFER_ALIGNMENT,
264    ///     descriptor::TensorDescriptor,
265    ///     layout::LayoutDescriptor,
266    /// };
267    ///
268    /// let shape  = Shape::new(vec![2u64, 3]).unwrap();
269    /// let buffer = BufferHandle::new(64, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
270    /// let desc   = TensorDescriptor::new(
271    ///     1, 0, ElementType::Float32, shape, 0,
272    ///     LayoutDescriptor::RowMajor, vec![buffer],
273    ///     None, None, None, None,
274    /// ).unwrap();
275    /// assert_eq!(desc.buffers.len(), 1);
276    /// ```
277    #[allow(clippy::too_many_arguments)]
278    pub fn new(
279        version_major: u8,
280        version_minor: u8,
281        element_type: ElementType,
282        shape: Shape,
283        byte_offset: u64,
284        layout: LayoutDescriptor,
285        buffers: Vec<BufferHandle>,
286        quantization: Option<Vec<u8>>,
287        shard: Option<ShardDesc>,
288        statistics: Option<Stats>,
289        extension_type: Option<ExtDesc>,
290    ) -> Result<Self> {
291        // Invariant (ADR-027 D-1): buffer_count == 0 is allowed only for the composite
292        // head (layout_tag 0x0B), which owns no data. A composite head additionally MUST
293        // have byte_offset == 0 and MUST NOT carry a quantization section — checked here,
294        // the seam where layout, buffers, byte_offset, and quantization are all in hand.
295        let is_composite_head = matches!(&layout, LayoutDescriptor::Composite(_));
296        if is_composite_head {
297            if !buffers.is_empty() {
298                return Err(Error::CompositeHeadHasBuffers {
299                    count: buffers.len().min(u8::MAX as usize) as u8,
300                });
301            }
302            if byte_offset != 0 {
303                return Err(Error::CompositeHeadHasByteOffset { byte_offset });
304            }
305            if quantization.is_some() {
306                return Err(Error::CompositeHeadHasQuantization);
307            }
308        } else if buffers.is_empty() {
309            return Err(Error::EmptyBufferTable);
310        }
311
312        // Invariant: buffer_count fits in uint8 (wire field ceiling is 255).
313        if buffers.len() > 255 {
314            return Err(Error::InvalidLayout(format!(
315                "buffer_count {} exceeds the maximum of 255 (uint8 ceiling)",
316                buffers.len()
317            )));
318        }
319
320        // Invariant: HAS_EXTENSION_TYPE ↔ type_tag in 0xF0–0xFE.
321        let is_ext_tag = matches!(element_type.tag(), 0xF0..=0xFE);
322        let has_ext = extension_type.is_some();
323        if is_ext_tag != has_ext {
324            return Err(Error::ExtensionTypeFlagMismatch {
325                flag_set: has_ext,
326                type_tag: element_type.tag(),
327                type_tag_in_range: if is_ext_tag {
328                    "in 0xF0-0xFE"
329                } else {
330                    "not in 0xF0-0xFE"
331                },
332            });
333        }
334
335        // Invariant: shard rank matches tensor rank.
336        if let Some(s) = &shard {
337            if s.parent_shape.len() != shape.rank() {
338                return Err(Error::InvalidShape(format!(
339                    "shard.parent_shape.len() ({}) != shape.rank() ({})",
340                    s.parent_shape.len(),
341                    shape.rank()
342                )));
343            }
344
345            // Spec (block-paged.md § Sharding): a block-paged descriptor MUST NOT
346            // carry a shard descriptor. Enforced here — the cross-section seam where
347            // both layout and shard are decoded — rather than in the layout codec.
348            if matches!(&layout, LayoutDescriptor::BlockPaged(_)) {
349                return Err(Error::InvalidLayout(
350                    "block-paged layout MUST NOT carry a shard descriptor (spec § Sharding)"
351                        .to_string(),
352                ));
353            }
354
355            // Spec (csf.md § Sharding): a CSF descriptor MUST NOT carry a shard
356            // descriptor in this version. Same cross-section seam as block-paged.
357            if matches!(&layout, LayoutDescriptor::Csf(_)) {
358                return Err(Error::InvalidLayout(
359                    "CSF layout MUST NOT carry a shard descriptor (spec § Sharding)".to_string(),
360                ));
361            }
362        }
363
364        // Spec (block-paged.md § Quantization Compatibility): a block-paged layout carrying a
365        // quantization descriptor must use a quant axis/block_size compatible with the paged
366        // layout. Enforced here — the cross-section seam where the typed layout and the raw
367        // quantization bytes are both in hand — by decoding the descriptor and delegating to
368        // the layout's own rule. (The bytes are stored raw on the descriptor; this is the one
369        // place both are known, mirroring the shard rejection above.)
370        if let (LayoutDescriptor::BlockPaged(bp), Some(q)) = (&layout, &quantization) {
371            let (qd, _) = QuantizationDescriptor::decode(q)?;
372            let (axis, block_size) = match &qd {
373                QuantizationDescriptor::PerChannelAffine(x) => (x.axis(), 0u32),
374                QuantizationDescriptor::PerBlockAffine(x) => (x.axis(), x.block_size()),
375                QuantizationDescriptor::Nf4(x) => (x.axis(), x.block_size()),
376                QuantizationDescriptor::Mxfp(x) => (x.axis(), x.block_size()),
377                QuantizationDescriptor::PerTensorAffine(_) => (0u32, 0u32),
378            };
379            bp.validate_quantization_compatibility(qd.scheme_tag().tag(), axis, block_size)?;
380        }
381
382        Ok(Self {
383            version_major,
384            version_minor,
385            element_type,
386            shape,
387            byte_offset,
388            layout,
389            buffers,
390            quantization,
391            shard,
392            statistics,
393            extension_type,
394            composite_member: None,
395        })
396    }
397
398    /// Attaches a [`CompositeMemberDesc`] (this tensor's role within an enclosing
399    /// overlay composite), returning `self` for chaining.
400    ///
401    /// Opt-in and separate from [`TensorDescriptor::new`] because a composite head
402    /// declares `member_count` before its members exist (see ADR-027 § D2); member
403    /// role assignment is necessarily a step applied after a member descriptor is
404    /// otherwise fully built.
405    ///
406    /// The one local cross-field invariant that applies regardless of composition
407    /// context — a composite head (`layout_tag = 0x0B`) MUST NOT itself carry a
408    /// Composite Member section — cannot be rejected here without breaking the
409    /// infallible `Self` return type this builder is specified to have; it is
410    /// re-validated at [`TensorDescriptor::encode`] / [`TensorDescriptor::decode`]
411    /// time instead, so the invariant still holds for every descriptor that
412    /// round-trips through the wire format.
413    ///
414    /// # Examples
415    ///
416    /// ```
417    /// use hurray_core::{
418    ///     BufferHandle, DeviceTag, ElementType, Shape, SyncMode, MIN_BUFFER_ALIGNMENT,
419    ///     descriptor::{CompositeMemberDescriptor, MemberRole, TensorDescriptor},
420    ///     layout::LayoutDescriptor,
421    /// };
422    ///
423    /// let shape  = Shape::new(vec![4u64]).unwrap();
424    /// let buffer = BufferHandle::new(64, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
425    /// let member = TensorDescriptor::new(
426    ///     1, 0, ElementType::Float32, shape, 0,
427    ///     LayoutDescriptor::RowMajor, vec![buffer],
428    ///     None, None, None, None,
429    /// )
430    /// .unwrap()
431    /// .with_composite_member(CompositeMemberDescriptor::new(MemberRole::Base));
432    ///
433    /// assert!(member.flags().has_composite_member());
434    /// ```
435    pub fn with_composite_member(mut self, cm: CompositeMemberDesc) -> Self {
436        self.composite_member = Some(cm);
437        self
438    }
439
440    /// Derives the [`DescriptorFlags`] bitmask from which optional sections are present.
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// use hurray_core::{
446    ///     BufferHandle, DeviceTag, ElementType, Shape, SyncMode, MIN_BUFFER_ALIGNMENT,
447    ///     descriptor::{TensorDescriptor, DescriptorFlags},
448    ///     layout::LayoutDescriptor,
449    /// };
450    ///
451    /// let shape  = Shape::new(vec![4u64]).unwrap();
452    /// let buffer = BufferHandle::new(64, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
453    /// let desc   = TensorDescriptor::new(
454    ///     1, 0, ElementType::Float32, shape, 0,
455    ///     LayoutDescriptor::RowMajor, vec![buffer],
456    ///     None, None, None, None,
457    /// ).unwrap();
458    /// assert_eq!(desc.flags().0, 0);
459    /// ```
460    pub fn flags(&self) -> DescriptorFlags {
461        let mut flags = 0u32;
462        if self.quantization.is_some() {
463            flags |= DescriptorFlags::HAS_QUANTIZATION;
464        }
465        if self.shard.is_some() {
466            flags |= DescriptorFlags::HAS_SHARD;
467        }
468        if self.extension_type.is_some() {
469            flags |= DescriptorFlags::HAS_EXTENSION_TYPE;
470        }
471        if self.statistics.is_some() {
472            flags |= DescriptorFlags::HAS_STATISTICS;
473        }
474        if self.composite_member.is_some() {
475            flags |= DescriptorFlags::HAS_COMPOSITE_MEMBER;
476        }
477        DescriptorFlags(flags)
478    }
479
480    /// Encodes this descriptor to its wire representation.
481    ///
482    /// # Errors
483    ///
484    /// Returns [`Error::DescriptorLengthMismatch`] if the encoded length exceeds
485    /// `u32::MAX` (practically impossible for well-formed descriptors).
486    ///
487    /// # Examples
488    ///
489    /// ```
490    /// use hurray_core::{
491    ///     BufferHandle, DeviceTag, ElementType, Shape, SyncMode, MIN_BUFFER_ALIGNMENT,
492    ///     descriptor::TensorDescriptor,
493    ///     layout::LayoutDescriptor,
494    /// };
495    ///
496    /// let shape  = Shape::new(vec![3u64, 4]).unwrap();
497    /// let buffer = BufferHandle::new(192, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
498    /// let desc   = TensorDescriptor::new(
499    ///     1, 0, ElementType::Float32, shape, 0,
500    ///     LayoutDescriptor::RowMajor, vec![buffer],
501    ///     None, None, None, None,
502    /// ).unwrap();
503    ///
504    /// let bytes = desc.encode().unwrap();
505    /// assert_eq!(bytes.len(), 61); // spec § Worked Example
506    /// ```
507    pub fn encode(&self) -> Result<Vec<u8>> {
508        encode::encode(self)
509    }
510
511    /// Decodes a [`TensorDescriptor`] from its wire representation.
512    ///
513    /// # Errors
514    ///
515    /// Returns a variant of [`Error`] for any malformed field:
516    /// - [`Error::InvalidMagic`] — magic bytes are not `"HRRY"`.
517    /// - [`Error::UnsupportedDescriptorVersion`] — `version_major > 1`.
518    /// - [`Error::DescriptorTooShort`] — `descriptor_length < 20`.
519    /// - [`Error::DescriptorTruncated`] — byte slice ends before a field is complete.
520    /// - [`Error::ReservedDescriptorFlagBitsSet`] — reserved flag bits are set.
521    /// - [`Error::EmptyBufferTable`] — `buffer_count == 0`.
522    /// - [`Error::DescriptorLengthMismatch`] — consumed bytes ≠ `descriptor_length`.
523    ///
524    /// # Examples
525    ///
526    /// ```
527    /// use hurray_core::{
528    ///     BufferHandle, DeviceTag, ElementType, Shape, SyncMode, MIN_BUFFER_ALIGNMENT,
529    ///     descriptor::TensorDescriptor,
530    ///     layout::LayoutDescriptor,
531    /// };
532    ///
533    /// let shape  = Shape::new(vec![3u64, 4]).unwrap();
534    /// let buffer = BufferHandle::new(192, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
535    /// let desc   = TensorDescriptor::new(
536    ///     1, 0, ElementType::Float32, shape, 0,
537    ///     LayoutDescriptor::RowMajor, vec![buffer],
538    ///     None, None, None, None,
539    /// ).unwrap();
540    ///
541    /// let bytes   = desc.encode().unwrap();
542    /// let decoded = TensorDescriptor::decode(&bytes).unwrap();
543    /// assert_eq!(decoded, desc);
544    /// ```
545    pub fn decode(bytes: &[u8]) -> Result<Self> {
546        decode::decode(bytes)
547    }
548}
549
550// ── Tests ─────────────────────────────────────────────────────────────────────
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use crate::layout::LayoutDescriptor;
556    use crate::{BufferHandle, DeviceTag, ElementType, Shape, SyncMode, MIN_BUFFER_ALIGNMENT};
557
558    // Helper: build the spec's worked example (float32, [3,4], row-major, 1 buffer).
559    fn worked_example() -> TensorDescriptor {
560        let shape = Shape::new(vec![3u64, 4]).unwrap();
561        let buffer = BufferHandle::new(
562            192,
563            MIN_BUFFER_ALIGNMENT,
564            DeviceTag::Cpu,
565            SyncMode::ProducerSynced,
566        )
567        .unwrap();
568        TensorDescriptor::new(
569            1,
570            0,
571            ElementType::Float32,
572            shape,
573            0,
574            LayoutDescriptor::RowMajor,
575            vec![buffer],
576            None,
577            None,
578            None,
579            None,
580        )
581        .unwrap()
582    }
583
584    /// Spec § Worked Example: the exact 61-byte encoding.
585    #[rustfmt::skip]
586    const WORKED_EXAMPLE_BYTES: [u8; 61] = [
587        // magic
588        0x48, 0x52, 0x52, 0x59,
589        // version 1.0
590        0x01, 0x00,
591        // descriptor_length = 61 (0x3D)
592        0x3D, 0x00, 0x00, 0x00,
593        // flags = 0
594        0x00, 0x00, 0x00, 0x00,
595        // type_tag = 0x03 (float32)
596        0x03,
597        // layout_tag = 0x01 (row-major)
598        0x01,
599        // rank = 2
600        0x02, 0x00, 0x00, 0x00,
601        // shape[0] = 3
602        0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
603        // shape[1] = 4
604        0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
605        // byte_offset = 0
606        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
607        // buffer_count = 1
608        0x01,
609        // buffer[0].byte_size = 192 (0xC0)
610        0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
611        // buffer[0].alignment = 64 (0x40)
612        0x40, 0x00, 0x00, 0x00,
613        // buffer[0].device_tag = 0x00 (CPU)
614        0x00,
615        // buffer[0].sync_mode = 0x00 (ProducerSynced, ADR-018)
616        0x00,
617        // buffer[0]._reserved[2]
618        0x00, 0x00,
619    ];
620
621    #[test]
622    fn worked_example_encode_exact_bytes() {
623        let desc = worked_example();
624        let encoded = desc.encode().unwrap();
625        assert_eq!(
626            encoded.len(),
627            61,
628            "expected 61 bytes per spec worked example, got {}",
629            encoded.len()
630        );
631        assert_eq!(
632            encoded.as_slice(),
633            &WORKED_EXAMPLE_BYTES,
634            "encoded bytes do not match spec worked example"
635        );
636    }
637
638    #[test]
639    fn worked_example_decode_round_trip() {
640        let desc = worked_example();
641        let encoded = desc.encode().unwrap();
642        let decoded = TensorDescriptor::decode(&encoded).unwrap();
643        assert_eq!(decoded, desc);
644    }
645
646    #[test]
647    fn worked_example_decode_from_spec_bytes() {
648        let decoded = TensorDescriptor::decode(&WORKED_EXAMPLE_BYTES).unwrap();
649        assert_eq!(decoded, worked_example());
650    }
651
652    // ── Negative tests ────────────────────────────────────────────────────────
653
654    #[test]
655    fn decode_bad_magic() {
656        let mut bytes = WORKED_EXAMPLE_BYTES;
657        bytes[0] = 0xFF;
658        let err = TensorDescriptor::decode(&bytes).unwrap_err();
659        assert!(matches!(err, Error::InvalidMagic { .. }));
660    }
661
662    #[test]
663    fn decode_version_major_too_high() {
664        let mut bytes = WORKED_EXAMPLE_BYTES;
665        bytes[4] = 2; // version_major = 2 > DESCRIPTOR_VERSION_MAJOR
666        let err = TensorDescriptor::decode(&bytes).unwrap_err();
667        assert!(matches!(
668            err,
669            Error::UnsupportedDescriptorVersion { major: 2, .. }
670        ));
671    }
672
673    #[test]
674    fn decode_descriptor_too_short() {
675        let mut bytes = WORKED_EXAMPLE_BYTES;
676        // descriptor_length = 10 < MIN_DESCRIPTOR_LEN (20)
677        bytes[6] = 10;
678        bytes[7] = 0;
679        bytes[8] = 0;
680        bytes[9] = 0;
681        let err = TensorDescriptor::decode(&bytes).unwrap_err();
682        assert!(matches!(err, Error::DescriptorTooShort { length: 10 }));
683    }
684
685    #[test]
686    fn decode_reserved_flag_bit_set() {
687        let mut bytes = WORKED_EXAMPLE_BYTES;
688        // Bit 4 is now HAS_COMPOSITE_MEMBER (ADR-027) and no longer reserved; set
689        // bit 5 (the first genuinely-still-reserved flag bit) at offset 10.
690        bytes[10] = 0x20;
691        let err = TensorDescriptor::decode(&bytes).unwrap_err();
692        assert!(matches!(err, Error::ReservedDescriptorFlagBitsSet { .. }));
693    }
694
695    #[test]
696    fn decode_empty_buffer_table() {
697        // Build a valid descriptor, then patch buffer_count to 0.
698        let desc = worked_example();
699        let mut bytes = desc.encode().unwrap();
700        // buffer_count is at offset 44 in the worked example.
701        bytes[44] = 0;
702        let err = TensorDescriptor::decode(&bytes).unwrap_err();
703        assert!(matches!(err, Error::EmptyBufferTable));
704    }
705
706    #[test]
707    fn decode_truncated() {
708        // Fewer than 10 bytes — cannot even read the header.
709        let bytes = &[0x48u8, 0x52, 0x52];
710        let err = TensorDescriptor::decode(bytes).unwrap_err();
711        assert!(matches!(err, Error::DescriptorTruncated { .. }));
712    }
713
714    // ── flags() helper ────────────────────────────────────────────────────────
715
716    #[test]
717    fn flags_none_set() {
718        assert_eq!(worked_example().flags().0, 0);
719    }
720
721    #[test]
722    fn flags_quantization_set() {
723        let shape = Shape::new(vec![4u64]).unwrap();
724        let buf = BufferHandle::new(
725            64,
726            MIN_BUFFER_ALIGNMENT,
727            DeviceTag::Cpu,
728            SyncMode::ProducerSynced,
729        )
730        .unwrap();
731        let desc = TensorDescriptor::new(
732            1,
733            0,
734            ElementType::Int8,
735            shape,
736            0,
737            LayoutDescriptor::RowMajor,
738            vec![buf],
739            Some(vec![0x01, 0x00, 0x00, 0x00]),
740            None,
741            None,
742            None,
743        )
744        .unwrap();
745        assert!(desc.flags().has_quantization());
746        assert!(!desc.flags().has_shard());
747    }
748
749    // ── constructor validation ────────────────────────────────────────────────
750
751    #[test]
752    fn new_rejects_empty_buffers() {
753        let shape = Shape::new(vec![4u64]).unwrap();
754        let err = TensorDescriptor::new(
755            1,
756            0,
757            ElementType::Float32,
758            shape,
759            0,
760            LayoutDescriptor::RowMajor,
761            vec![],
762            None,
763            None,
764            None,
765            None,
766        )
767        .unwrap_err();
768        assert!(matches!(err, Error::EmptyBufferTable));
769    }
770
771    #[test]
772    fn new_rejects_shard_rank_mismatch() {
773        let shape = Shape::new(vec![4u64, 4]).unwrap(); // rank 2
774        let buf = BufferHandle::new(
775            64,
776            MIN_BUFFER_ALIGNMENT,
777            DeviceTag::Cpu,
778            SyncMode::ProducerSynced,
779        )
780        .unwrap();
781        // shard with rank 1 (mismatch)
782        let shard = ShardDescriptor::new(vec![10u64], vec![0u64]).unwrap();
783        let err = TensorDescriptor::new(
784            1,
785            0,
786            ElementType::Float32,
787            shape,
788            0,
789            LayoutDescriptor::RowMajor,
790            vec![buf],
791            None,
792            Some(shard),
793            None,
794            None,
795        )
796        .unwrap_err();
797        assert!(matches!(err, Error::InvalidShape(_)));
798    }
799
800    #[test]
801    fn new_rejects_block_paged_with_shard() {
802        use crate::layout::{BlockPagedLayout, BlockTableIndexType, KvRole};
803
804        // Block-paged is rank-3; give the shard a matching rank-3 parent_shape so the
805        // rank check passes and we reach the block-paged § Sharding prohibition.
806        let shape = Shape::new(vec![6u64, 2, 8]).unwrap();
807        let buf = BufferHandle::new(
808            64,
809            MIN_BUFFER_ALIGNMENT,
810            DeviceTag::Cpu,
811            SyncMode::ProducerSynced,
812        )
813        .unwrap();
814        let shard = ShardDescriptor::new(vec![12u64, 2, 8], vec![0u64, 0, 0]).unwrap();
815        let layout = LayoutDescriptor::BlockPaged(BlockPagedLayout::new(
816            4,
817            5,
818            0,
819            2,
820            KvRole::Key,
821            Some(3),
822            BlockTableIndexType::U32,
823        ));
824        let err = TensorDescriptor::new(
825            1,
826            0,
827            ElementType::Float16,
828            shape,
829            0,
830            layout,
831            vec![buf],
832            None,
833            Some(shard),
834            None,
835            None,
836        )
837        .unwrap_err();
838        assert!(matches!(err, Error::InvalidLayout(_)));
839    }
840
841    #[test]
842    fn new_rejects_block_paged_with_incompatible_quantization() {
843        use crate::layout::{BlockPagedLayout, BlockTableIndexType, KvRole};
844        use crate::quantization::PerChannelAffine;
845
846        // Per-channel quantization on the paged axis (axis 0) is prohibited for a
847        // block-paged layout (block-paged.md § Quantization Compatibility). This is the
848        // cross-section check wired into TensorDescriptor::new.
849        let quant = QuantizationDescriptor::PerChannelAffine(
850            PerChannelAffine::new_symmetric(0, 1).unwrap(),
851        )
852        .encode_to_vec();
853        let shape = Shape::new(vec![6u64, 2, 8]).unwrap();
854        let buf = BufferHandle::new(
855            64,
856            MIN_BUFFER_ALIGNMENT,
857            DeviceTag::Cpu,
858            SyncMode::ProducerSynced,
859        )
860        .unwrap();
861        let layout = LayoutDescriptor::BlockPaged(BlockPagedLayout::new(
862            4,
863            5,
864            0,
865            2,
866            KvRole::Key,
867            Some(3),
868            BlockTableIndexType::U32,
869        ));
870        let err = TensorDescriptor::new(
871            1,
872            0,
873            ElementType::Float16,
874            shape,
875            0,
876            layout,
877            vec![buf],
878            Some(quant),
879            None,
880            None,
881            None,
882        )
883        .unwrap_err();
884        assert!(matches!(err, Error::InvalidQuantization(_)));
885    }
886
887    #[test]
888    fn new_accepts_block_paged_with_compatible_quantization() {
889        use crate::layout::{BlockPagedLayout, BlockTableIndexType, KvRole};
890        use crate::quantization::PerChannelAffine;
891
892        // Per-channel on axis 1 (num_heads) is permitted for block-paged.
893        let quant = QuantizationDescriptor::PerChannelAffine(
894            PerChannelAffine::new_symmetric(1, 1).unwrap(),
895        )
896        .encode_to_vec();
897        let shape = Shape::new(vec![6u64, 2, 8]).unwrap();
898        let buf = BufferHandle::new(
899            64,
900            MIN_BUFFER_ALIGNMENT,
901            DeviceTag::Cpu,
902            SyncMode::ProducerSynced,
903        )
904        .unwrap();
905        let layout = LayoutDescriptor::BlockPaged(BlockPagedLayout::new(
906            4,
907            5,
908            0,
909            2,
910            KvRole::Key,
911            Some(3),
912            BlockTableIndexType::U32,
913        ));
914        let desc = TensorDescriptor::new(
915            1,
916            0,
917            ElementType::Float16,
918            shape,
919            0,
920            layout,
921            vec![buf],
922            Some(quant),
923            None,
924            None,
925            None,
926        );
927        assert!(desc.is_ok());
928    }
929
930    #[test]
931    fn new_rejects_csf_with_shard() {
932        use crate::layout::CsfLayout;
933
934        // CSF is rank-3+; give the shard a matching rank-3 parent_shape so the rank
935        // check passes and we reach the CSF § Sharding prohibition.
936        let shape = Shape::new(vec![2u64, 3, 4]).unwrap();
937        let buf = BufferHandle::new(
938            64,
939            MIN_BUFFER_ALIGNMENT,
940            DeviceTag::Cpu,
941            SyncMode::ProducerSynced,
942        )
943        .unwrap();
944        let shard = ShardDescriptor::new(vec![4u64, 3, 4], vec![0u64, 0, 0]).unwrap();
945        let layout = LayoutDescriptor::Csf(CsfLayout::new(4, vec![0, 1, 2]));
946        let err = TensorDescriptor::new(
947            1,
948            0,
949            ElementType::Float32,
950            shape,
951            0,
952            layout,
953            vec![buf],
954            None,
955            Some(shard),
956            None,
957            None,
958        )
959        .unwrap_err();
960        assert!(matches!(err, Error::InvalidLayout(_)));
961    }
962
963    // ── round-trip with optional sections ────────────────────────────────────
964
965    #[test]
966    fn round_trip_with_shard() {
967        use crate::layout::LayoutDescriptor;
968        let shape = Shape::new(vec![4u64, 8]).unwrap();
969        let buf = BufferHandle::new(
970            128,
971            MIN_BUFFER_ALIGNMENT,
972            DeviceTag::Cpu,
973            SyncMode::ProducerSynced,
974        )
975        .unwrap();
976        let shard = ShardDescriptor::new(vec![10u64, 20], vec![0u64, 4]).unwrap();
977        let desc = TensorDescriptor::new(
978            1,
979            0,
980            ElementType::Float32,
981            shape,
982            0,
983            LayoutDescriptor::RowMajor,
984            vec![buf],
985            None,
986            Some(shard),
987            None,
988            None,
989        )
990        .unwrap();
991        let decoded = TensorDescriptor::decode(&desc.encode().unwrap()).unwrap();
992        assert_eq!(decoded, desc);
993    }
994
995    #[test]
996    fn round_trip_with_quantization_payload() {
997        let shape = Shape::new(vec![8u64]).unwrap();
998        let buf = BufferHandle::new(
999            64,
1000            MIN_BUFFER_ALIGNMENT,
1001            DeviceTag::Cpu,
1002            SyncMode::ProducerSynced,
1003        )
1004        .unwrap();
1005        let quant_bytes = vec![0x01u8, 0x00, 0x00, 0x00, 0xAB, 0xCD];
1006        let desc = TensorDescriptor::new(
1007            1,
1008            0,
1009            ElementType::Int8,
1010            shape,
1011            0,
1012            LayoutDescriptor::RowMajor,
1013            vec![buf],
1014            Some(quant_bytes),
1015            None,
1016            None,
1017            None,
1018        )
1019        .unwrap();
1020        let decoded = TensorDescriptor::decode(&desc.encode().unwrap()).unwrap();
1021        assert_eq!(decoded, desc);
1022    }
1023
1024    #[test]
1025    fn round_trip_with_statistics() {
1026        use crate::descriptor::statistics::{Statistics, StatisticsMask};
1027        let shape = Shape::new(vec![16u64]).unwrap();
1028        let buf = BufferHandle::new(
1029            128,
1030            MIN_BUFFER_ALIGNMENT,
1031            DeviceTag::Cpu,
1032            SyncMode::ProducerSynced,
1033        )
1034        .unwrap();
1035        let stats = Statistics {
1036            computed_mask: StatisticsMask(StatisticsMask::NNZ_VALID),
1037            nnz: 10,
1038            sparsity_ratio: 0.0,
1039            value_min: 0.0,
1040            value_max: 0.0,
1041            value_abs_max: 0.0,
1042            value_mean: 0.0,
1043            value_stddev: 0.0,
1044            nm_n: 0,
1045            nm_m: 0,
1046            has_nan: false,
1047            has_inf: false,
1048        };
1049        let desc = TensorDescriptor::new(
1050            1,
1051            0,
1052            ElementType::Float32,
1053            shape,
1054            0,
1055            LayoutDescriptor::RowMajor,
1056            vec![buf],
1057            None,
1058            None,
1059            Some(stats),
1060            None,
1061        )
1062        .unwrap();
1063        let encoded = desc.encode().unwrap();
1064        let decoded = TensorDescriptor::decode(&encoded).unwrap();
1065        assert_eq!(decoded.statistics.as_ref().unwrap().nnz, 10);
1066        assert!(decoded.flags().has_statistics());
1067    }
1068
1069    #[test]
1070    fn round_trip_strided_layout() {
1071        use crate::layout::StridedLayout;
1072        let shape = Shape::new(vec![3u64, 4]).unwrap();
1073        let buf = BufferHandle::new(
1074            192,
1075            MIN_BUFFER_ALIGNMENT,
1076            DeviceTag::Cpu,
1077            SyncMode::ProducerSynced,
1078        )
1079        .unwrap();
1080        let layout = LayoutDescriptor::Strided(StridedLayout::new(vec![8i64, 1]));
1081        let desc = TensorDescriptor::new(
1082            1,
1083            0,
1084            ElementType::Float32,
1085            shape,
1086            0,
1087            layout,
1088            vec![buf],
1089            None,
1090            None,
1091            None,
1092            None,
1093        )
1094        .unwrap();
1095        let decoded = TensorDescriptor::decode(&desc.encode().unwrap()).unwrap();
1096        assert_eq!(decoded, desc);
1097    }
1098
1099    #[test]
1100    fn round_trip_coo_layout() {
1101        use crate::layout::CooLayout;
1102        let shape = Shape::new(vec![10u64, 10]).unwrap();
1103        // COO requires 2 buffers (values + indices)
1104        let buf0 = BufferHandle::new(
1105            64,
1106            MIN_BUFFER_ALIGNMENT,
1107            DeviceTag::Cpu,
1108            SyncMode::ProducerSynced,
1109        )
1110        .unwrap();
1111        let buf1 = BufferHandle::new(
1112            128,
1113            MIN_BUFFER_ALIGNMENT,
1114            DeviceTag::Cpu,
1115            SyncMode::ProducerSynced,
1116        )
1117        .unwrap();
1118        let layout = LayoutDescriptor::Coo(CooLayout::new(5, true));
1119        let desc = TensorDescriptor::new(
1120            1,
1121            0,
1122            ElementType::Float32,
1123            shape,
1124            0,
1125            layout,
1126            vec![buf0, buf1],
1127            None,
1128            None,
1129            None,
1130            None,
1131        )
1132        .unwrap();
1133        let decoded = TensorDescriptor::decode(&desc.encode().unwrap()).unwrap();
1134        assert_eq!(decoded, desc);
1135    }
1136
1137    // ── C-1: Extension type round-trip ────────────────────────────────────────
1138
1139    /// A descriptor with ElementType::Extension(0xF1) and a populated
1140    /// ExtensionTypeDescriptor must round-trip through encode/decode unchanged.
1141    #[test]
1142    fn round_trip_extension_element_type() {
1143        use crate::descriptor::ExtensionTypeDescriptor;
1144        let shape = Shape::new(vec![4u64]).unwrap();
1145        let buf = BufferHandle::new(
1146            64,
1147            MIN_BUFFER_ALIGNMENT,
1148            DeviceTag::Cpu,
1149            SyncMode::ProducerSynced,
1150        )
1151        .unwrap();
1152        // 8-bit integer extension type, packing_factor=1.
1153        let ext =
1154            ExtensionTypeDescriptor::new(8, 1, false, true, 0, 0, 0, 0, false, false).unwrap();
1155        let desc = TensorDescriptor::new(
1156            1,
1157            0,
1158            ElementType::Extension(0xF1),
1159            shape,
1160            0,
1161            LayoutDescriptor::RowMajor,
1162            vec![buf],
1163            None,
1164            None,
1165            None,
1166            Some(ext.clone()),
1167        )
1168        .unwrap();
1169        let encoded = desc.encode().unwrap();
1170        let decoded = TensorDescriptor::decode(&encoded).unwrap();
1171        assert_eq!(decoded.element_type, ElementType::Extension(0xF1));
1172        assert_eq!(decoded.extension_type.as_ref().unwrap().bit_width, 8);
1173        assert_eq!(decoded, desc);
1174    }
1175
1176    // ── H-1: Buffer count ceiling ─────────────────────────────────────────────
1177
1178    /// Constructing a TensorDescriptor with 256 buffers must be rejected at
1179    /// new() with InvalidLayout, not silently truncated.
1180    #[test]
1181    fn new_rejects_buffer_count_exceeding_255() {
1182        let shape = Shape::new(vec![4u64]).unwrap();
1183        let buffers: Vec<BufferHandle> = (0..256)
1184            .map(|_| {
1185                BufferHandle::new(
1186                    64,
1187                    MIN_BUFFER_ALIGNMENT,
1188                    DeviceTag::Cpu,
1189                    SyncMode::ProducerSynced,
1190                )
1191                .unwrap()
1192            })
1193            .collect();
1194        let err = TensorDescriptor::new(
1195            1,
1196            0,
1197            ElementType::Float32,
1198            shape,
1199            0,
1200            LayoutDescriptor::RowMajor,
1201            buffers,
1202            None,
1203            None,
1204            None,
1205            None,
1206        )
1207        .unwrap_err();
1208        assert!(
1209            matches!(err, Error::InvalidLayout(_)),
1210            "expected InvalidLayout, got {err:?}"
1211        );
1212    }
1213
1214    // ── M-2: descriptor_length mismatch ───────────────────────────────────────
1215
1216    /// Patching descriptor_length to declared+1 (without adding a real byte)
1217    /// must produce DescriptorLengthMismatch on decode.
1218    #[test]
1219    fn decode_descriptor_length_mismatch() {
1220        let desc = worked_example();
1221        let mut bytes = desc.encode().unwrap();
1222        // descriptor_length is a little-endian u32 at bytes[6..10].
1223        let declared = u32::from_le_bytes([bytes[6], bytes[7], bytes[8], bytes[9]]);
1224        let patched = declared + 1;
1225        bytes[6..10].copy_from_slice(&patched.to_le_bytes());
1226        let err = TensorDescriptor::decode(&bytes).unwrap_err();
1227        assert!(
1228            matches!(err, Error::DescriptorLengthMismatch { .. }),
1229            "expected DescriptorLengthMismatch, got {err:?}"
1230        );
1231    }
1232
1233    // ── M-3: tiled nesting too deep ───────────────────────────────────────────
1234
1235    /// A tiled descriptor nested 9 levels deep (exceeding MAX_RECURSION_DEPTH=8)
1236    /// must be rejected with SubpavingNestingTooDeep on decode.
1237    ///
1238    /// The layout bytes are hand-crafted to bypass TiledLayout::new's own depth
1239    /// guard (which fires at construction), producing wire bytes that the decoder
1240    /// must reject at the depth limit.
1241    #[test]
1242    fn decode_tiled_nesting_too_deep() {
1243        // Hand-craft 9-level nested tiled layout bytes for rank=1.
1244        // Each tiled level for rank=1 with inner_layout=TAG_TILED (0x04):
1245        //   tile_shape u64[1] = 8 bytes
1246        //   outer_layout u8   = 0x01 (row-major)
1247        //   inner_layout u8   = 0x04 (tiled, recurse) OR 0x01 (innermost)
1248        //   _reserved u16     = 0x0000
1249        // Total per level: 12 bytes.
1250        // 9 levels (0..=8): 9 × 12 = 108 bytes of layout payload.
1251        let mut layout_bytes: Vec<u8> = Vec::new();
1252        for level in 0..9u32 {
1253            // tile_shape[0] = 2 (LE u64)
1254            layout_bytes.extend_from_slice(&2u64.to_le_bytes());
1255            // outer_layout = 0x01 (row-major)
1256            layout_bytes.push(0x01);
1257            // inner_layout: 0x04 (nested tiled) for levels 0..8, 0x01 (row-major) for level 8
1258            if level < 8 {
1259                layout_bytes.push(0x04); // TAG_TILED — recurse
1260            } else {
1261                layout_bytes.push(0x01); // innermost — row-major
1262            }
1263            // _reserved[2]
1264            layout_bytes.extend_from_slice(&[0x00, 0x00]);
1265        }
1266
1267        // Build a minimal valid descriptor for a rank-1 float32 tensor with a
1268        // shallow (1-level) tiled layout, then splice in the 9-level bytes.
1269        use crate::layout::TiledLayout;
1270        let shape = Shape::new(vec![2u64]).unwrap();
1271        let buf = BufferHandle::new(
1272            64,
1273            MIN_BUFFER_ALIGNMENT,
1274            DeviceTag::Cpu,
1275            SyncMode::ProducerSynced,
1276        )
1277        .unwrap();
1278        let shallow = LayoutDescriptor::Tiled(Box::new(
1279            TiledLayout::new(vec![2u64], 0x01, 0x01, None, None, None).unwrap(),
1280        ));
1281        let desc = TensorDescriptor::new(
1282            1,
1283            0,
1284            ElementType::Float32,
1285            shape,
1286            0,
1287            shallow,
1288            vec![buf],
1289            None,
1290            None,
1291            None,
1292            None,
1293        )
1294        .unwrap();
1295        let mut encoded = desc.encode().unwrap();
1296
1297        // Splice: layout payload starts at offset 36
1298        // (fixed header 20 + shape u64[1] 8 + byte_offset u64 8).
1299        // The shallow 1-level tiled layout for rank=1: 12 bytes.
1300        let layout_start = 36usize;
1301        let shallow_layout_len = 12usize;
1302        let after_layout = layout_start + shallow_layout_len;
1303
1304        let rest = encoded[after_layout..].to_vec();
1305        encoded.truncate(layout_start);
1306        encoded.extend_from_slice(&layout_bytes);
1307        encoded.extend_from_slice(&rest);
1308
1309        // Back-patch descriptor_length (bytes 6..10).
1310        let new_len = encoded.len() as u32;
1311        encoded[6..10].copy_from_slice(&new_len.to_le_bytes());
1312
1313        // Decoding must fail at the depth guard (MAX_RECURSION_DEPTH = 8).
1314        let err = TensorDescriptor::decode(&encoded).unwrap_err();
1315        assert!(
1316            matches!(err, Error::SubpavingNestingTooDeep),
1317            "expected SubpavingNestingTooDeep, got {err:?}"
1318        );
1319    }
1320
1321    // ── ADR-027 D-1: composite head buffer-table carve-out ───────────────────
1322
1323    fn composite_head(member_count: u32) -> TensorDescriptor {
1324        use crate::layout::{CompositeLayout, CompositionRule};
1325        let layout = LayoutDescriptor::Composite(
1326            CompositeLayout::new(CompositionRule::Group, member_count).unwrap(),
1327        );
1328        TensorDescriptor::new(
1329            1,
1330            0,
1331            ElementType::Float32,
1332            Shape::new(vec![4u64]).unwrap(),
1333            0,
1334            layout,
1335            vec![],
1336            None,
1337            None,
1338            None,
1339            None,
1340        )
1341        .unwrap()
1342    }
1343
1344    /// A composite head (`layout_tag = 0x0B`) with an empty buffer table is the
1345    /// *only* valid buffer table for that layout, and round-trips cleanly.
1346    #[test]
1347    fn composite_head_empty_buffers_round_trips() {
1348        let desc = composite_head(0);
1349        let decoded = TensorDescriptor::decode(&desc.encode().unwrap()).unwrap();
1350        assert_eq!(decoded, desc);
1351        assert!(decoded.buffers.is_empty());
1352    }
1353
1354    /// A non-empty buffer table on a composite head is rejected.
1355    #[test]
1356    fn composite_head_nonzero_buffer_count_rejected() {
1357        use crate::layout::{CompositeLayout, CompositionRule};
1358        let buf = BufferHandle::new(
1359            64,
1360            MIN_BUFFER_ALIGNMENT,
1361            DeviceTag::Cpu,
1362            SyncMode::ProducerSynced,
1363        )
1364        .unwrap();
1365        let layout =
1366            LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Group, 0).unwrap());
1367        let err = TensorDescriptor::new(
1368            1,
1369            0,
1370            ElementType::Float32,
1371            Shape::new(vec![4u64]).unwrap(),
1372            0,
1373            layout,
1374            vec![buf],
1375            None,
1376            None,
1377            None,
1378            None,
1379        )
1380        .unwrap_err();
1381        assert!(matches!(err, Error::CompositeHeadHasBuffers { count: 1 }));
1382    }
1383
1384    /// A non-zero `byte_offset` on a composite head is rejected.
1385    #[test]
1386    fn composite_head_nonzero_byte_offset_rejected() {
1387        use crate::layout::{CompositeLayout, CompositionRule};
1388        let layout =
1389            LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Group, 0).unwrap());
1390        let err = TensorDescriptor::new(
1391            1,
1392            0,
1393            ElementType::Float32,
1394            Shape::new(vec![4u64]).unwrap(),
1395            8, // non-zero byte_offset
1396            layout,
1397            vec![],
1398            None,
1399            None,
1400            None,
1401            None,
1402        )
1403        .unwrap_err();
1404        assert!(matches!(
1405            err,
1406            Error::CompositeHeadHasByteOffset { byte_offset: 8 }
1407        ));
1408    }
1409
1410    /// A composite head MUST NOT set `HAS_QUANTIZATION` (it owns no stored data).
1411    #[test]
1412    fn composite_head_with_quantization_rejected() {
1413        use crate::layout::{CompositeLayout, CompositionRule};
1414        let layout =
1415            LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Group, 0).unwrap());
1416        let err = TensorDescriptor::new(
1417            1,
1418            0,
1419            ElementType::Float32,
1420            Shape::new(vec![4u64]).unwrap(),
1421            0,
1422            layout,
1423            vec![],
1424            Some(vec![0x01, 0x00, 0x00, 0x00]),
1425            None,
1426            None,
1427            None,
1428        )
1429        .unwrap_err();
1430        assert!(matches!(err, Error::CompositeHeadHasQuantization));
1431    }
1432
1433    /// A composite head carrying a Composite Member section itself (not a
1434    /// member of a further-enclosing composite) is rejected at encode time.
1435    #[test]
1436    fn composite_head_with_composite_member_rejected_at_encode() {
1437        use crate::descriptor::composite_member::CompositeMemberDescriptor;
1438        let desc = composite_head(0).with_composite_member(CompositeMemberDescriptor::new(
1439            crate::descriptor::MemberRole::Base,
1440        ));
1441        let err = desc.encode().unwrap_err();
1442        assert!(matches!(err, Error::InvalidLayout(_)));
1443    }
1444
1445    /// The same invariant, enforced symmetrically on the decode path: a
1446    /// hand-crafted composite head with `HAS_COMPOSITE_MEMBER` set and a valid
1447    /// 16-byte Composite Member section appended is rejected.
1448    #[test]
1449    fn composite_head_with_composite_member_rejected_at_decode() {
1450        let desc = composite_head(0);
1451        let mut encoded = desc.encode().unwrap();
1452
1453        // Set HAS_COMPOSITE_MEMBER (bit 4 = 0x10) in the flags field at offset 10.
1454        encoded[10] |= DescriptorFlags::HAS_COMPOSITE_MEMBER as u8;
1455
1456        // Append a valid 16-byte Composite Member section (member_role=0x00, 15
1457        // zero reserved bytes) — this is the last section per spec wire order.
1458        let mut cm_bytes = vec![0x00u8]; // member_role = Correction
1459        cm_bytes.extend(std::iter::repeat_n(0u8, 15)); // _reserved
1460        encoded.extend_from_slice(&cm_bytes);
1461
1462        // Back-patch descriptor_length.
1463        let new_len = encoded.len() as u32;
1464        encoded[6..10].copy_from_slice(&new_len.to_le_bytes());
1465
1466        let err = TensorDescriptor::decode(&encoded).unwrap_err();
1467        assert!(matches!(err, Error::InvalidLayout(_)));
1468    }
1469
1470    /// A non-composite descriptor with `with_composite_member` set round-trips
1471    /// through full encode/decode.
1472    #[test]
1473    fn with_composite_member_round_trips() {
1474        use crate::descriptor::composite_member::CompositeMemberDescriptor;
1475        let desc = worked_example().with_composite_member(CompositeMemberDescriptor::new(
1476            crate::descriptor::MemberRole::Base,
1477        ));
1478        assert!(desc.flags().has_composite_member());
1479        let decoded = TensorDescriptor::decode(&desc.encode().unwrap()).unwrap();
1480        assert_eq!(decoded, desc);
1481        assert_eq!(
1482            decoded.composite_member.unwrap().member_role,
1483            crate::descriptor::MemberRole::Base
1484        );
1485    }
1486}