vyre_driver/program_walks/
outputs.rs1use std::ops::Range;
4use std::sync::Arc;
5
6use vyre_foundation::ir::{BufferDecl, DataType, Program};
7
8use crate::backend::{BackendError, DispatchConfig};
9
10pub fn enforce_actual_output_budget(
16 config: &DispatchConfig,
17 outputs: &[Vec<u8>],
18) -> Result<(), BackendError> {
19 let Some(limit) = config.max_output_bytes else {
20 return Ok(());
21 };
22 let actual = outputs.iter().try_fold(0usize, |sum, output| {
23 sum.checked_add(output.len()).ok_or_else(|| {
24 BackendError::new(
25 "actual readback size overflows usize. Fix: split the Program output before dispatch.",
26 )
27 })
28 })?;
29 if actual > limit {
30 return Err(BackendError::new(format!(
31 "actual readback size {actual} exceeds DispatchConfig.max_output_bytes {limit}. Fix: narrow BufferDecl::output_byte_range or raise max_output_bytes."
32 )));
33 }
34 Ok(())
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub struct OutputLayout {
40 pub full_size: usize,
42 pub read_size: usize,
44 pub copy_offset: usize,
46 pub copy_size: usize,
48 pub trim_start: usize,
50}
51
52#[derive(Clone, Debug)]
54pub struct OutputBindingLayout {
55 pub binding: u32,
57 pub name: Arc<str>,
59 pub layout: OutputLayout,
61 pub word_count: usize,
63}
64
65pub fn output_layout_from_program(program: &Program) -> Result<OutputLayout, BackendError> {
72 let Some(&index) = program.output_buffer_indices().first() else {
73 return Err(BackendError::new(
74 "program has no output buffer. Fix: declare exactly one output buffer in the vyre Program.",
75 ));
76 };
77 let output = program.buffers().get(index as usize).ok_or_else(|| {
78 BackendError::new(format!(
79 "output buffer index {index} is out of bounds. Fix: rebuild the Program so writable buffer metadata stays consistent."
80 ))
81 })?;
82 output_binding_layout(output).map(|output| output.layout)
83}
84
85pub fn output_binding_layouts(program: &Program) -> Result<Vec<OutputBindingLayout>, BackendError> {
92 let mut outputs = reserved_output_layout_slots(program.output_buffer_indices().len())?;
93 output_binding_layouts_into(program, &mut outputs)?;
94 Ok(outputs)
95}
96
97pub fn output_binding_layouts_into(
104 program: &Program,
105 outputs: &mut Vec<OutputBindingLayout>,
106) -> Result<(), BackendError> {
107 outputs.clear();
108 reserve_output_layout_slots(outputs, program.output_buffer_indices().len())?;
109 for &index in program.output_buffer_indices() {
110 let output = program.buffers().get(index as usize).ok_or_else(|| {
111 BackendError::new(
112 format!(
113 "output buffer index {index} is out of bounds. Fix: rebuild the Program so writable buffer metadata stays consistent."
114 ),
115 )
116 })?;
117 outputs.push(output_binding_layout(output)?);
118 }
119 if outputs.is_empty() {
120 return Err(BackendError::new(
121 "program has no output buffer. Fix: declare at least one writable buffer in the vyre Program.",
122 ));
123 }
124 Ok(())
125}
126
127pub fn output_binding_layout(output: &BufferDecl) -> Result<OutputBindingLayout, BackendError> {
133 output_binding_layout_parts(
134 output.binding(),
135 &output.name,
136 &output.element,
137 output.count(),
138 output.output_byte_range(),
139 )
140}
141
142pub fn output_binding_layout_parts(
155 binding: u32,
156 name: &Arc<str>,
157 element: &DataType,
158 count: u32,
159 output_byte_range: Option<Range<usize>>,
160) -> Result<OutputBindingLayout, BackendError> {
161 let count = usize::try_from(count).map_err(|_| {
162 BackendError::new(
163 "program output element count exceeds usize. Fix: split the dispatch into smaller output buffers.",
164 )
165 })?;
166 element.validate_layout().map_err(|error| {
167 BackendError::new(format!(
168 "program output `{name}` has malformed data-type layout metadata: {error}"
169 ))
170 })?;
171 let full_size = element.packed_size_bytes(count).map_err(|error| {
172 BackendError::new(format!(
173 "program output `{name}` byte size could not be computed: {error}"
174 ))
175 })?.ok_or_else(|| {
176 BackendError::new(
177 "program output element type has no fixed packed byte size. Fix: validate the Program and flatten variable-size outputs before backend pipeline compilation.",
178 )
179 })?;
180 let layout = output_layout(output_byte_range, full_size)?;
181 let word_count = full_size
182 .checked_add(3)
183 .and_then(|n| n.checked_div(4))
184 .ok_or_else(|| {
185 BackendError::new(
186 "program output word count overflows usize. Fix: split the dispatch into smaller output buffers.",
187 )
188 })?
189 .max(1);
190 Ok(OutputBindingLayout {
191 binding,
192 name: Arc::clone(name),
193 layout,
194 word_count,
195 })
196}
197
198fn output_layout(
199 output_byte_range: Option<Range<usize>>,
200 full_size: usize,
201) -> Result<OutputLayout, BackendError> {
202 let range = output_byte_range.unwrap_or(0..full_size);
203 if range.start > range.end || range.end > full_size {
204 return Err(BackendError::new(format!(
205 "output byte range {:?} is outside output buffer size {full_size}. Fix: declare a range within the output buffer.",
206 range
207 )));
208 }
209 let copy_offset = range.start & !3;
210 let copy_end = align_up_to_u32_word(range.end)?.min(full_size.max(4));
211 let copy_size = copy_end.checked_sub(copy_offset).ok_or_else(|| {
212 BackendError::new(format!(
213 "aligned output copy range underflowed: copy_end={copy_end}, copy_offset={copy_offset}. Fix: declare output_byte_range inside the output buffer."
214 ))
215 })?.max(4);
216 Ok(OutputLayout {
217 full_size,
218 read_size: range.end - range.start,
219 copy_offset,
220 copy_size,
221 trim_start: range.start - copy_offset,
222 })
223}
224
225fn reserve_output_layout_slots(
226 outputs: &mut Vec<OutputBindingLayout>,
227 capacity: usize,
228) -> Result<(), BackendError> {
229 crate::allocation::try_reserve_vec_to_capacity(outputs, capacity).map_err(|error| {
230 BackendError::new(format!(
231 "output binding layout planning could not reserve {capacity} output slot(s): {error}. Fix: split the Program output set or reuse caller-owned output layout scratch."
232 ))
233 })
234}
235
236fn reserved_output_layout_slots(capacity: usize) -> Result<Vec<OutputBindingLayout>, BackendError> {
237 let mut outputs = Vec::new();
238 reserve_output_layout_slots(&mut outputs, capacity)?;
239 Ok(outputs)
240}
241
242fn align_up_to_u32_word(value: usize) -> Result<usize, BackendError> {
243 value.checked_add(3).map(|end| end & !3).ok_or_else(|| {
244 BackendError::new(format!(
245 "aligned output copy end overflows usize for byte offset {value}. Fix: declare a smaller output_byte_range before backend readback planning."
246 ))
247 })
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253 use vyre_foundation::ir::{BufferDecl, DataType};
254
255 #[test]
256 fn output_layout_planning_uses_fallible_modular_reservation_and_alignment() {
257 let source = include_str!("outputs.rs");
258 let production = source
259 .split("#[cfg(test)]")
260 .next()
261 .expect("Fix: output-layout source must contain production section before tests");
262
263 assert!(
264 production.contains("fn reserve_output_layout_slots")
265 && production.contains("fn align_up_to_u32_word")
266 && production.contains("try_reserve_vec_to_capacity"),
267 "Fix: output layout planning must keep reservation and alignment as modular fallible helpers."
268 );
269 assert!(
270 !production.contains("Vec::with_capacity")
271 && !production.contains(".reserve(program.output_buffer_indices().len())")
272 && !production.contains(".next_multiple_of(4)")
273 && !production.contains(".unwrap_or(full_size)"),
274 "Fix: output layout planning must not allocate infallibly or hide overflow in release paths."
275 );
276 }
277
278 #[test]
279 fn output_layout_alignment_rejects_usize_overflow() {
280 let error =
281 align_up_to_u32_word(usize::MAX).expect_err("max byte offset cannot align upward");
282 assert!(
283 error.to_string().contains("Fix:"),
284 "alignment overflow must be actionable: {error}"
285 );
286 }
287
288 #[test]
289 fn output_layout_uses_packed_size_for_subbyte_elements() {
290 let output = BufferDecl::output("packed_i4", 0, DataType::I4).with_count(3);
291 let layout = output_binding_layout(&output)
292 .expect("Fix: packed I4 output layout should use packed byte sizing");
293
294 assert_eq!(layout.layout.full_size, 2);
295 assert_eq!(layout.layout.read_size, 2);
296 assert_eq!(layout.word_count, 1);
297 }
298
299 #[test]
300 fn output_layout_rejects_malformed_data_type_layouts() {
301 let output = BufferDecl::output(
302 "bad_bsr",
303 0,
304 DataType::SparseBsr {
305 element: Box::new(DataType::F32),
306 block_rows: 0,
307 block_cols: 4,
308 },
309 )
310 .with_count(1);
311
312 let error = output_binding_layout(&output)
313 .expect_err("zero-height BSR blocks must not enter output planning");
314 assert!(
315 error
316 .to_string()
317 .contains("SparseBsr block_rows must be > 0"),
318 "Fix: malformed output data-type layout diagnostics must remain actionable: {error}"
319 );
320 }
321}
322
323pub fn element_size_bytes(data_type: &DataType) -> Result<usize, BackendError> {
329 data_type.size_bytes().ok_or_else(|| {
330 BackendError::new(
331 "output buffer element type has no fixed scalar element size. Fix: validate the Program and flatten variable-size outputs before backend pipeline compilation.",
332 )
333 })
334}