Skip to main content

hurray_core/descriptor/
layout_codec.rs

1//! Per-layout-tag encode/decode dispatch for the layout-specific fields section.
2//!
3//! Wire formats per layout tag (spec § Layout-Specific Fields):
4//!
5//! - `0x01` (RowMajor), `0x02` (ColMajor): no payload
6//! - `0x03` (Strided): `strides int64[rank]`
7//! - `0x04` (Tiled): `tile_shape u64[rank]`, `outer_layout u8`, `inner_layout u8`,
8//!   `_reserved u8[2]`, then conditionally outer_strides / inner_strides / inner_tiled
9//! - `0x05` (Morton): `morton_bits u32[rank]`
10//! - `0x06` (COO): `nnz u64`, `is_sorted u8`, `_reserved u8[7]`
11//! - `0x07` (CSR): `nnz u64`, `_reserved u8[8]`
12//! - `0x08` (CSC): `nnz u64`, `_reserved u8[8]`
13//! - `0x09` (CSF): `nnz u64`, `mode_order u32[rank]`, `_reserved u8[8]`
14//! - `0x0A` (BlockPaged): see § BlockPaged field table below
15//! - `0x0B` (Composite): `composition_rule u8`, `combine_op u8`, `_reserved u8[2]`,
16//!   `member_count u32`
17//! - `0x40` (Hilbert): `hilbert_order u32`, `hilbert_rank u32`
18//! - `0xF0`–`0xFE` (PrivateExtension): `extension_layout_id u64`, `extension_data_length u32`,
19//!   `extension_data bytes[len]`
20
21use crate::descriptor::cursor::{ByteCursor, ByteWriter};
22use crate::layout::{
23    BlockPagedLayout, BlockTableIndexType, CompositeLayout, CompositionRule, CooLayout, CscLayout,
24    CsfLayout, CsrLayout, HilbertLayout, InnerStrides, KvRole, LayoutDescriptor, MortonLayout,
25    OuterStrides, PrivateExtensionLayout, StridedLayout, TiledLayout, MAX_TILED_DEPTH,
26    TAG_BLOCK_PAGED, TAG_COL_MAJOR, TAG_COMPOSITE, TAG_COO, TAG_CSC, TAG_CSF, TAG_CSR, TAG_HILBERT,
27    TAG_MORTON, TAG_ROW_MAJOR, TAG_STRIDED, TAG_TILED,
28};
29use crate::{Error, Result};
30
31// ── Public entry points ───────────────────────────────────────────────────────
32
33/// Encodes the layout-specific payload for `layout` into `w`.
34///
35/// The layout tag byte itself is NOT written here — it is part of the fixed
36/// header written by the caller (`encode.rs`).
37pub(crate) fn encode_layout_payload(
38    layout: &LayoutDescriptor,
39    rank: u32,
40    w: &mut ByteWriter,
41) -> Result<()> {
42    // Top-level callers always start at depth 0; recursion uses the internal
43    // depth-aware variant so the MAX_TILED_DEPTH guard fires on the encode path.
44    encode_layout_payload_at_depth(layout, rank, w, 0)
45}
46
47/// Internal depth-aware dispatcher.  `depth` mirrors the same counter used in
48/// `decode_layout_payload` so the recursion guard applies symmetrically to
49/// both encode and decode.
50fn encode_layout_payload_at_depth(
51    layout: &LayoutDescriptor,
52    rank: u32,
53    w: &mut ByteWriter,
54    depth: u8,
55) -> Result<()> {
56    match layout {
57        LayoutDescriptor::RowMajor | LayoutDescriptor::ColMajor => {
58            // No additional payload.
59            Ok(())
60        }
61        LayoutDescriptor::Strided(s) => encode_strided(s, w),
62        LayoutDescriptor::Tiled(t) => encode_tiled(t, rank, w, depth),
63        LayoutDescriptor::Morton(m) => encode_morton(m, w),
64        LayoutDescriptor::Coo(c) => encode_coo(c, w),
65        LayoutDescriptor::Csr(c) => encode_csr(c, w),
66        LayoutDescriptor::Csc(c) => encode_csc(c, w),
67        LayoutDescriptor::Csf(c) => encode_csf(c, rank, w),
68        LayoutDescriptor::BlockPaged(bp) => encode_block_paged(bp, w),
69        LayoutDescriptor::Composite(c) => encode_composite(c, w),
70        LayoutDescriptor::Hilbert(h) => encode_hilbert(h, w),
71        LayoutDescriptor::PrivateExtension(p) => encode_private_extension(p, w),
72        LayoutDescriptor::Unknown(_) => {
73            // Unknown layouts cannot be re-encoded in strict mode.
74            Err(Error::UnknownLayoutTag(layout.tag()))
75        }
76    }
77}
78
79/// Decodes the layout-specific payload for `tag` from `cursor`, returning a
80/// fully constructed [`LayoutDescriptor`].
81///
82/// `depth` tracks recursion level; callers at the top level pass `0`.
83pub(crate) fn decode_layout_payload(
84    tag: u8,
85    rank: u32,
86    cursor: &mut ByteCursor<'_>,
87    depth: u8,
88) -> Result<LayoutDescriptor> {
89    if depth >= MAX_TILED_DEPTH as u8 {
90        return Err(Error::SubpavingNestingTooDeep);
91    }
92    match tag {
93        TAG_ROW_MAJOR => Ok(LayoutDescriptor::RowMajor),
94        TAG_COL_MAJOR => Ok(LayoutDescriptor::ColMajor),
95        TAG_STRIDED => decode_strided(cursor, rank),
96        TAG_TILED => Ok(LayoutDescriptor::Tiled(Box::new(decode_tiled(
97            cursor, rank, depth,
98        )?))),
99        TAG_MORTON => decode_morton(cursor, rank),
100        TAG_COO => decode_coo(cursor),
101        TAG_CSR => decode_csr(cursor),
102        TAG_CSC => decode_csc(cursor),
103        TAG_CSF => decode_csf(cursor, rank),
104        TAG_BLOCK_PAGED => decode_block_paged(cursor),
105        TAG_COMPOSITE => decode_composite(cursor),
106        TAG_HILBERT => decode_hilbert(cursor),
107        0xF0..=0xFE => decode_private_extension(tag, cursor),
108        0x00 | 0xFF => Err(Error::InvalidLayoutTag(tag)),
109        t if crate::layout::is_reserved_tag(t) => Err(Error::ReservedLayoutTag(tag)),
110        _ => Err(Error::UnknownLayoutTag(tag)),
111    }
112}
113
114// ── Strided ───────────────────────────────────────────────────────────────────
115
116fn encode_strided(layout: &StridedLayout, w: &mut ByteWriter) -> Result<()> {
117    for &stride in &layout.strides {
118        w.write_i64_le(stride);
119    }
120    Ok(())
121}
122
123fn decode_strided(cursor: &mut ByteCursor<'_>, rank: u32) -> Result<LayoutDescriptor> {
124    let mut strides = Vec::with_capacity(rank as usize);
125    for _ in 0..rank {
126        strides.push(cursor.read_i64_le()?);
127    }
128    Ok(LayoutDescriptor::Strided(StridedLayout::new(strides)))
129}
130
131// ── Tiled ─────────────────────────────────────────────────────────────────────
132
133// `rank` is passed through to recursive calls of encode_tiled; not used directly in this level.
134#[allow(clippy::only_used_in_recursion)]
135fn encode_tiled(layout: &TiledLayout, rank: u32, w: &mut ByteWriter, depth: u8) -> Result<()> {
136    if depth >= MAX_TILED_DEPTH as u8 {
137        return Err(Error::SubpavingNestingTooDeep);
138    }
139    // tile_shape: uint64[rank]
140    for &dim in &layout.tile_shape {
141        w.write_u64_le(dim);
142    }
143    w.write_u8(layout.outer_layout);
144    w.write_u8(layout.inner_layout);
145    w.write_zeros(2); // _reserved
146
147    // Conditional: outer_strides if outer_layout == 0x03.
148    // The stride-length invariant (strides.len() == rank) is maintained by TiledLayout::new,
149    // so no padding is needed here.
150    if layout.outer_layout == TAG_STRIDED {
151        if let Some(os) = &layout.outer_strides {
152            for &s in &os.strides {
153                w.write_i64_le(s);
154            }
155        }
156    }
157
158    // Conditional: inner_strides if inner_layout == 0x03
159    if layout.inner_layout == TAG_STRIDED {
160        if let Some(is) = &layout.inner_strides {
161            for &s in &is.strides {
162                w.write_i64_le(s);
163            }
164        }
165    }
166
167    // Conditional: recurse if inner_layout == 0x04
168    if layout.inner_layout == TAG_TILED {
169        if let Some(inner) = &layout.inner_tiled {
170            encode_tiled(inner, rank, w, depth + 1)?;
171        }
172    }
173
174    Ok(())
175}
176
177// Returns TiledLayout directly so call sites can wrap it without an unreachable!()
178// match arm on the LayoutDescriptor::Tiled variant.
179fn decode_tiled(cursor: &mut ByteCursor<'_>, rank: u32, depth: u8) -> Result<TiledLayout> {
180    if depth >= MAX_TILED_DEPTH as u8 {
181        return Err(Error::SubpavingNestingTooDeep);
182    }
183
184    // tile_shape: uint64[rank]
185    let mut tile_shape = Vec::with_capacity(rank as usize);
186    for _ in 0..rank {
187        tile_shape.push(cursor.read_u64_le()?);
188    }
189
190    let outer_layout = cursor.read_u8()?;
191    let inner_layout = cursor.read_u8()?;
192    let reserved = cursor.read_bytes(2)?;
193    if reserved != [0u8, 0] {
194        return Err(Error::ReservedBytesNonZero {
195            field: "tiled._reserved",
196        });
197    }
198
199    // Conditional: outer_strides if outer_layout == strided
200    let outer_strides = if outer_layout == TAG_STRIDED {
201        let mut strides = Vec::with_capacity(rank as usize);
202        for _ in 0..rank {
203            strides.push(cursor.read_i64_le()?);
204        }
205        Some(OuterStrides::new(strides))
206    } else {
207        None
208    };
209
210    // Conditional: inner_strides if inner_layout == strided
211    let inner_strides = if inner_layout == TAG_STRIDED {
212        let mut strides = Vec::with_capacity(rank as usize);
213        for _ in 0..rank {
214            strides.push(cursor.read_i64_le()?);
215        }
216        Some(InnerStrides::new(strides))
217    } else {
218        None
219    };
220
221    // Conditional: recursive inner tiled if inner_layout == tiled
222    let inner_tiled: Option<Box<TiledLayout>> = if inner_layout == TAG_TILED {
223        Some(Box::new(decode_tiled(cursor, rank, depth + 1)?))
224    } else {
225        None
226    };
227
228    TiledLayout::new(
229        tile_shape,
230        outer_layout,
231        inner_layout,
232        outer_strides,
233        inner_strides,
234        inner_tiled,
235    )
236}
237
238// ── Morton ────────────────────────────────────────────────────────────────────
239
240fn encode_morton(layout: &MortonLayout, w: &mut ByteWriter) -> Result<()> {
241    for &bits in &layout.morton_bits {
242        w.write_u32_le(bits);
243    }
244    Ok(())
245}
246
247fn decode_morton(cursor: &mut ByteCursor<'_>, rank: u32) -> Result<LayoutDescriptor> {
248    let mut bits = Vec::with_capacity(rank as usize);
249    for _ in 0..rank {
250        bits.push(cursor.read_u32_le()?);
251    }
252    let layout = MortonLayout::new(bits)?;
253    Ok(LayoutDescriptor::Morton(layout))
254}
255
256// ── COO ───────────────────────────────────────────────────────────────────────
257
258fn encode_coo(layout: &CooLayout, w: &mut ByteWriter) -> Result<()> {
259    w.write_u64_le(layout.nnz);
260    w.write_u8(u8::from(layout.is_sorted));
261    w.write_zeros(7); // _reserved
262    Ok(())
263}
264
265fn decode_coo(cursor: &mut ByteCursor<'_>) -> Result<LayoutDescriptor> {
266    let nnz = cursor.read_u64_le()?;
267    let is_sorted = cursor.read_u8()? != 0;
268    let reserved = cursor.read_bytes(7)?;
269    if reserved.iter().any(|&b| b != 0) {
270        return Err(Error::ReservedBytesNonZero {
271            field: "coo._reserved",
272        });
273    }
274    Ok(LayoutDescriptor::Coo(CooLayout::new(nnz, is_sorted)))
275}
276
277// ── CSR ───────────────────────────────────────────────────────────────────────
278
279fn encode_csr(layout: &CsrLayout, w: &mut ByteWriter) -> Result<()> {
280    w.write_u64_le(layout.nnz);
281    w.write_zeros(8); // _reserved
282    Ok(())
283}
284
285fn decode_csr(cursor: &mut ByteCursor<'_>) -> Result<LayoutDescriptor> {
286    let nnz = cursor.read_u64_le()?;
287    let reserved = cursor.read_bytes(8)?;
288    if reserved.iter().any(|&b| b != 0) {
289        return Err(Error::ReservedBytesNonZero {
290            field: "csr._reserved",
291        });
292    }
293    Ok(LayoutDescriptor::Csr(CsrLayout::new(nnz)))
294}
295
296// ── CSC ───────────────────────────────────────────────────────────────────────
297
298fn encode_csc(layout: &CscLayout, w: &mut ByteWriter) -> Result<()> {
299    w.write_u64_le(layout.nnz);
300    w.write_zeros(8); // _reserved
301    Ok(())
302}
303
304fn decode_csc(cursor: &mut ByteCursor<'_>) -> Result<LayoutDescriptor> {
305    let nnz = cursor.read_u64_le()?;
306    let reserved = cursor.read_bytes(8)?;
307    if reserved.iter().any(|&b| b != 0) {
308        return Err(Error::ReservedBytesNonZero {
309            field: "csc._reserved",
310        });
311    }
312    Ok(LayoutDescriptor::Csc(CscLayout::new(nnz)))
313}
314
315// ── CSF ───────────────────────────────────────────────────────────────────────
316
317/// Field order for CSF (spec `docs/spec/layouts/csf.md § Additional Descriptor Fields`,
318/// all little-endian):
319///
320/// | Field         | Wire type       | Bytes          |
321/// |---------------|-----------------|----------------|
322/// | `nnz`         | `uint64`        | 8              |
323/// | `mode_order`  | `uint32[rank]`  | `4 * rank`     |
324/// | `_reserved`   | `uint8[8]`      | 8              |
325fn encode_csf(layout: &CsfLayout, rank: u32, w: &mut ByteWriter) -> Result<()> {
326    w.write_u64_le(layout.nnz);
327    // mode_order.len() IS the authoritative rank; it MUST agree with the caller's
328    // `rank` (derived from shape.rank()). Fail fast on a mismatch rather than padding
329    // or over-writing — either would emit a wire payload the decoder mis-frames.
330    let mo_len = layout.mode_order.len() as u32;
331    if mo_len != rank {
332        return Err(Error::InvalidLayout(format!(
333            "csf encode: mode_order.len() ({mo_len}) != rank ({rank}); \
334             call validate_against_shape before encoding"
335        )));
336    }
337    for &dim in &layout.mode_order {
338        w.write_u32_le(dim);
339    }
340    w.write_zeros(8); // _reserved — MUST be 0x00
341    Ok(())
342}
343
344fn decode_csf(cursor: &mut ByteCursor<'_>, rank: u32) -> Result<LayoutDescriptor> {
345    let nnz = cursor.read_u64_le()?;
346
347    // Read mode_order[rank]: the permutation of logical dimensions.
348    let mut mode_order = Vec::with_capacity(rank as usize);
349    for _ in 0..rank {
350        mode_order.push(cursor.read_u32_le()?);
351    }
352
353    // Spec: _reserved MUST be 0x00; readers MUST reject non-zero reserved bytes.
354    let reserved = cursor.read_bytes(8)?;
355    if reserved.iter().any(|&b| b != 0) {
356        return Err(Error::ReservedBytesNonZero {
357            field: "csf._reserved",
358        });
359    }
360
361    // Validate mode_order is a permutation of 0..rank.
362    // Structural validation here matches what validate_against_shape also checks,
363    // but the codec validates eagerly so a malformed wire descriptor is rejected
364    // before the caller can observe a partially-constructed CsfLayout.
365    let rank_us = rank as usize;
366    let mut seen = 0u64;
367    for (level, &dim) in mode_order.iter().enumerate() {
368        if dim as usize >= rank_us {
369            return Err(Error::InvalidLayout(format!(
370                "csf: mode_order[{level}]={dim} out of range [0, {rank})"
371            )));
372        }
373        let bit = 1u64 << dim;
374        if seen & bit != 0 {
375            return Err(Error::InvalidLayout(format!(
376                "csf: mode_order contains duplicate value {dim}"
377            )));
378        }
379        seen |= bit;
380    }
381
382    Ok(LayoutDescriptor::Csf(CsfLayout::new(nnz, mode_order)))
383}
384
385// ── BlockPaged ────────────────────────────────────────────────────────────────
386
387/// Field order for block-paged (spec § Additional Descriptor Fields, all little-endian):
388///
389/// | Field                  | Wire type | Bytes |
390/// |------------------------|-----------|-------|
391/// | `page_size`            | uint32    | 4     |
392/// | `num_pages`            | uint64    | 8     |
393/// | `paged_axis`           | uint32    | 4     |
394/// | `num_seqs`             | uint32    | 4     |
395/// | `kv_role`              | uint8     | 1     |
396/// | `layer_index`          | uint32    | 4     |
397/// | `block_table_index_type` | uint8   | 1     |
398/// | `_reserved`            | uint8[6]  | 6     |
399///                                        Total 32 bytes
400fn encode_block_paged(layout: &BlockPagedLayout, w: &mut ByteWriter) -> crate::Result<()> {
401    w.write_u32_le(layout.page_size);
402    w.write_u64_le(layout.num_pages);
403    w.write_u32_le(layout.paged_axis);
404    w.write_u32_le(layout.num_seqs);
405    w.write_u8(layout.kv_role.wire_byte());
406    // layer_index: None → 0xFFFFFFFF sentinel; Some(n) → n.
407    let layer_index_wire = layout
408        .layer_index
409        .unwrap_or(crate::layout::block_paged::LAYER_INDEX_NONE);
410    w.write_u32_le(layer_index_wire);
411    w.write_u8(layout.block_table_index_type.wire_byte());
412    w.write_zeros(6); // _reserved — MUST be 0x00
413    Ok(())
414}
415
416fn decode_block_paged(cursor: &mut ByteCursor<'_>) -> crate::Result<LayoutDescriptor> {
417    let page_size = cursor.read_u32_le()?;
418    let num_pages = cursor.read_u64_le()?;
419    let paged_axis = cursor.read_u32_le()?;
420    let num_seqs = cursor.read_u32_le()?;
421
422    let kv_role_byte = cursor.read_u8()?;
423    let kv_role = KvRole::from_wire(kv_role_byte).ok_or_else(|| {
424        crate::Error::InvalidLayout(format!(
425            "block-paged: unknown kv_role byte 0x{kv_role_byte:02X}"
426        ))
427    })?;
428
429    let layer_index_wire = cursor.read_u32_le()?;
430    let layer_index = if layer_index_wire == crate::layout::block_paged::LAYER_INDEX_NONE {
431        None
432    } else {
433        Some(layer_index_wire)
434    };
435
436    let index_type_byte = cursor.read_u8()?;
437    let block_table_index_type =
438        BlockTableIndexType::from_wire(index_type_byte).ok_or_else(|| {
439            crate::Error::InvalidLayout(format!(
440                "block-paged: unknown block_table_index_type byte 0x{index_type_byte:02X}"
441            ))
442        })?;
443
444    // Spec: readers MUST reject a descriptor with any non-zero reserved byte.
445    let reserved = cursor.read_bytes(6)?;
446    if reserved.iter().any(|&b| b != 0) {
447        return Err(crate::Error::ReservedBytesNonZero {
448            field: "block_paged._reserved",
449        });
450    }
451
452    // Block-paged quantization compatibility (block-paged.md § Quantization Compatibility)
453    // is NOT checked here: the quantization bytes are stored raw at this layer
454    // (TensorDescriptor.quantization is Option<Vec<u8>>), so the layout codec cannot see them.
455    // It is enforced in TensorDescriptor::new — the cross-section seam where the typed layout
456    // and the raw quantization bytes are both in hand — via
457    // BlockPagedLayout::validate_quantization_compatibility, alongside the shard rejection
458    // (block-paged.md § Sharding). See descriptor/mod.rs.
459
460    Ok(LayoutDescriptor::BlockPaged(BlockPagedLayout::new(
461        page_size,
462        num_pages,
463        paged_axis,
464        num_seqs,
465        kv_role,
466        layer_index,
467        block_table_index_type,
468    )))
469}
470
471// ── Composite ─────────────────────────────────────────────────────────────────
472
473/// Field order for the composite head (spec `docs/spec/layouts/composite.md`
474/// § Head Layout-Specific Fields, all little-endian):
475///
476/// | Field              | Wire type | Bytes |
477/// |---------------------|-----------|-------|
478/// | `composition_rule`  | uint8     | 1     |
479/// | `combine_op`        | uint8     | 1     |
480/// | `_reserved`         | uint8[2]  | 2     |
481/// | `member_count`      | uint32    | 4     |
482///                                     Total 8 bytes
483fn encode_composite(layout: &CompositeLayout, w: &mut ByteWriter) -> Result<()> {
484    w.write_u8(layout.rule.rule_byte());
485    w.write_u8(layout.rule.combine_op_byte());
486    w.write_zeros(2); // _reserved — MUST be 0x00
487    w.write_u32_le(layout.member_count);
488    Ok(())
489}
490
491fn decode_composite(cursor: &mut ByteCursor<'_>) -> Result<LayoutDescriptor> {
492    let rule_byte = cursor.read_u8()?;
493    let combine_op_byte = cursor.read_u8()?;
494
495    // Spec: readers MUST reject a descriptor with any non-zero reserved byte.
496    let reserved = cursor.read_bytes(2)?;
497    if reserved.iter().any(|&b| b != 0) {
498        return Err(Error::ReservedBytesNonZero {
499            field: "composite._reserved",
500        });
501    }
502
503    let member_count = cursor.read_u32_le()?;
504
505    let rule = CompositionRule::from_wire(rule_byte, combine_op_byte)?;
506    let layout = CompositeLayout::new(rule, member_count)?;
507    Ok(LayoutDescriptor::Composite(layout))
508}
509
510// ── Hilbert ───────────────────────────────────────────────────────────────────
511
512fn encode_hilbert(layout: &HilbertLayout, w: &mut ByteWriter) -> Result<()> {
513    w.write_u32_le(layout.hilbert_order);
514    w.write_u32_le(layout.hilbert_rank);
515    Ok(())
516}
517
518fn decode_hilbert(cursor: &mut ByteCursor<'_>) -> Result<LayoutDescriptor> {
519    let hilbert_order = cursor.read_u32_le()?;
520    let hilbert_rank = cursor.read_u32_le()?;
521    let layout = HilbertLayout::new(hilbert_order, hilbert_rank)?;
522    Ok(LayoutDescriptor::Hilbert(layout))
523}
524
525// ── PrivateExtension ──────────────────────────────────────────────────────────
526
527fn encode_private_extension(layout: &PrivateExtensionLayout, w: &mut ByteWriter) -> Result<()> {
528    w.write_u64_le(layout.extension_layout_id);
529    w.write_u32_le(layout.extension_data.len() as u32);
530    w.write_bytes(&layout.extension_data);
531    Ok(())
532}
533
534fn decode_private_extension(tag: u8, cursor: &mut ByteCursor<'_>) -> Result<LayoutDescriptor> {
535    let extension_layout_id = cursor.read_u64_le()?;
536    let extension_data_length = cursor.read_u32_le()?;
537    let data = cursor.read_bytes(extension_data_length as usize)?.to_vec();
538    let layout = PrivateExtensionLayout::new(tag, extension_layout_id, data)?;
539    Ok(LayoutDescriptor::PrivateExtension(layout))
540}
541
542// ── Tests ─────────────────────────────────────────────────────────────────────
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547    use crate::descriptor::cursor::{ByteCursor, ByteWriter};
548
549    fn round_trip(layout: &LayoutDescriptor, rank: u32) -> LayoutDescriptor {
550        let mut w = ByteWriter::new();
551        encode_layout_payload(layout, rank, &mut w).unwrap();
552        let bytes = w.into_vec();
553        let mut c = ByteCursor::new(&bytes, bytes.len());
554        decode_layout_payload(layout.tag(), rank, &mut c, 0).unwrap()
555    }
556
557    #[test]
558    fn row_major_round_trip() {
559        let layout = LayoutDescriptor::RowMajor;
560        assert_eq!(round_trip(&layout, 2), layout);
561    }
562
563    #[test]
564    fn col_major_round_trip() {
565        let layout = LayoutDescriptor::ColMajor;
566        assert_eq!(round_trip(&layout, 2), layout);
567    }
568
569    #[test]
570    fn strided_round_trip() {
571        let layout = LayoutDescriptor::Strided(StridedLayout::new(vec![4, 1]));
572        assert_eq!(round_trip(&layout, 2), layout);
573    }
574
575    #[test]
576    fn morton_round_trip() {
577        let layout = LayoutDescriptor::Morton(MortonLayout::new(vec![4, 4]).unwrap());
578        assert_eq!(round_trip(&layout, 2), layout);
579    }
580
581    #[test]
582    fn coo_round_trip() {
583        let layout = LayoutDescriptor::Coo(CooLayout::new(42, true));
584        assert_eq!(round_trip(&layout, 2), layout);
585    }
586
587    #[test]
588    fn csr_round_trip() {
589        let layout = LayoutDescriptor::Csr(CsrLayout::new(100));
590        assert_eq!(round_trip(&layout, 2), layout);
591    }
592
593    #[test]
594    fn csc_round_trip() {
595        let layout = LayoutDescriptor::Csc(CscLayout::new(50));
596        assert_eq!(round_trip(&layout, 2), layout);
597    }
598
599    #[test]
600    fn hilbert_round_trip() {
601        let layout = LayoutDescriptor::Hilbert(HilbertLayout::new(3, 2).unwrap());
602        assert_eq!(round_trip(&layout, 2), layout);
603    }
604
605    #[test]
606    fn private_extension_round_trip() {
607        let layout = LayoutDescriptor::PrivateExtension(
608            PrivateExtensionLayout::new(0xF0, 0xDEAD_BEEF, vec![1, 2, 3]).unwrap(),
609        );
610        assert_eq!(round_trip(&layout, 0), layout);
611    }
612
613    #[test]
614    fn tiled_row_row_round_trip() {
615        let t = TiledLayout::new(vec![4, 4], 0x01, 0x01, None, None, None).unwrap();
616        let layout = LayoutDescriptor::Tiled(Box::new(t));
617        assert_eq!(round_trip(&layout, 2), layout);
618    }
619
620    #[test]
621    fn tiled_strided_outer_round_trip() {
622        let os = OuterStrides::new(vec![2, 1]);
623        let t = TiledLayout::new(vec![8, 8], 0x03, 0x01, Some(os), None, None).unwrap();
624        let layout = LayoutDescriptor::Tiled(Box::new(t));
625        assert_eq!(round_trip(&layout, 2), layout);
626    }
627
628    #[test]
629    fn tiled_nested_round_trip() {
630        let inner = TiledLayout::new(vec![2, 2], 0x01, 0x02, None, None, None).unwrap();
631        let outer =
632            TiledLayout::new(vec![4, 4], 0x01, 0x04, None, None, Some(Box::new(inner))).unwrap();
633        let layout = LayoutDescriptor::Tiled(Box::new(outer));
634        assert_eq!(round_trip(&layout, 2), layout);
635    }
636
637    // ── Composite ─────────────────────────────────────────────────────────────
638
639    #[test]
640    fn composite_partition_round_trip() {
641        let layout = LayoutDescriptor::Composite(
642            CompositeLayout::new(CompositionRule::Partition, 3).unwrap(),
643        );
644        assert_eq!(round_trip(&layout, 0), layout);
645    }
646
647    #[test]
648    fn composite_group_round_trip() {
649        let layout =
650            LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Group, 0).unwrap());
651        assert_eq!(round_trip(&layout, 0), layout);
652    }
653
654    #[test]
655    fn composite_overlay_replace_round_trip() {
656        use crate::layout::CombineOp;
657        let layout = LayoutDescriptor::Composite(
658            CompositeLayout::new(CompositionRule::Overlay(CombineOp::Replace), 2).unwrap(),
659        );
660        assert_eq!(round_trip(&layout, 0), layout);
661    }
662
663    #[test]
664    fn composite_overlay_add_round_trip() {
665        use crate::layout::CombineOp;
666        let layout = LayoutDescriptor::Composite(
667            CompositeLayout::new(CompositionRule::Overlay(CombineOp::Add), 5).unwrap(),
668        );
669        assert_eq!(round_trip(&layout, 0), layout);
670    }
671
672    /// Non-zero `_reserved` bytes in the composite head payload are rejected.
673    #[test]
674    fn composite_reserved_bytes_rejected() {
675        let mut w = ByteWriter::new();
676        w.write_u8(0x01); // composition_rule = partition
677        w.write_u8(0x00); // combine_op
678        w.write_u8(0xFF); // _reserved[0] non-zero
679        w.write_u8(0x00);
680        w.write_u32_le(0); // member_count
681        let bytes = w.into_vec();
682        let mut c = ByteCursor::new(&bytes, bytes.len());
683        let err = decode_layout_payload(TAG_COMPOSITE, 0, &mut c, 0).unwrap_err();
684        assert!(matches!(err, Error::ReservedBytesNonZero { .. }));
685    }
686
687    /// The `member_count = 0xFFFFFFFF` open-composite sentinel is rejected at
688    /// the codec layer too, not only by `CompositeLayout::new` — confirming a
689    /// decoder that reads raw wire bytes cannot bypass the constructor guard.
690    #[test]
691    fn composite_open_sentinel_rejected_at_codec_layer() {
692        let mut w = ByteWriter::new();
693        w.write_u8(0x01); // composition_rule = partition
694        w.write_u8(0x00); // combine_op
695        w.write_zeros(2); // _reserved
696        w.write_u32_le(0xFFFF_FFFF); // member_count = open-composite sentinel
697        let bytes = w.into_vec();
698        let mut c = ByteCursor::new(&bytes, bytes.len());
699        let err = decode_layout_payload(TAG_COMPOSITE, 0, &mut c, 0).unwrap_err();
700        assert!(matches!(err, Error::OpenCompositeReserved));
701    }
702
703    #[test]
704    fn coo_reserved_bytes_rejected() {
705        let mut w = ByteWriter::new();
706        w.write_u64_le(0u64); // nnz
707        w.write_u8(0u8); // is_sorted
708        w.write_u8(0xFFu8); // _reserved[0] non-zero
709        w.write_zeros(6);
710        let bytes = w.into_vec();
711        let mut c = ByteCursor::new(&bytes, bytes.len());
712        let err = decode_layout_payload(TAG_COO, 0, &mut c, 0).unwrap_err();
713        assert!(matches!(err, Error::ReservedBytesNonZero { .. }));
714    }
715
716    #[test]
717    fn invalid_tag_rejected() {
718        let bytes: &[u8] = &[];
719        let mut c = ByteCursor::new(bytes, 0);
720        let err = decode_layout_payload(0x00, 0, &mut c, 0).unwrap_err();
721        assert!(matches!(err, Error::InvalidLayoutTag(0x00)));
722    }
723
724    #[test]
725    fn reserved_tag_rejected() {
726        // 0x0A is now TAG_BLOCK_PAGED (a named layout), not reserved. Use 0x0C instead.
727        let bytes: &[u8] = &[];
728        let mut c = ByteCursor::new(bytes, 0);
729        let err = decode_layout_payload(0x0C, 0, &mut c, 0).unwrap_err();
730        assert!(matches!(err, Error::ReservedLayoutTag(0x0C)));
731    }
732
733    // ── Unknown layout encode rejection ───────────────────────────────────────
734
735    mod unknown_encode {
736        use crate::descriptor::TensorDescriptor;
737        use crate::layout::{LayoutDescriptor, UnknownLayout};
738        use crate::{
739            BufferHandle, DeviceTag, ElementType, Error, Shape, SyncMode, MIN_BUFFER_ALIGNMENT,
740        };
741
742        /// `encode_layout_payload` MUST return `Err(UnknownLayoutTag)` when the
743        /// layout descriptor is `LayoutDescriptor::Unknown`.
744        ///
745        /// The spec (§ strict mode) prohibits re-encoding unrecognized layouts;
746        /// the implementation enforces this by rejecting `Unknown` on the encode
747        /// path, not only on the decode path.
748        #[test]
749        fn encode_unknown_layout_returns_error() {
750            // Use a reserved-range tag (0x0C) for the Unknown variant, since 0x0A is now TAG_BLOCK_PAGED.
751            let unknown_layout =
752                LayoutDescriptor::Unknown(UnknownLayout::new(0x0C, vec![0x01, 0x02]).unwrap());
753            let shape = Shape::new(vec![4u64]).unwrap();
754            let buf = BufferHandle::new(
755                64,
756                MIN_BUFFER_ALIGNMENT,
757                DeviceTag::Cpu,
758                SyncMode::ProducerSynced,
759            )
760            .unwrap();
761            // TensorDescriptor::new accepts Unknown (it does not validate the layout tag).
762            let desc = TensorDescriptor::new(
763                1,
764                0,
765                ElementType::Float32,
766                shape,
767                0,
768                unknown_layout,
769                vec![buf],
770                None,
771                None,
772                None,
773                None,
774            )
775            .unwrap();
776            let err = desc.encode().unwrap_err();
777            assert!(
778                matches!(err, Error::UnknownLayoutTag(_)),
779                "expected UnknownLayoutTag, got {err:?}"
780            );
781        }
782    }
783}