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}