1use super::{DispatchU32Schema, U32_BYTES};
2
3#[cold]
4fn only_output_mismatch(stage: &str, output: &str, got: usize) -> String {
5 format!(
6 "{stage} dispatch returned {got} output buffers, expected exactly one {output} buffer. Fix: backend must return only {output}."
7 )
8}
9
10#[cold]
11fn output_count_mismatch(
12 stage: &str,
13 expected_count: usize,
14 output_names: &str,
15 got: usize,
16) -> String {
17 format!(
18 "{stage} dispatch returned {got} output buffers, expected exactly {expected_count}: {output_names}. Fix: backend must return the declared output ABI and no extras."
19 )
20}
21
22#[inline]
29pub(crate) fn only_output<'a>(
30 outputs: &'a [Vec<u8>],
31 stage: &str,
32 output: &str,
33) -> Result<&'a [u8], String> {
34 if outputs.len() != 1 {
35 return Err(only_output_mismatch(stage, output, outputs.len()));
37 }
38 Ok(outputs[0].as_slice())
39}
40
41#[inline]
43pub(crate) fn require_output_count(
44 outputs: &[Vec<u8>],
45 stage: &str,
46 expected_count: usize,
47 output_names: &str,
48) -> Result<(), String> {
49 if outputs.len() != expected_count {
50 return Err(output_count_mismatch(
52 stage,
53 expected_count,
54 output_names,
55 outputs.len(),
56 ));
57 }
58 Ok(())
59}
60
61#[inline]
67pub(crate) fn unpack_only_exact_u32_into(
68 outputs: &[Vec<u8>],
69 stage: &str,
70 output: &str,
71 words: usize,
72 out: &mut Vec<u32>,
73) -> Result<(), String> {
74 let bytes = only_output(outputs, stage, output)?;
75 unpack_exact_u32_into(bytes, words, stage, out)
76}
77
78#[inline]
85pub(crate) fn unpack_exact_u32(bytes: &[u8], words: usize, name: &str) -> Result<Vec<u32>, String> {
86 DispatchU32Schema::output(name, words).require_output_bytes(bytes.len())?;
87 let mut out = crate::staging_reserve::reserved_vec(words, "exact u32 decode")?;
88 vyre_primitives::wire::unpack_u32_slice_into(bytes, words, name, &mut out)?;
89 Ok(out)
90}
91
92#[inline]
94pub(crate) fn unpack_exact_u32_scalar(bytes: &[u8], name: &str) -> Result<u32, String> {
95 DispatchU32Schema::scalar_output(name).require_scalar_output_bytes(bytes.len())?;
96 vyre_primitives::wire::read_u32_le_word(bytes, 0, name)
97}
98
99#[inline]
106pub(crate) fn unpack_exact_u32_into(
107 bytes: &[u8],
108 words: usize,
109 name: &str,
110 out: &mut Vec<u32>,
111) -> Result<(), String> {
112 DispatchU32Schema::output(name, words).require_output_bytes(bytes.len())?;
113 crate::staging_reserve::reserve_vec(out, words, "exact u32 decode")?;
114 vyre_primitives::wire::unpack_u32_slice_into(bytes, words, name, out)?;
115 Ok(())
116}
117
118#[inline]
124pub fn unpack_u32_from(bytes: &[u8], out: &mut Vec<u32>) -> Result<(), String> {
125 if bytes.len() % U32_BYTES != 0 {
126 return Err(format!(
127 "unpack_u32_from: byte length {} is not a multiple of 4. Fix: backend output must contain whole u32 words.",
128 bytes.len()
129 ));
130 }
131 let words = bytes.len() / U32_BYTES;
132 unpack_exact_u32_into(bytes, words, "unpack_u32_from", out)
133}
134
135#[cfg(test)]
136mod tests {
137 use super::{
138 only_output, require_output_count, unpack_exact_u32, unpack_exact_u32_into,
139 unpack_exact_u32_scalar, unpack_only_exact_u32_into,
140 };
141 use crate::dispatch_decode::{
142 require_bitset_tail_clear, require_csr_offsets_targets, require_csr_shape,
143 require_positive_iterations,
144 };
145
146 #[test]
147 fn require_csr_shape_rejects_nonzero_first_offset() {
148 let err = require_csr_shape("test", 2, &[1, 1, 1], &[0], &[0])
149 .expect_err("nonzero first CSR offset must be rejected");
150 assert!(
151 err.contains("edge_offsets[0]"),
152 "unexpected diagnostic: {err}"
153 );
154 }
155
156 #[test]
157 fn require_csr_shape_rejects_zero_node_graph_with_edges() {
158 let err = require_csr_shape("test", 0, &[1], &[0], &[0])
159 .expect_err("zero-node CSR graph must not declare edges");
160 assert!(
161 err.contains("edge_offsets[0]"),
162 "unexpected diagnostic: {err}"
163 );
164 }
165
166 #[test]
167 fn require_csr_offsets_targets_rejects_target_outside_domain() {
168 let err = require_csr_offsets_targets("test", 2, &[0, 1, 1], &[2])
169 .expect_err("CSR target outside node_count must be rejected");
170 assert!(
171 err.contains("targets node 2"),
172 "unexpected diagnostic: {err}"
173 );
174 }
175
176 #[test]
177 fn require_bitset_tail_clear_rejects_out_of_domain_bits() {
178 let err = require_bitset_tail_clear("test", &[0b1000], 3)
179 .expect_err("tail bit beyond three-bit domain must be rejected");
180 assert!(
181 err.contains("outside the declared domain"),
182 "unexpected diagnostic: {err}"
183 );
184 }
185
186 #[test]
187 fn require_bitset_tail_clear_accepts_full_words_and_empty_domains() {
188 require_bitset_tail_clear("test", &[u32::MAX], 32)
189 .expect("full u32 domain has no tail bits");
190 require_bitset_tail_clear("test", &[], 0)
191 .expect("empty domain with empty bitset has no tail bits");
192 require_bitset_tail_clear("test", &[0], 0)
193 .expect("empty domain with backend-minimum zero word has no set tail bits");
194 }
195
196 #[test]
197 fn require_bitset_tail_clear_rejects_set_bits_in_empty_domain() {
198 let err = require_bitset_tail_clear("test", &[1], 0)
199 .expect_err("nonzero word in empty domain must be rejected");
200 assert!(err.contains("empty domain"), "unexpected diagnostic: {err}");
201 }
202
203 #[test]
204 fn require_positive_iterations_rejects_zero_budget() {
205 let err = require_positive_iterations("test", 0)
206 .expect_err("zero fixpoint budget must be rejected");
207 assert!(
208 err.contains("max_iterations=0"),
209 "unexpected diagnostic: {err}"
210 );
211 }
212
213 #[test]
214 fn only_output_rejects_missing_and_extra_backend_outputs() {
215 let missing: Vec<Vec<u8>> = Vec::new();
216 let err = only_output(&missing, "decode-test", "frontier")
217 .expect_err("missing backend output must be rejected");
218 assert!(
219 err.contains("returned 0 output buffers")
220 && err.contains("expected exactly one frontier buffer")
221 && err.contains("Fix:"),
222 "unexpected missing-output diagnostic: {err}"
223 );
224
225 let extra = vec![vec![0; 4], vec![0; 4]];
226 let err = only_output(&extra, "decode-test", "frontier")
227 .expect_err("extra backend output must be rejected");
228 assert!(
229 err.contains("returned 2 output buffers")
230 && err.contains("expected exactly one frontier buffer")
231 && err.contains("Fix:"),
232 "unexpected extra-output diagnostic: {err}"
233 );
234 }
235
236 #[test]
237 fn require_output_count_rejects_missing_and_extra_multi_outputs() {
238 let one = vec![vec![0; 4]];
239 let err = require_output_count(&one, "csr-prepare", 4, "row_ptr,col_idx,len,flags")
240 .expect_err("missing multi-output buffers must be rejected");
241 assert!(
242 err.contains("returned 1 output buffers")
243 && err.contains("expected exactly 4")
244 && err.contains("no extras"),
245 "unexpected missing multi-output diagnostic: {err}"
246 );
247
248 let five = vec![vec![0; 4]; 5];
249 let err = require_output_count(&five, "csr-prepare", 4, "row_ptr,col_idx,len,flags")
250 .expect_err("extra multi-output buffers must be rejected");
251 assert!(
252 err.contains("returned 5 output buffers")
253 && err.contains("expected exactly 4")
254 && err.contains("no extras"),
255 "unexpected extra multi-output diagnostic: {err}"
256 );
257 }
258
259 #[test]
260 fn exact_u32_decode_rejects_short_and_trailing_bytes() {
261 let short = unpack_exact_u32(&[1, 2, 3], 1, "decode-test")
262 .expect_err("short u32 output must be rejected");
263 assert!(
264 short.contains("got 3 bytes")
265 && short.contains("expected exactly 4")
266 && short.contains("no trailing bytes"),
267 "unexpected short-output diagnostic: {short}"
268 );
269
270 let trailing = unpack_exact_u32(&[1, 2, 3, 4, 5], 1, "decode-test")
271 .expect_err("trailing backend bytes must be rejected");
272 assert!(
273 trailing.contains("got 5 bytes")
274 && trailing.contains("expected exactly 4")
275 && trailing.contains("no trailing bytes"),
276 "unexpected trailing-output diagnostic: {trailing}"
277 );
278 }
279
280 #[test]
281 fn exact_u32_decode_into_reuses_stable_scratch_capacity() {
282 let mut out = Vec::with_capacity(8);
283 out.extend([99, 100]);
284 let capacity = out.capacity();
285
286 unpack_exact_u32_into(&[1, 0, 0, 0, 2, 0, 0, 0], 2, "decode-test", &mut out)
287 .expect("exact output should decode into caller-owned scratch");
288
289 assert_eq!(out, vec![1, 2]);
290 assert_eq!(
291 out.capacity(),
292 capacity,
293 "exact decode into stable scratch must not discard reusable allocation"
294 );
295 }
296
297 #[test]
298 fn scalar_and_single_output_decode_share_exact_abi_contract() {
299 let scalar = unpack_exact_u32_scalar(&[7, 0, 0, 0], "scalar-test")
300 .expect("four-byte scalar output should decode");
301 assert_eq!(scalar, 7);
302 let err = unpack_exact_u32_scalar(&[7, 0, 0, 0, 0], "scalar-test")
303 .expect_err("scalar output with trailing bytes must be rejected");
304 assert!(
305 err.contains("expected exactly 4") && err.contains("no trailing bytes"),
306 "unexpected scalar diagnostic: {err}"
307 );
308
309 let outputs = vec![vec![3, 0, 0, 0, 4, 0, 0, 0]];
310 let mut decoded = Vec::new();
311 unpack_only_exact_u32_into(&outputs, "single-output-test", "frontier", 2, &mut decoded)
312 .expect("single output with exact words should decode");
313 assert_eq!(decoded, vec![3, 4]);
314 }
315}
316
317#[cfg(test)]
318mod extra_tests {
319 use super::{
320 only_output, require_output_count, unpack_exact_u32, unpack_exact_u32_scalar,
321 unpack_only_exact_u32_into,
322 };
323 use crate::dispatch_decode::{
324 require_bitset_tail_clear, require_csr_offsets_targets, require_csr_shape,
325 require_positive_iterations,
326 };
327
328 #[test]
329 fn require_csr_offsets_targets_rejects_target_equal_to_node_count() {
330 let err = require_csr_offsets_targets("test", 2, &[0, 1, 1], &[2])
331 .expect_err("target equal to node_count must be rejected");
332 assert!(err.contains("targets node 2"), "{err}");
333 }
334
335 #[test]
336 fn require_csr_shape_rejects_missing_mask() {
337 let err = require_csr_shape("test", 2, &[0, 1, 1], &[0], &[])
338 .expect_err("missing mask must be rejected");
339 assert!(err.contains("edge_kind_mask"), "{err}");
340 }
341
342 #[test]
343 fn require_positive_iterations_rejects_zero() {
344 let err = require_positive_iterations("test", 0).expect_err("zero budget must be rejected");
345 assert!(err.contains("max_iterations=0"), "{err}");
346 }
347
348 #[test]
349 fn require_bitset_tail_clear_rejects_set_bits_in_tail() {
350 let err = require_bitset_tail_clear("test", &[0b10000], 3)
351 .expect_err("set tail bits must be rejected");
352 assert!(err.contains("outside the declared domain"), "{err}");
353 }
354
355 #[test]
356 fn unpack_exact_u32_rejects_empty_buffer_for_one_word() {
357 let err =
358 unpack_exact_u32(&[], 1, "decode-test").expect_err("empty buffer must be rejected");
359 assert!(err.contains("got 0 bytes"), "{err}");
360 }
361
362 #[test]
363 fn unpack_exact_u32_scalar_rejects_three_bytes() {
364 let err = unpack_exact_u32_scalar(&[1, 2, 3], "scalar-test")
365 .expect_err("three bytes must be rejected");
366 assert!(err.contains("expected exactly 4"), "{err}");
367 }
368
369 #[test]
370 fn only_output_rejects_empty() {
371 let err = only_output(&[], "test", "frontier").expect_err("empty outputs must be rejected");
372 assert!(err.contains("returned 0 output buffers"), "{err}");
373 }
374
375 #[test]
376 fn require_output_count_rejects_zero_for_one() {
377 let err =
378 require_output_count(&[], "test", 1, "out").expect_err("zero outputs must be rejected");
379 assert!(err.contains("returned 0 output buffers"), "{err}");
380 }
381
382 #[test]
383 fn unpack_only_exact_u32_into_rejects_extra_outputs() {
384 let outputs = vec![vec![1, 0, 0, 0], vec![2, 0, 0, 0]];
385 let mut decoded = Vec::new();
386 let err = unpack_only_exact_u32_into(&outputs, "test", "frontier", 1, &mut decoded)
387 .expect_err("extra outputs must be rejected");
388 assert!(err.contains("returned 2 output buffers"), "{err}");
389 }
390}