weir/dispatch_decode/
validate.rs1use super::U32_BITS_PER_WORD;
2
3#[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#[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#[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#[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#[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#[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#[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#[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}