Skip to main content

hurray_core/layout/
composite.rs

1//! Composite / Virtual layout descriptor — the head payload for a composite tensor.
2//!
3//! Tag `0x0B`. The composite head is a **virtual** (data-less) descriptor: it owns
4//! no buffers. Its layout-specific payload declares the composition rule that binds
5//! the `member_count` tensor descriptors that immediately follow it in stream/file
6//! order. See `docs/spec/layouts/composite.md` for the full normative definition.
7//!
8//! This module carries only the wire-level head payload (`CompositeLayout` and its
9//! `CompositionRule`/`CombineOp` fields). The cross-descriptor aggregate — a head
10//! plus its actual member `TensorDescriptor`s, and the stateful validator that
11//! checks them against each other — lives in the separate top-level
12//! [`crate::composite`] module (distinct module path; see that module's docs for
13//! why the two are kept apart).
14
15use crate::{Error, Result};
16
17/// Wire sentinel for `member_count` denoting an **open composite** (unbounded,
18/// appendable membership). RESERVED in v1.0 — see `docs/spec/layouts/composite.md`
19/// § Head Layout-Specific Fields. [`CompositeLayout::new`] rejects it.
20pub(crate) const OPEN_COMPOSITE_SENTINEL: u32 = 0xFFFF_FFFF;
21
22/// The combine operation for an **overlay** composition (`composition_rule = 0x02`).
23///
24/// Selects how a correction member's value at a covered index combines with the
25/// value shown by lower-precedence members (down to the base).
26///
27/// # Examples
28///
29/// ```
30/// use hurray_core::layout::CombineOp;
31///
32/// assert_eq!(CombineOp::Replace.wire_byte(), 0x01);
33/// assert_eq!(CombineOp::Add.wire_byte(), 0x02);
34/// assert_eq!(CombineOp::from_wire(0x01), Some(CombineOp::Replace));
35/// assert_eq!(CombineOp::from_wire(0x00), None);
36/// ```
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38#[non_exhaustive]
39pub enum CombineOp {
40    /// Last-wins: the topmost (latest-emitted) member covering an index wins
41    /// within its box, and the base shows through elsewhere. Wire byte `0x01`.
42    Replace,
43    /// The value at an index is the base value plus the sum of all covering
44    /// corrections' values at that index. Wire byte `0x02`.
45    Add,
46}
47
48impl CombineOp {
49    /// Returns the wire byte for this combine operation.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use hurray_core::layout::CombineOp;
55    ///
56    /// assert_eq!(CombineOp::Replace.wire_byte(), 0x01);
57    /// ```
58    #[inline]
59    pub fn wire_byte(self) -> u8 {
60        match self {
61            Self::Replace => 0x01,
62            Self::Add => 0x02,
63        }
64    }
65
66    /// Constructs a [`CombineOp`] from its wire byte. Returns `None` for any
67    /// value other than `0x01` or `0x02`.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// use hurray_core::layout::CombineOp;
73    ///
74    /// assert_eq!(CombineOp::from_wire(0x02), Some(CombineOp::Add));
75    /// assert_eq!(CombineOp::from_wire(0xFF), None);
76    /// ```
77    #[inline]
78    pub fn from_wire(byte: u8) -> Option<Self> {
79        match byte {
80            0x01 => Some(Self::Replace),
81            0x02 => Some(Self::Add),
82            _ => None,
83        }
84    }
85}
86
87/// The composition rule declared by a composite head (`composition_rule` wire field).
88///
89/// Models `combine_op` as part of the rule (rather than a sibling field) so that
90/// illegal combinations — e.g. a non-zero `combine_op` on a partition or group head —
91/// are unrepresentable in this type: only [`CompositionRule::Overlay`] carries a
92/// [`CombineOp`] at all.
93///
94/// # Examples
95///
96/// ```
97/// use hurray_core::layout::{CombineOp, CompositionRule};
98///
99/// let partition = CompositionRule::Partition;
100/// assert_eq!(partition.rule_byte(), 0x01);
101/// assert_eq!(partition.combine_op_byte(), 0x00);
102///
103/// let overlay = CompositionRule::Overlay(CombineOp::Add);
104/// assert_eq!(overlay.rule_byte(), 0x02);
105/// assert_eq!(overlay.combine_op_byte(), 0x02);
106///
107/// assert_eq!(
108///     CompositionRule::from_wire(0x02, 0x01).unwrap(),
109///     CompositionRule::Overlay(CombineOp::Replace),
110/// );
111/// ```
112#[derive(Debug, Clone, PartialEq, Eq, Hash)]
113#[non_exhaustive]
114pub enum CompositionRule {
115    /// Exact-cover, non-overlapping tiling of the head's index space. Wire `0x01`.
116    Partition,
117    /// A base spanning the whole index space plus scattered corrections. Wire `0x02`.
118    Overlay(CombineOp),
119    /// Independent tensors under one head identity; no spatial semantics. Wire `0x03`.
120    Group,
121}
122
123impl CompositionRule {
124    /// Returns the wire `composition_rule` byte for this rule.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// use hurray_core::layout::{CombineOp, CompositionRule};
130    ///
131    /// assert_eq!(CompositionRule::Group.rule_byte(), 0x03);
132    /// assert_eq!(CompositionRule::Overlay(CombineOp::Replace).rule_byte(), 0x02);
133    /// ```
134    #[inline]
135    pub fn rule_byte(&self) -> u8 {
136        match self {
137            Self::Partition => 0x01,
138            Self::Overlay(_) => 0x02,
139            Self::Group => 0x03,
140        }
141    }
142
143    /// Returns the wire `combine_op` byte: `0x00` unless this is
144    /// [`CompositionRule::Overlay`], in which case it is the wrapped
145    /// [`CombineOp`]'s wire byte.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use hurray_core::layout::CompositionRule;
151    ///
152    /// assert_eq!(CompositionRule::Partition.combine_op_byte(), 0x00);
153    /// ```
154    #[inline]
155    pub fn combine_op_byte(&self) -> u8 {
156        match self {
157            Self::Overlay(op) => op.wire_byte(),
158            Self::Partition | Self::Group => 0x00,
159        }
160    }
161
162    /// Constructs a [`CompositionRule`] from its wire `composition_rule` and
163    /// `combine_op` bytes, validating both per spec § Head Layout-Specific Fields.
164    ///
165    /// The private-range rejection (`0xF0`–`0xFE`) mirrors
166    /// [`crate::layout::is_private_tag`]'s layout-tag convention, for consistency
167    /// across the crate's wire-byte taxonomies.
168    ///
169    /// # Errors
170    ///
171    /// - [`Error::InvalidCompositionRule`] — `rule == 0xFF` (permanently invalid).
172    /// - [`Error::ReservedCompositionRule`] — `rule == 0x00` or in `0x04..=0xEF`.
173    /// - [`Error::PrivateCompositionRule`] — `rule` is in `0xF0..=0xFE`.
174    /// - [`Error::InvalidCombineOp`] — `combine_op` is not `0x00` for partition/group,
175    ///   or not `0x01`/`0x02` for overlay.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use hurray_core::{Error, layout::{CombineOp, CompositionRule}};
181    ///
182    /// assert_eq!(CompositionRule::from_wire(0x01, 0x00).unwrap(), CompositionRule::Partition);
183    /// assert_eq!(CompositionRule::from_wire(0x03, 0x00).unwrap(), CompositionRule::Group);
184    /// assert_eq!(
185    ///     CompositionRule::from_wire(0x02, 0x02).unwrap(),
186    ///     CompositionRule::Overlay(CombineOp::Add),
187    /// );
188    ///
189    /// // 0x00 is reserved (not the permanently-invalid sentinel — that's 0xFF).
190    /// assert!(matches!(
191    ///     CompositionRule::from_wire(0x00, 0x00),
192    ///     Err(Error::ReservedCompositionRule(0x00))
193    /// ));
194    /// assert!(matches!(
195    ///     CompositionRule::from_wire(0xFF, 0x00),
196    ///     Err(Error::InvalidCompositionRule(0xFF))
197    /// ));
198    /// // A non-zero combine_op on a partition head is rejected.
199    /// assert!(CompositionRule::from_wire(0x01, 0x01).is_err());
200    /// // combine_op outside {0x01, 0x02} on an overlay head is rejected.
201    /// assert!(CompositionRule::from_wire(0x02, 0x03).is_err());
202    /// ```
203    pub fn from_wire(rule: u8, combine_op: u8) -> Result<Self> {
204        // Every u8 value is classified by exactly one of these four arms
205        // (0x00, 0x01-0x03, 0x04-0xEF, 0xF0-0xFE, 0xFF) — rustc proves this match
206        // exhaustive without a wildcard, so none is added here.
207        match rule {
208            0xFF => return Err(Error::InvalidCompositionRule(rule)),
209            0x00 | 0x04..=0xEF => return Err(Error::ReservedCompositionRule(rule)),
210            0xF0..=0xFE => return Err(Error::PrivateCompositionRule(rule)),
211            0x01..=0x03 => {}
212        }
213
214        match (rule, combine_op) {
215            (0x01, 0x00) => Ok(Self::Partition),
216            (0x03, 0x00) => Ok(Self::Group),
217            (0x02, 0x01) => Ok(Self::Overlay(CombineOp::Replace)),
218            (0x02, 0x02) => Ok(Self::Overlay(CombineOp::Add)),
219            _ => Err(Error::InvalidCombineOp { rule, combine_op }),
220        }
221    }
222}
223
224/// The composite head's layout-specific payload (spec § Head Layout-Specific Fields).
225///
226/// Carried by [`crate::layout::LayoutDescriptor::Composite`]. See
227/// `docs/spec/layouts/composite.md` and [`crate::composite`] for the full head +
228/// members model and cross-member validation.
229///
230/// # Examples
231///
232/// ```
233/// use hurray_core::layout::{CompositeLayout, CompositionRule, LayoutDescriptor};
234///
235/// let layout = CompositeLayout::new(CompositionRule::Partition, 2).unwrap();
236/// let desc = LayoutDescriptor::Composite(layout);
237/// assert_eq!(desc.tag(), 0x0B);
238/// assert!(desc.buffer_count().is_none()); // "known zero" isn't NonZeroU8-representable
239/// assert!(desc.is_virtual());
240/// ```
241#[derive(Debug, Clone, PartialEq, Eq, Hash)]
242#[non_exhaustive]
243pub struct CompositeLayout {
244    /// The composition rule (`composition_rule` + `combine_op` wire fields).
245    pub rule: CompositionRule,
246    /// Number of member tensors that immediately follow the head. A definite count
247    /// for every v1.0 composition rule; `0xFFFFFFFF` (open composite) is RESERVED.
248    pub member_count: u32,
249}
250
251impl CompositeLayout {
252    /// Creates a new [`CompositeLayout`].
253    ///
254    /// # Errors
255    ///
256    /// Returns [`Error::OpenCompositeReserved`] if `member_count == 0xFFFFFFFF` (the
257    /// open-composite sentinel, RESERVED and not usable in v1.0).
258    ///
259    /// # Examples
260    ///
261    /// ```
262    /// use hurray_core::layout::{CompositeLayout, CompositionRule};
263    ///
264    /// let ok = CompositeLayout::new(CompositionRule::Group, 3).unwrap();
265    /// assert_eq!(ok.member_count, 3);
266    ///
267    /// // The open-composite sentinel is reserved in v1.0.
268    /// assert!(CompositeLayout::new(CompositionRule::Partition, 0xFFFF_FFFF).is_err());
269    /// ```
270    pub fn new(rule: CompositionRule, member_count: u32) -> Result<Self> {
271        if member_count == OPEN_COMPOSITE_SENTINEL {
272            return Err(Error::OpenCompositeReserved);
273        }
274        Ok(Self { rule, member_count })
275    }
276}
277
278// ── Tests ─────────────────────────────────────────────────────────────────────
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::layout::LayoutDescriptor;
284    use crate::shape::DYNAMIC;
285    use crate::Shape;
286
287    // ── CombineOp wire round trip ───────────────────────────────────────────
288
289    #[test]
290    fn combine_op_wire_round_trip() {
291        for op in [CombineOp::Replace, CombineOp::Add] {
292            let byte = op.wire_byte();
293            assert_eq!(CombineOp::from_wire(byte), Some(op));
294        }
295    }
296
297    #[test]
298    fn combine_op_wire_bytes_match_spec() {
299        assert_eq!(CombineOp::Replace.wire_byte(), 0x01);
300        assert_eq!(CombineOp::Add.wire_byte(), 0x02);
301    }
302
303    #[test]
304    fn combine_op_from_wire_rejects_invalid_bytes() {
305        for byte in [0x00_u8, 0x03, 0xFF] {
306            assert!(
307                CombineOp::from_wire(byte).is_none(),
308                "0x{byte:02X} should not decode to a CombineOp"
309            );
310        }
311    }
312
313    // ── CompositionRule wire round trip ─────────────────────────────────────
314
315    #[test]
316    fn composition_rule_round_trip_partition() {
317        let rule = CompositionRule::from_wire(0x01, 0x00).unwrap();
318        assert_eq!(rule, CompositionRule::Partition);
319        assert_eq!(rule.rule_byte(), 0x01);
320        assert_eq!(rule.combine_op_byte(), 0x00);
321    }
322
323    #[test]
324    fn composition_rule_round_trip_group() {
325        let rule = CompositionRule::from_wire(0x03, 0x00).unwrap();
326        assert_eq!(rule, CompositionRule::Group);
327        assert_eq!(rule.rule_byte(), 0x03);
328        assert_eq!(rule.combine_op_byte(), 0x00);
329    }
330
331    #[test]
332    fn composition_rule_round_trip_overlay_replace() {
333        let rule = CompositionRule::from_wire(0x02, 0x01).unwrap();
334        assert_eq!(rule, CompositionRule::Overlay(CombineOp::Replace));
335        assert_eq!(rule.rule_byte(), 0x02);
336        assert_eq!(rule.combine_op_byte(), 0x01);
337    }
338
339    #[test]
340    fn composition_rule_round_trip_overlay_add() {
341        let rule = CompositionRule::from_wire(0x02, 0x02).unwrap();
342        assert_eq!(rule, CompositionRule::Overlay(CombineOp::Add));
343        assert_eq!(rule.rule_byte(), 0x02);
344        assert_eq!(rule.combine_op_byte(), 0x02);
345    }
346
347    // ── CompositionRule::from_wire rejection ────────────────────────────────
348
349    #[test]
350    fn composition_rule_rejects_reserved_zero() {
351        assert!(matches!(
352            CompositionRule::from_wire(0x00, 0x00),
353            Err(Error::ReservedCompositionRule(0x00))
354        ));
355    }
356
357    #[test]
358    fn composition_rule_rejects_invalid_0xff() {
359        assert!(matches!(
360            CompositionRule::from_wire(0xFF, 0x00),
361            Err(Error::InvalidCompositionRule(0xFF))
362        ));
363    }
364
365    #[test]
366    fn composition_rule_rejects_reserved_range() {
367        for rule in [0x04_u8, 0x50, 0xEF] {
368            assert!(
369                matches!(
370                    CompositionRule::from_wire(rule, 0x00),
371                    Err(Error::ReservedCompositionRule(_))
372                ),
373                "0x{rule:02X} should be ReservedCompositionRule"
374            );
375        }
376    }
377
378    #[test]
379    fn composition_rule_rejects_private_range() {
380        for rule in [0xF0_u8, 0xF7, 0xFE] {
381            assert!(
382                matches!(
383                    CompositionRule::from_wire(rule, 0x00),
384                    Err(Error::PrivateCompositionRule(_))
385                ),
386                "0x{rule:02X} should be PrivateCompositionRule"
387            );
388        }
389    }
390
391    #[test]
392    fn composition_rule_rejects_nonzero_combine_op_for_partition() {
393        assert!(matches!(
394            CompositionRule::from_wire(0x01, 0x01),
395            Err(Error::InvalidCombineOp {
396                rule: 0x01,
397                combine_op: 0x01
398            })
399        ));
400    }
401
402    #[test]
403    fn composition_rule_rejects_nonzero_combine_op_for_group() {
404        assert!(matches!(
405            CompositionRule::from_wire(0x03, 0x02),
406            Err(Error::InvalidCombineOp {
407                rule: 0x03,
408                combine_op: 0x02
409            })
410        ));
411    }
412
413    #[test]
414    fn composition_rule_rejects_combine_op_outside_replace_add_for_overlay() {
415        for combine_op in [0x00_u8, 0x03, 0xFF] {
416            assert!(
417                matches!(
418                    CompositionRule::from_wire(0x02, combine_op),
419                    Err(Error::InvalidCombineOp { rule: 0x02, .. })
420                ),
421                "combine_op 0x{combine_op:02X} should be rejected for overlay"
422            );
423        }
424    }
425
426    // ── CompositeLayout::new ────────────────────────────────────────────────
427
428    #[test]
429    fn composite_layout_new_rejects_open_composite_sentinel() {
430        assert!(matches!(
431            CompositeLayout::new(CompositionRule::Partition, OPEN_COMPOSITE_SENTINEL),
432            Err(Error::OpenCompositeReserved)
433        ));
434    }
435
436    #[test]
437    fn composite_layout_new_accepts_definite_counts() {
438        let layout = CompositeLayout::new(CompositionRule::Group, 0).unwrap();
439        assert_eq!(layout.member_count, 0);
440        let layout =
441            CompositeLayout::new(CompositionRule::Partition, OPEN_COMPOSITE_SENTINEL - 1).unwrap();
442        assert_eq!(layout.member_count, OPEN_COMPOSITE_SENTINEL - 1);
443    }
444
445    // ── LayoutDescriptor::Composite integration ─────────────────────────────
446
447    #[test]
448    fn composite_layout_descriptor_tag_is_0x0b() {
449        let layout =
450            LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Group, 0).unwrap());
451        assert_eq!(layout.tag(), 0x0B);
452    }
453
454    #[test]
455    fn composite_layout_descriptor_buffer_count_is_none() {
456        let layout = LayoutDescriptor::Composite(
457            CompositeLayout::new(CompositionRule::Partition, 2).unwrap(),
458        );
459        assert!(layout.buffer_count().is_none());
460    }
461
462    #[test]
463    fn composite_layout_descriptor_is_virtual() {
464        let composite =
465            LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Group, 0).unwrap());
466        assert!(composite.is_virtual());
467        // A non-composite layout must NOT be reported virtual — proves the
468        // predicate actually discriminates rather than trivially returning true.
469        assert!(!LayoutDescriptor::RowMajor.is_virtual());
470    }
471
472    #[test]
473    fn composite_tag_is_not_reserved() {
474        assert!(!crate::layout::is_reserved_tag(0x0B));
475    }
476
477    #[test]
478    fn composite_tag_passes_strict_validation() {
479        assert!(crate::layout::validate_layout_tag_strict(0x0B).is_ok());
480    }
481
482    #[test]
483    fn composite_element_offset_returns_layout_is_virtual() {
484        let layout =
485            LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Group, 0).unwrap());
486        let shape = Shape::new(vec![4u64]).unwrap();
487        let err = layout.element_offset(&[0], &shape).unwrap_err();
488        assert!(matches!(err, Error::LayoutIsVirtual { layout_tag: 0x0B }));
489    }
490
491    // ── validate_against_shape: DYNAMIC head shape rejection ────────────────
492
493    #[test]
494    fn composite_validate_against_shape_rejects_dynamic_for_partition() {
495        let layout = LayoutDescriptor::Composite(
496            CompositeLayout::new(CompositionRule::Partition, 0).unwrap(),
497        );
498        let shape = Shape::new(vec![DYNAMIC, 4]).unwrap();
499        assert!(matches!(
500            layout.validate_against_shape(&shape),
501            Err(Error::InvalidLayout(_))
502        ));
503    }
504
505    #[test]
506    fn composite_validate_against_shape_rejects_dynamic_for_overlay() {
507        let layout = LayoutDescriptor::Composite(
508            CompositeLayout::new(CompositionRule::Overlay(CombineOp::Replace), 0).unwrap(),
509        );
510        let shape = Shape::new(vec![DYNAMIC]).unwrap();
511        assert!(matches!(
512            layout.validate_against_shape(&shape),
513            Err(Error::InvalidLayout(_))
514        ));
515    }
516
517    #[test]
518    fn composite_validate_against_shape_accepts_dynamic_for_group() {
519        // Group has no spatial semantics, so a DYNAMIC head dimension is fine.
520        let layout =
521            LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Group, 0).unwrap());
522        let shape = Shape::new(vec![DYNAMIC]).unwrap();
523        assert!(layout.validate_against_shape(&shape).is_ok());
524    }
525
526    #[test]
527    fn composite_validate_against_shape_accepts_static_shape_for_partition() {
528        let layout = LayoutDescriptor::Composite(
529            CompositeLayout::new(CompositionRule::Partition, 0).unwrap(),
530        );
531        let shape = Shape::new(vec![4u64, 4]).unwrap();
532        assert!(layout.validate_against_shape(&shape).is_ok());
533    }
534}