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}