Skip to main content

weir/dispatch_decode/
schema.rs

1//! Shared dispatch-decode ABI schema for Weir GPU boundaries.
2//!
3//! Pack, unpack, validation, fuzz, and release evidence all need the same
4//! byte-width and exact-length contract. This module owns that schema while
5//! the sibling modules keep doing one kind of work each.
6
7use crate::error_format::ErrorFormatScratch;
8
9pub(crate) const U32_BYTES: usize = std::mem::size_of::<u32>();
10pub(crate) const U32_BITS_PER_WORD: u32 = u32::BITS;
11
12#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
13pub(crate) enum DispatchDecodeDirection {
14    Input,
15    Output,
16}
17
18#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
19pub(crate) struct DispatchU32Schema<'a> {
20    pub name: &'a str,
21    pub words: usize,
22    pub direction: DispatchDecodeDirection,
23}
24
25impl<'a> DispatchU32Schema<'a> {
26    #[inline]
27    #[must_use]
28    pub(crate) const fn input(name: &'a str, words: usize) -> Self {
29        Self {
30            name,
31            words,
32            direction: DispatchDecodeDirection::Input,
33        }
34    }
35
36    #[inline]
37    #[must_use]
38    pub(crate) const fn output(name: &'a str, words: usize) -> Self {
39        Self {
40            name,
41            words,
42            direction: DispatchDecodeDirection::Output,
43        }
44    }
45
46    #[inline]
47    #[must_use]
48    pub(crate) const fn scalar_output(name: &'a str) -> Self {
49        Self::output(name, 1)
50    }
51
52    #[inline]
53    pub(crate) fn required_bytes(self) -> Result<usize, String> {
54        byte_len_for_u32_words(self.words, self.name)
55    }
56
57    #[inline]
58    pub(crate) fn require_input_words(self, got: usize) -> Result<(), String> {
59        if got != self.words {
60            return Err(input_word_count_mismatch(self.name, got, self.words));
61        }
62        Ok(())
63    }
64
65    #[inline]
66    pub(crate) fn require_output_bytes(self, got: usize) -> Result<(), String> {
67        let required = self.required_bytes()?;
68        if got != required {
69            return Err(output_byte_len_mismatch(self.name, got, required));
70        }
71        Ok(())
72    }
73
74    #[inline]
75    pub(crate) fn require_scalar_output_bytes(self, got: usize) -> Result<(), String> {
76        if got != U32_BYTES {
77            return Err(scalar_byte_len_mismatch(self.name, got));
78        }
79        Ok(())
80    }
81}
82
83#[inline]
84pub(crate) fn byte_len_for_u32_words(words: usize, caller: &str) -> Result<usize, String> {
85    words.checked_mul(U32_BYTES).ok_or_else(|| {
86        // Byte-length overflow is a cold malformed-dimension path.
87        byte_len_overflow(caller, words)
88    })
89}
90
91#[cold]
92fn input_word_count_mismatch(name: &str, got: usize, expected: usize) -> String {
93    format!(
94        "{name} input has {got} u32 words, expected exactly {expected}. Fix: pass the declared semantic input width; Weir GPU wrappers never silently pad or truncate inputs."
95    )
96}
97
98#[cold]
99fn output_byte_len_mismatch(name: &str, got: usize, required: usize) -> String {
100    let mut scratch = ErrorFormatScratch::default();
101    crate::error_format::write_dispatch_byte_len_mismatch(&mut scratch, name, got, required)
102}
103
104#[cold]
105fn scalar_byte_len_mismatch(name: &str, got: usize) -> String {
106    let mut scratch = ErrorFormatScratch::default();
107    crate::error_format::write_scalar_byte_len_mismatch(&mut scratch, name, got)
108}
109
110#[cold]
111fn byte_len_overflow(caller: &str, words: usize) -> String {
112    format!(
113        "{caller} word count {words} overflows host byte indexing. Fix: split the Weir GPU dispatch buffer before packing or decoding."
114    )
115}
116
117#[cfg(test)]
118mod tests {
119    use super::{byte_len_for_u32_words, DispatchU32Schema, U32_BITS_PER_WORD, U32_BYTES};
120
121    #[test]
122    fn u32_schema_reports_canonical_widths() {
123        assert_eq!(U32_BYTES, 4);
124        assert_eq!(U32_BITS_PER_WORD, 32);
125        assert_eq!(
126            DispatchU32Schema::output("schema-test", 3)
127                .required_bytes()
128                .expect("three u32 words should fit bytes"),
129            12
130        );
131    }
132
133    #[test]
134    fn u32_schema_rejects_malformed_input_word_count() {
135        let err = DispatchU32Schema::input("schema input", 2)
136            .require_input_words(3)
137            .expect_err("wrong input word count must fail closed");
138        assert!(
139            err.contains("schema input input has 3 u32 words")
140                && err.contains("expected exactly 2")
141                && err.contains("never silently pad or truncate inputs"),
142            "unexpected diagnostic: {err}"
143        );
144    }
145
146    #[test]
147    fn u32_schema_rejects_malformed_output_byte_count() {
148        let err = DispatchU32Schema::output("schema output", 2)
149            .require_output_bytes(7)
150            .expect_err("wrong output byte count must fail closed");
151        assert!(
152            err.contains("schema output output has malformed byte length")
153                && err.contains("got 7 bytes")
154                && err.contains("expected exactly 8")
155                && err.contains("no trailing bytes"),
156            "unexpected diagnostic: {err}"
157        );
158    }
159
160    #[test]
161    fn u32_schema_rejects_malformed_scalar_byte_count() {
162        let err = DispatchU32Schema::scalar_output("schema scalar")
163            .require_scalar_output_bytes(8)
164            .expect_err("wrong scalar byte count must fail closed");
165        assert!(
166            err.contains("schema scalar output has malformed byte length")
167                && err.contains("expected exactly 4")
168                && err.contains("scalar u32 output size"),
169            "unexpected diagnostic: {err}"
170        );
171    }
172
173    #[test]
174    fn u32_schema_checked_byte_length_overflow_is_actionable() {
175        let err = byte_len_for_u32_words(usize::MAX, "schema overflow")
176            .expect_err("overflowing byte length must fail closed");
177        assert!(
178            err.contains("schema overflow word count")
179                && err.contains("overflows host byte indexing")
180                && err.contains("split the Weir GPU dispatch buffer"),
181            "unexpected diagnostic: {err}"
182        );
183    }
184}