Skip to main content

weir/dispatch_decode/
unpack.rs

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/// Require exactly one GPU output buffer and return it as bytes.
23///
24/// # Errors
25///
26/// Returns an actionable backend-contract error when the stage returns no
27/// output or extra output buffers.
28#[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        // Wrong output count is a cold backend-contract violation.
36        return Err(only_output_mismatch(stage, output, outputs.len()));
37    }
38    Ok(outputs[0].as_slice())
39}
40
41/// Require an exact GPU output-buffer count for multi-output stages.
42#[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        // Wrong output count is a cold backend-contract violation.
51        return Err(output_count_mismatch(
52            stage,
53            expected_count,
54            output_names,
55            outputs.len(),
56        ));
57    }
58    Ok(())
59}
60
61/// Require one GPU output buffer and decode exactly `words` little-endian `u32`s into scratch.
62///
63/// This is the default ABI boundary for single-output Weir primitives: it
64/// rejects missing outputs, extra outputs, short buffers, and trailing bytes in
65/// one place before any primitive-specific interpretation happens.
66#[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/// Decode exactly `words` little-endian `u32` values from a GPU output buffer.
79///
80/// # Errors
81///
82/// Returns an actionable backend-contract error if the byte length is not
83/// exactly `words * 4`.
84#[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/// Decode one little-endian `u32` from a GPU output buffer without allocation.
93#[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/// Decode exactly `words` little-endian `u32` values into caller-owned scratch.
100///
101/// # Errors
102///
103/// Returns an actionable backend-contract error if the byte length is not
104/// exactly `words * 4`.
105#[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/// Decode all bytes as little-endian `u32` values into caller-owned scratch.
119///
120/// # Errors
121///
122/// Returns an error if the byte length is not a multiple of 4.
123#[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}