1use std::fmt;
7
8use nxpu_ir::{Expression, Module};
9
10use crate::Pass;
11
12#[derive(Debug, Clone)]
14pub struct ValidationWarning {
15 pub message: String,
16}
17
18impl fmt::Display for ValidationWarning {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 f.write_str(&self.message)
21 }
22}
23
24#[derive(Debug)]
26pub struct IrValidation;
27
28impl Pass for IrValidation {
29 fn name(&self) -> &str {
30 "ir-validation"
31 }
32
33 fn run(&self, module: &mut Module) -> bool {
34 for w in collect_warnings(module) {
35 log::warn!("{}", w.message);
36 }
37 false
38 }
39}
40
41pub fn collect_warnings(module: &Module) -> Vec<ValidationWarning> {
46 let mut warnings = Vec::new();
47 let type_count = module.types.len();
48
49 validate_expression_arena(
51 &module.global_expressions,
52 "global_expressions",
53 &mut warnings,
54 );
55
56 for (handle, gv) in module.global_variables.iter() {
58 if gv.ty.index() >= type_count {
59 warnings.push(ValidationWarning {
60 message: format!(
61 "global variable {:?} (handle {:?}) references out-of-bounds type handle {:?}",
62 gv.name, handle, gv.ty
63 ),
64 });
65 }
66 }
67
68 for ep in &module.entry_points {
70 for (i, &size) in ep.workgroup_size.iter().enumerate() {
72 if size == 0 {
73 warnings.push(ValidationWarning {
74 message: format!("entry point '{}' has workgroup_size[{}] = 0", ep.name, i),
75 });
76 }
77 }
78
79 validate_expression_arena(
80 &ep.function.expressions,
81 &format!("ep '{}'", ep.name),
82 &mut warnings,
83 );
84
85 validate_local_and_arg_types(
87 &ep.function,
88 type_count,
89 &format!("ep '{}'", ep.name),
90 &mut warnings,
91 );
92 }
93
94 for (handle, func) in module.functions.iter() {
96 let ctx = format!(
97 "function '{}' ({:?})",
98 func.name.as_deref().unwrap_or("<unnamed>"),
99 handle
100 );
101 validate_expression_arena(&func.expressions, &ctx, &mut warnings);
102 validate_local_and_arg_types(func, type_count, &ctx, &mut warnings);
103 }
104
105 warnings
106}
107
108fn validate_expression_arena(
109 arena: &nxpu_ir::Arena<Expression>,
110 context: &str,
111 warnings: &mut Vec<ValidationWarning>,
112) {
113 let arena_len = arena.len();
114
115 for (handle, expr) in arena.iter() {
116 let operands = crate::dce::expression_operands(expr);
117 for operand in operands {
118 if operand.index() >= arena_len {
119 warnings.push(ValidationWarning {
120 message: format!(
121 "{}: expression {:?} references out-of-bounds operand {:?} (arena size {})",
122 context, handle, operand, arena_len,
123 ),
124 });
125 }
126 }
127 }
128}
129
130fn validate_local_and_arg_types(
131 func: &nxpu_ir::Function,
132 type_count: usize,
133 context: &str,
134 warnings: &mut Vec<ValidationWarning>,
135) {
136 for (lv_handle, local) in func.local_variables.iter() {
137 if local.ty.index() >= type_count {
138 warnings.push(ValidationWarning {
139 message: format!(
140 "{}: local variable {:?} ({:?}) references out-of-bounds type {:?}",
141 context, local.name, lv_handle, local.ty
142 ),
143 });
144 }
145 }
146 for (i, arg) in func.arguments.iter().enumerate() {
147 if arg.ty.index() >= type_count {
148 warnings.push(ValidationWarning {
149 message: format!(
150 "{}: argument {} ({:?}) references out-of-bounds type {:?}",
151 context, i, arg.name, arg.ty
152 ),
153 });
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use nxpu_ir::{EntryPoint, Function, Literal};
162
163 #[test]
164 fn valid_module_no_warnings() {
165 let mut module = Module::default();
166 module.entry_points.push(EntryPoint {
167 name: "main".into(),
168 workgroup_size: [64, 1, 1],
169 function: Function::new("main"),
170 });
171
172 let warnings = collect_warnings(&module);
173 assert_eq!(warnings.len(), 0);
174 }
175
176 #[test]
177 fn zero_workgroup_size_detected() {
178 let mut module = Module::default();
179 module.entry_points.push(EntryPoint {
180 name: "bad_ep".into(),
181 workgroup_size: [0, 1, 1],
182 function: Function::new("bad_ep"),
183 });
184
185 let warnings = collect_warnings(&module);
186 assert_ne!(warnings.len(), 0);
187 assert!(
188 warnings[0].message.contains("workgroup_size[0] = 0"),
189 "unexpected message: {}",
190 warnings[0].message
191 );
192 }
193
194 #[test]
195 fn valid_expressions_no_warnings() {
196 let mut module = Module::default();
197 let mut func = Function::new("main");
198 let _lit = func
199 .expressions
200 .append(Expression::Literal(Literal::F32(1.0)));
201 module.entry_points.push(EntryPoint {
202 name: "main".into(),
203 workgroup_size: [1, 1, 1],
204 function: func,
205 });
206
207 let warnings = collect_warnings(&module);
208 assert_eq!(warnings.len(), 0);
209 }
210
211 #[test]
212 fn pass_returns_false() {
213 let mut module = Module::default();
214 let pass = IrValidation;
215 assert!(!pass.run(&mut module));
216 }
217
218 #[test]
219 fn zero_workgroup_size_all_dimensions() {
220 let mut module = Module::default();
221 module.entry_points.push(EntryPoint {
222 name: "bad".into(),
223 workgroup_size: [0, 0, 0],
224 function: Function::new("bad"),
225 });
226
227 let warnings = collect_warnings(&module);
228 assert_eq!(warnings.len(), 3);
229 assert!(warnings[0].message.contains("workgroup_size[0] = 0"));
230 assert!(warnings[1].message.contains("workgroup_size[1] = 0"));
231 assert!(warnings[2].message.contains("workgroup_size[2] = 0"));
232 }
233
234 #[test]
235 fn zero_workgroup_size_y_only() {
236 let mut module = Module::default();
237 module.entry_points.push(EntryPoint {
238 name: "ep".into(),
239 workgroup_size: [64, 0, 1],
240 function: Function::new("ep"),
241 });
242
243 let warnings = collect_warnings(&module);
244 assert_eq!(warnings.len(), 1);
245 assert!(warnings[0].message.contains("workgroup_size[1] = 0"));
246 }
247
248 #[test]
249 fn multiple_entry_points_partial_invalid() {
250 let mut module = Module::default();
251 module.entry_points.push(EntryPoint {
252 name: "good".into(),
253 workgroup_size: [64, 1, 1],
254 function: Function::new("good"),
255 });
256 module.entry_points.push(EntryPoint {
257 name: "bad".into(),
258 workgroup_size: [0, 1, 1],
259 function: Function::new("bad"),
260 });
261
262 let warnings = collect_warnings(&module);
263 assert_eq!(warnings.len(), 1);
264 assert!(warnings[0].message.contains("bad"));
265 }
266
267 #[test]
268 fn warning_display() {
269 let w = ValidationWarning {
270 message: "test message".into(),
271 };
272 assert_eq!(format!("{w}"), "test message");
273 }
274
275 #[test]
276 fn pass_name() {
277 let pass = IrValidation;
278 assert_eq!(pass.name(), "ir-validation");
279 }
280
281 #[test]
282 fn valid_expression_with_binary_op() {
283 let mut module = Module::default();
284 let mut func = Function::new("main");
285 let a = func
286 .expressions
287 .append(Expression::Literal(Literal::F32(1.0)));
288 let b = func
289 .expressions
290 .append(Expression::Literal(Literal::F32(2.0)));
291 func.expressions.append(Expression::Binary {
292 op: nxpu_ir::BinaryOp::Add,
293 left: a,
294 right: b,
295 });
296 module.entry_points.push(EntryPoint {
297 name: "main".into(),
298 workgroup_size: [1, 1, 1],
299 function: func,
300 });
301
302 assert!(collect_warnings(&module).is_empty());
303 }
304
305 #[test]
306 fn valid_helper_function_no_warnings() {
307 let mut module = Module::default();
308 let mut func = Function::new("helper");
309 func.expressions
310 .append(Expression::Literal(Literal::F32(42.0)));
311 module.functions.append(func);
312
313 assert!(collect_warnings(&module).is_empty());
314 }
315}