Skip to main content

hurray_io/stream/sans_io/
composite.rs

1//! Reassembly of composite tensors from the flat tensors a stream carries.
2//!
3//! This is bookkeeping, not I/O. The wire carries a composite as its head followed by its
4//! members, each of which may be a composite in turn (ADR-027 § Binding), and nothing about
5//! putting that tree back together needs a byte to arrive: by the time a tensor reaches this
6//! module it is already decoded. So it lives beside the byte-level state machine rather than
7//! inside it — [`StreamReaderCore`][super::StreamReaderCore] stays a machine over bytes, and
8//! every driver that wants structure layers this on top.
9//!
10//! That split is also what lets one reader offer both views of the same stream: flat tensors
11//! in wire order, or assembled composites, from the same core.
12
13use hurray_core::{CompositeValidator, LayoutDescriptor, TensorDescriptor};
14
15use crate::stream::types::{StreamComposite, StreamItem, StreamTensor};
16use crate::{Error, Result};
17
18/// A composite head whose members have not all arrived.
19struct Open {
20    head: TensorDescriptor,
21    member_count: u32,
22    members: Vec<StreamItem>,
23    validator: CompositeValidator,
24}
25
26/// Rebuilds composite tensors from a stream of flat tensors.
27///
28/// Feed every tensor the stream yields, in wire order, to [`push`][Self::push]. Plain
29/// tensors come straight back out; a composite head opens a group that stays open until its
30/// declared `member_count` members have been fed, at which point the whole composite is
31/// returned as one item. Nesting is tracked with an explicit stack rather than recursion
32/// (ADR-041 § 10), so arbitrarily deep composites cost stack space in the heap, not on the
33/// call stack — and a half-assembled tree survives between calls.
34///
35/// Members are validated as they arrive, with the same [`CompositeValidator`] the writer
36/// uses: member count, partition exact-cover, and overlay base-first ordering.
37///
38/// # Examples
39///
40/// ```
41/// use hurray_core::{
42///     layout::{CompositeLayout, CompositionRule},
43///     BufferHandle, DeviceTag, ElementType, LayoutDescriptor, Shape, SyncMode,
44///     TensorDescriptor, MIN_BUFFER_ALIGNMENT,
45/// };
46/// use hurray_io::stream::{
47///     CompositeAssembler, StreamItem, StreamTensor, DEFAULT_MAX_COMPOSITE_DEPTH,
48/// };
49///
50/// // A group of one, and the tensor that fills it.
51/// let head = TensorDescriptor::new(
52///     1, 0, ElementType::Float32, Shape::new(vec![1u64])?, 0,
53///     LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Group, 1)?),
54///     vec![], None, None, None, None,
55/// )?;
56/// let handle =
57///     BufferHandle::new(4, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced)?;
58/// let member = TensorDescriptor::new(
59///     1, 0, ElementType::Float32, Shape::new(vec![1u64])?, 0,
60///     LayoutDescriptor::RowMajor, vec![handle], None, None, None, None,
61/// )?;
62///
63/// let mut assembler = CompositeAssembler::new(DEFAULT_MAX_COMPOSITE_DEPTH);
64///
65/// // The head opens the group: nothing comes out yet.
66/// let head_tensor = StreamTensor { descriptor: head, buffers: vec![] };
67/// assert!(assembler.push(head_tensor)?.is_none());
68/// assert_eq!(assembler.depth(), 1);
69///
70/// // The member closes it, and the whole composite comes back as one item.
71/// let member_tensor = StreamTensor {
72///     descriptor: member,
73///     buffers: vec![bytes::Bytes::from_static(&[0u8; 4])],
74/// };
75/// let item = assembler.push(member_tensor)?.expect("the group is complete");
76/// let StreamItem::Composite(composite) = item else { panic!("expected a composite") };
77/// assert_eq!(composite.members.len(), 1);
78/// assert_eq!(assembler.depth(), 0);
79/// # Ok::<(), Box<dyn std::error::Error>>(())
80/// ```
81pub struct CompositeAssembler {
82    max_depth: usize,
83    stack: Vec<Open>,
84}
85
86impl CompositeAssembler {
87    /// Creates an assembler that refuses nesting deeper than `max_depth`.
88    ///
89    /// [`DEFAULT_MAX_COMPOSITE_DEPTH`][crate::stream::DEFAULT_MAX_COMPOSITE_DEPTH] is the
90    /// limit the readers apply.
91    ///
92    /// # Examples
93    ///
94    /// See the type-level example.
95    pub fn new(max_depth: usize) -> Self {
96        Self {
97            max_depth,
98            stack: Vec::new(),
99        }
100    }
101
102    /// Feeds the next tensor from the stream.
103    ///
104    /// Returns `Some` when a top-level item is complete — a plain tensor outside any
105    /// composite, or the outermost composite once its last member arrives — and `None`
106    /// while a composite is still being filled.
107    ///
108    /// # Errors
109    ///
110    /// - [`Error::CompositeNestingTooDeep`] — a head arrived at `max_depth`
111    /// - [`Error::Core`] — the head is not a valid composite head, or the group failed
112    ///   validation (member count, partition coverage, overlay ordering)
113    ///
114    /// # Examples
115    ///
116    /// See the type-level example.
117    pub fn push(&mut self, tensor: StreamTensor) -> Result<Option<StreamItem>> {
118        if let LayoutDescriptor::Composite(layout) = &tensor.descriptor.layout {
119            let member_count = layout.member_count;
120            // Depth is the number of heads already open, which is what the recursive
121            // reader this replaces counted as its call depth.
122            if self.stack.len() >= self.max_depth {
123                return Err(Error::CompositeNestingTooDeep {
124                    limit: self.max_depth,
125                });
126            }
127            let validator = CompositeValidator::new(&tensor.descriptor)?;
128            self.stack.push(Open {
129                head: tensor.descriptor,
130                member_count,
131                members: Vec::new(),
132                validator,
133            });
134            return Ok(None);
135        }
136
137        self.deliver(StreamItem::Tensor(tensor))
138    }
139
140    /// Reports end of input.
141    ///
142    /// # Errors
143    ///
144    /// [`Error::TornComposite`] — the stream ended with a composite still open. The
145    /// innermost one is reported: that is the level whose members actually went missing.
146    ///
147    /// # Examples
148    ///
149    /// ```
150    /// use hurray_io::stream::{CompositeAssembler, DEFAULT_MAX_COMPOSITE_DEPTH};
151    ///
152    /// // Nothing open, so end of input is clean.
153    /// let assembler = CompositeAssembler::new(DEFAULT_MAX_COMPOSITE_DEPTH);
154    /// assert!(assembler.finish().is_ok());
155    /// ```
156    pub fn finish(&self) -> Result<()> {
157        match self.stack.last() {
158            None => Ok(()),
159            Some(open) => Err(Error::TornComposite {
160                declared: open.member_count,
161                actual: open.members.len() as u32,
162            }),
163        }
164    }
165
166    /// How many composite heads are currently open.
167    ///
168    /// `0` between top-level items.
169    ///
170    /// # Examples
171    ///
172    /// See the type-level example.
173    pub fn depth(&self) -> usize {
174        self.stack.len()
175    }
176
177    /// Hands `item` to the innermost open head, closing heads — and their parents — as each
178    /// one fills. Returns the item once no head is left to take it.
179    fn deliver(&mut self, item: StreamItem) -> Result<Option<StreamItem>> {
180        let mut item = item;
181        loop {
182            // Pop and push back rather than peek: the closing branch needs to own the
183            // head, and `CompositeValidator::finish` consumes the validator.
184            let Some(mut top) = self.stack.pop() else {
185                return Ok(Some(item));
186            };
187
188            top.validator.push_member(item.descriptor())?;
189            top.members.push(item);
190
191            if (top.members.len() as u32) < top.member_count {
192                self.stack.push(top);
193                return Ok(None);
194            }
195
196            let Open {
197                head,
198                members,
199                validator,
200                ..
201            } = top;
202            validator.finish()?;
203            // A closed composite is itself an item: a member of its parent, or the one the
204            // caller gets. Loop rather than recurse (§ 10).
205            item = StreamItem::Composite(StreamComposite { head, members });
206        }
207    }
208}