Skip to main content

nxpu_opt/
validation.rs

1//! IR validation pass.
2//!
3//! Checks structural invariants of the IR module and collects warnings.
4//! This pass never modifies the module.
5
6use std::fmt;
7
8use nxpu_ir::{Expression, Module};
9
10use crate::Pass;
11
12/// A validation warning describing a structural issue in the IR.
13#[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/// Validates IR structural invariants. Returns `false` (never modifies the module).
25#[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
41/// Collect all validation warnings for a module without logging.
42///
43/// This is the primary validation API — usable in tests and debug builds
44/// without needing a logger configured.
45pub fn collect_warnings(module: &Module) -> Vec<ValidationWarning> {
46    let mut warnings = Vec::new();
47    let type_count = module.types.len();
48
49    // Validate expression operand bounds in global expressions.
50    validate_expression_arena(
51        &module.global_expressions,
52        "global_expressions",
53        &mut warnings,
54    );
55
56    // Validate global variable type handles.
57    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    // Validate entry points.
69    for ep in &module.entry_points {
70        // Workgroup sizes must be > 0.
71        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 variable type handles.
86        validate_local_and_arg_types(
87            &ep.function,
88            type_count,
89            &format!("ep '{}'", ep.name),
90            &mut warnings,
91        );
92    }
93
94    // Validate helper functions.
95    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}