Skip to main content

weir/dispatch_decode/
validate.rs

1use super::U32_BITS_PER_WORD;
2
3/// Require a bitset input to contain exactly the declared number of words.
4///
5/// # Errors
6///
7/// Returns an actionable ABI error when a production GPU wrapper receives a
8/// short or trailing bitset. Silent truncation/padding changes dataflow facts.
9#[inline]
10pub(crate) fn require_bitset_words(
11    stage: &str,
12    values: &[u32],
13    words: usize,
14) -> Result<(), String> {
15    if values.len() != words {
16        return Err(format!(
17            "{stage} bitset has {} u32 words, expected exactly {words}. Fix: pass the declared semantic bitset width; Weir GPU wrappers never silently pad or truncate facts.",
18            values.len()
19        ));
20    }
21    Ok(())
22}
23
24/// Number of `u32` words needed to represent `bits` domain facts.
25#[inline]
26#[must_use]
27pub(crate) const fn bitset_words(bits: u32) -> u32 {
28    let full_words = bits / U32_BITS_PER_WORD;
29    if bits % U32_BITS_PER_WORD == 0 {
30        full_words
31    } else {
32        full_words + 1
33    }
34}
35
36/// Checked host capacity for a semantic bitset width.
37#[inline]
38pub(crate) fn bitset_word_capacity(stage: &str, bits: u32) -> Result<usize, String> {
39    usize::try_from(bitset_words(bits)).map_err(|source| {
40        format!(
41            "{stage} bitset word count cannot fit usize: {source}. Fix: shard the dataflow domain before host scratch allocation."
42        )
43    })
44}
45
46/// Convert a u32 GPU/IR count to a host index with a uniform diagnostic.
47#[inline]
48pub(crate) fn u32_to_usize(value: u32, label: &str) -> Result<usize, String> {
49    usize::try_from(value).map_err(|source| {
50        format!(
51            "{label} cannot fit host usize: {source}. Fix: shard the dataflow problem before GPU dispatch."
52        )
53    })
54}
55
56/// Require unused tail bits in the final bitset word to be zero.
57///
58/// # Errors
59///
60/// Returns an actionable ABI error when a caller sets bits beyond the declared
61/// node/fact domain. Production GPU wrappers must reject those bits instead of
62/// masking them silently because they represent out-of-domain dataflow facts.
63#[inline]
64pub(crate) fn require_bitset_tail_clear(
65    stage: &str,
66    values: &[u32],
67    domain_bits: u32,
68) -> Result<(), String> {
69    if domain_bits == 0 {
70        if values.iter().any(|word| *word != 0) {
71            return Err(format!(
72                "{stage} bitset sets bits outside the declared empty domain: values must be empty or all-zero. Fix: pass zero semantic words for empty domains before GPU dispatch."
73            ));
74        }
75        return Ok(());
76    }
77    let tail = domain_bits % U32_BITS_PER_WORD;
78    if tail == 0 || values.is_empty() {
79        return Ok(());
80    }
81    let allowed = (1u32 << tail) - 1;
82    let actual = *values.last().unwrap_or(&0);
83    if actual & !allowed != 0 {
84        return Err(format!(
85            "{stage} seed bitset sets bits outside the declared domain of {domain_bits} bits: last word {actual:#010x}, allowed mask {allowed:#010x}. Fix: clear out-of-domain tail bits before GPU dispatch."
86        ));
87    }
88    Ok(())
89}
90
91/// Require at least one fixpoint iteration budget before GPU setup.
92///
93/// # Errors
94///
95/// Returns an actionable boundary error for zero-iteration closure calls.
96/// Production wrappers should fail before packing buffers or constructing
97/// Programs when the caller supplied an impossible convergence budget.
98#[inline]
99pub(crate) fn require_positive_iterations(stage: &str, max_iterations: u32) -> Result<(), String> {
100    if max_iterations == 0 {
101        return Err(format!(
102            "{stage} received max_iterations=0. Fix: pass a positive fixpoint iteration budget; Weir GPU wrappers reject impossible convergence budgets before dispatch setup."
103        ));
104    }
105    Ok(())
106}
107
108/// Require a forward CSR graph to match its declared node and edge counts.
109///
110/// # Errors
111///
112/// Returns an actionable ABI error for missing sentinel offsets, non-monotonic
113/// offsets, mismatched edge arrays, or targets outside `node_count`.
114#[inline]
115pub(crate) fn require_csr_shape(
116    stage: &str,
117    node_count: u32,
118    edge_offsets: &[u32],
119    edge_targets: &[u32],
120    edge_kind_mask: &[u32],
121) -> Result<(), String> {
122    require_csr_offsets_targets(stage, node_count, edge_offsets, edge_targets)?;
123    let nodes = usize::try_from(node_count).map_err(|_| {
124        format!("{stage} node_count={node_count} cannot fit usize. Fix: shard the graph before dispatch.")
125    })?;
126    let edge_count = u32_to_usize(edge_offsets[nodes], "final CSR offset")?;
127    if edge_kind_mask.len() != edge_count {
128        return Err(format!(
129            "{stage} declares {edge_count} edges but edge_kind_mask has {}. Fix: pass edge-kind arrays matching the final offset exactly.",
130            edge_kind_mask.len()
131        ));
132    }
133    Ok(())
134}
135
136/// Require generated CSR offsets and targets to match the declared graph domain.
137///
138/// # Errors
139///
140/// Returns an actionable ABI error for missing sentinel offsets, non-monotonic
141/// offsets, mismatched target arrays, orphan prefix edges, or targets outside
142/// `node_count`.
143#[inline]
144pub(crate) fn require_csr_offsets_targets(
145    stage: &str,
146    node_count: u32,
147    edge_offsets: &[u32],
148    edge_targets: &[u32],
149) -> Result<(), String> {
150    let nodes = usize::try_from(node_count).map_err(|_| {
151        format!("{stage} node_count={node_count} cannot fit usize. Fix: shard the graph before dispatch.")
152    })?;
153    let expected_offsets = nodes.checked_add(1).ok_or_else(|| {
154        format!("{stage} node_count={node_count} overflows when adding CSR sentinel offset.")
155    })?;
156    if edge_offsets.len() != expected_offsets {
157        return Err(format!(
158            "{stage} edge_offsets has {} entries, expected exactly node_count + 1 = {expected_offsets}. Fix: pass a complete CSR offset table.",
159            edge_offsets.len()
160        ));
161    }
162    let edge_count = u32_to_usize(edge_offsets[nodes], "final CSR offset")?;
163    if edge_targets.len() != edge_count {
164        return Err(format!(
165            "{stage} declares {edge_count} edges but edge_targets has {}. Fix: pass edge arrays matching the final offset exactly.",
166            edge_targets.len()
167        ));
168    }
169    if edge_offsets[0] != 0 {
170        return Err(format!(
171            "{stage} edge_offsets[0] is {}, expected 0. Fix: rebuild CSR offsets from zero; Weir GPU wrappers reject orphan prefix edges.",
172            edge_offsets[0]
173        ));
174    }
175    for node in 0..nodes {
176        let start = u32_to_usize(edge_offsets[node], "CSR row start offset")?;
177        let end = u32_to_usize(edge_offsets[node + 1], "CSR row end offset")?;
178        if start > end {
179            return Err(format!(
180                "{stage} edge_offsets is not monotonic at node {node}: start={start}, end={end}. Fix: rebuild CSR offsets."
181            ));
182        }
183        if end > edge_count {
184            return Err(format!(
185                "{stage} edge_offsets[{}]={end} exceeds declared edge count {edge_count}. Fix: rebuild CSR offsets.",
186                node + 1
187            ));
188        }
189        for (edge, &target) in edge_targets.iter().enumerate().take(end).skip(start) {
190            if target >= node_count {
191                return Err(format!(
192                    "{stage} edge {edge} targets node {target}, but node_count is {node_count}. Fix: reject or remap malformed graph edges before dispatch."
193                ));
194            }
195        }
196    }
197    Ok(())
198}