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}