1use std::collections::{HashMap, HashSet};
7
8use nxpu_ir::{Arena, Expression, Function, Handle, LocalVariable, Module, Statement};
9
10use crate::Pass;
11
12#[derive(Debug)]
15pub struct DeadCodeElimination;
16
17impl Pass for DeadCodeElimination {
18 fn name(&self) -> &str {
19 "dce"
20 }
21
22 fn run(&self, module: &mut Module) -> bool {
23 let mut changed = false;
24 for (_, func) in module.functions.iter_mut() {
25 changed |= run_on_function(func);
26 changed |= remove_dead_locals(func);
27 }
28 for ep in &mut module.entry_points {
29 changed |= run_on_function(&mut ep.function);
30 changed |= remove_dead_locals(&mut ep.function);
31 }
32 changed
33 }
34}
35
36fn run_on_function(func: &mut Function) -> bool {
37 let loaded_locals = collect_loaded_locals(func);
39
40 let mut used: HashSet<Handle<Expression>> = HashSet::new();
42 collect_used_from_block(&func.body, &mut used, &loaded_locals, func);
43
44 for (_, local) in func.local_variables.iter() {
46 if let Some(init) = local.init {
47 used.insert(init);
48 }
49 }
50
51 let mut worklist: Vec<Handle<Expression>> = used.iter().copied().collect();
53 while let Some(handle) = worklist.pop() {
54 if let Some(expr) = func.expressions.try_get(handle) {
55 for operand in expression_operands(expr) {
56 if used.insert(operand) {
57 worklist.push(operand);
58 }
59 }
60 }
61 }
62
63 let dead_store_ptrs: HashSet<Handle<Expression>> = func
65 .expressions
66 .iter()
67 .filter(|(_, expr)| {
68 matches!(expr, Expression::LocalVariable(lv) if !loaded_locals.contains(lv))
69 })
70 .map(|(h, _)| h)
71 .collect();
72
73 filter_dead_in_block(&mut func.body, &used, &dead_store_ptrs)
75}
76
77#[allow(clippy::collapsible_if)] fn collect_loaded_locals(func: &Function) -> HashSet<Handle<LocalVariable>> {
80 let mut loaded = HashSet::new();
81 for (_, expr) in func.expressions.iter() {
82 if let Expression::Load { pointer } = expr {
83 if let Some(Expression::LocalVariable(lv)) = func.expressions.try_get(*pointer) {
84 loaded.insert(*lv);
85 }
86 }
87 }
88 loaded
89}
90
91fn collect_used_from_block(
92 block: &[Statement],
93 used: &mut HashSet<Handle<Expression>>,
94 loaded_locals: &HashSet<Handle<LocalVariable>>,
95 func: &Function,
96) {
97 for stmt in block {
98 match stmt {
99 Statement::Emit(_) => {}
100 Statement::Store { pointer, value } => {
101 let is_dead_store = if let Some(Expression::LocalVariable(lv)) =
103 func.expressions.try_get(*pointer)
104 {
105 !loaded_locals.contains(lv)
106 } else {
107 false
108 };
109 if !is_dead_store {
110 used.insert(*pointer);
111 used.insert(*value);
112 }
113 }
114 Statement::If {
115 condition,
116 accept,
117 reject,
118 } => {
119 used.insert(*condition);
120 collect_used_from_block(accept, used, loaded_locals, func);
121 collect_used_from_block(reject, used, loaded_locals, func);
122 }
123 Statement::Loop {
124 body,
125 continuing,
126 break_if,
127 } => {
128 collect_used_from_block(body, used, loaded_locals, func);
129 collect_used_from_block(continuing, used, loaded_locals, func);
130 if let Some(brk) = break_if {
131 used.insert(*brk);
132 }
133 }
134 Statement::Call {
135 arguments, result, ..
136 } => {
137 for arg in arguments {
139 used.insert(*arg);
140 }
141 if let Some(r) = result {
142 used.insert(*r);
143 }
144 }
145 Statement::Atomic {
146 pointer,
147 fun,
148 value,
149 result,
150 } => {
151 used.insert(*pointer);
152 used.insert(*value);
153 if let Some(r) = result {
154 used.insert(*r);
155 }
156 if let nxpu_ir::AtomicFunction::Exchange {
157 compare: Some(cmp), ..
158 } = fun
159 {
160 used.insert(*cmp);
161 }
162 }
163 Statement::Return { value } => {
164 if let Some(v) = value {
165 used.insert(*v);
166 }
167 }
168 Statement::Barrier(_) | Statement::Break | Statement::Continue => {}
169 }
170 }
171}
172
173pub(crate) fn expression_operands(expr: &Expression) -> Vec<Handle<Expression>> {
175 match expr {
176 Expression::Literal(_)
177 | Expression::FunctionArgument(_)
178 | Expression::GlobalVariable(_)
179 | Expression::LocalVariable(_)
180 | Expression::CallResult(_)
181 | Expression::AtomicResult { .. }
182 | Expression::ZeroValue(_) => vec![],
183
184 Expression::Load { pointer } => vec![*pointer],
185 Expression::Unary { expr, .. } => vec![*expr],
186 Expression::ArrayLength(e) => vec![*e],
187 Expression::Splat { value, .. } => vec![*value],
188 Expression::As { expr, .. } => vec![*expr],
189
190 Expression::Binary { left, right, .. } => vec![*left, *right],
191 Expression::Access { base, index } => vec![*base, *index],
192 Expression::AccessIndex { base, .. } => vec![*base],
193 Expression::Select {
194 condition,
195 accept,
196 reject,
197 } => vec![*condition, *accept, *reject],
198 Expression::Swizzle { vector, .. } => vec![*vector],
199
200 Expression::Compose { components, .. } => components.clone(),
201 Expression::Math {
202 arg,
203 arg1,
204 arg2,
205 arg3,
206 ..
207 } => {
208 let mut ops = vec![*arg];
209 if let Some(a) = arg1 {
210 ops.push(*a);
211 }
212 if let Some(a) = arg2 {
213 ops.push(*a);
214 }
215 if let Some(a) = arg3 {
216 ops.push(*a);
217 }
218 ops
219 }
220 }
221}
222
223fn filter_dead_in_block(
224 block: &mut Vec<Statement>,
225 used: &HashSet<Handle<Expression>>,
226 dead_store_ptrs: &HashSet<Handle<Expression>>,
227) -> bool {
228 let mut changed = false;
229 block.retain_mut(|stmt| match stmt {
230 Statement::Emit(range) => {
231 let has_used = used
232 .iter()
233 .any(|h| range.index_range().contains(&(h.index() as u32)));
234 if !has_used {
235 changed = true;
236 return false;
237 }
238 true
239 }
240 Statement::Store { pointer, .. } => {
241 if dead_store_ptrs.contains(pointer) {
243 changed = true;
244 return false;
245 }
246 true
247 }
248 Statement::If { accept, reject, .. } => {
249 changed |= filter_dead_in_block(accept, used, dead_store_ptrs);
250 changed |= filter_dead_in_block(reject, used, dead_store_ptrs);
251 true
252 }
253 Statement::Loop {
254 body, continuing, ..
255 } => {
256 changed |= filter_dead_in_block(body, used, dead_store_ptrs);
257 changed |= filter_dead_in_block(continuing, used, dead_store_ptrs);
258 true
259 }
260 _ => true,
261 });
262 changed
263}
264
265#[allow(clippy::collapsible_if)] fn remove_dead_locals(func: &mut Function) -> bool {
269 let mut referenced: HashSet<Handle<LocalVariable>> = HashSet::new();
271 for (_, expr) in func.expressions.iter() {
272 if let Expression::LocalVariable(lv) = expr {
273 referenced.insert(*lv);
274 }
275 }
276
277 let old_len = func.local_variables.len();
278 let all_referenced = func
280 .local_variables
281 .iter()
282 .all(|(h, _)| referenced.contains(&h));
283 if all_referenced {
284 return false;
285 }
286
287 let mut new_arena: Arena<LocalVariable> = Arena::new();
289 let mut remap: HashMap<Handle<LocalVariable>, Handle<LocalVariable>> = HashMap::new();
290
291 for (old_handle, local) in func.local_variables.iter() {
292 if referenced.contains(&old_handle) {
293 let new_handle = new_arena.append(local.clone());
294 remap.insert(old_handle, new_handle);
295 }
296 }
297
298 let expr_handles: Vec<Handle<Expression>> = func.expressions.iter().map(|(h, _)| h).collect();
300 for h in expr_handles {
301 if let Expression::LocalVariable(lv) = &func.expressions[h] {
302 if let Some(&new_lv) = remap.get(lv) {
303 if new_lv != *lv {
304 func.expressions[h] = Expression::LocalVariable(new_lv);
305 }
306 }
307 }
308 }
309
310 func.local_variables = new_arena;
311 func.local_variables.len() < old_len
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317 use nxpu_ir::{BinaryOp, Literal, Range, Statement};
318
319 #[test]
320 fn removes_unused_emit() {
321 let mut func = Function::new("test");
322 let lit_a = func
323 .expressions
324 .append(Expression::Literal(Literal::F32(1.0)));
325 let _lit_b = func
326 .expressions
327 .append(Expression::Literal(Literal::F32(2.0)));
328 let _lit_c = func
329 .expressions
330 .append(Expression::Literal(Literal::F32(3.0)));
331
332 func.body
334 .push(Statement::Emit(Range::from_index_range(0..3)));
335 func.body.push(Statement::Return { value: Some(lit_a) });
337 let _lit_d = func
339 .expressions
340 .append(Expression::Literal(Literal::F32(99.0)));
341 func.body
342 .push(Statement::Emit(Range::from_index_range(3..4)));
343
344 let changed = run_on_function(&mut func);
345 assert!(changed);
346 assert_eq!(func.body.len(), 2);
348 }
349
350 #[test]
351 fn keeps_transitively_used() {
352 let mut func = Function::new("test");
353 let a = func
354 .expressions
355 .append(Expression::Literal(Literal::F32(1.0)));
356 let b = func
357 .expressions
358 .append(Expression::Literal(Literal::F32(2.0)));
359 let add = func.expressions.append(Expression::Binary {
360 op: BinaryOp::Add,
361 left: a,
362 right: b,
363 });
364
365 func.body
366 .push(Statement::Emit(Range::from_index_range(0..3)));
367 func.body.push(Statement::Return { value: Some(add) });
368
369 let changed = run_on_function(&mut func);
370 assert!(!changed);
372 assert_eq!(func.body.len(), 2);
373 }
374
375 #[test]
376 fn no_change_on_empty_function() {
377 let mut func = Function::new("test");
378 let changed = run_on_function(&mut func);
379 assert!(!changed);
380 }
381
382 fn dummy_type_handle() -> Handle<nxpu_ir::Type> {
383 let mut types = nxpu_ir::UniqueArena::new();
384 types.insert(nxpu_ir::Type {
385 name: None,
386 inner: nxpu_ir::TypeInner::Scalar(nxpu_ir::Scalar::F32),
387 })
388 }
389
390 fn dummy_gv_handle() -> Handle<nxpu_ir::GlobalVariable> {
391 let mut arena = Arena::new();
392 arena.append(nxpu_ir::GlobalVariable {
393 name: None,
394 space: nxpu_ir::AddressSpace::Storage {
395 access: nxpu_ir::StorageAccess::LOAD | nxpu_ir::StorageAccess::STORE,
396 },
397 binding: None,
398 ty: dummy_type_handle(),
399 init: None,
400 layout: None,
401 })
402 }
403
404 #[test]
405 fn removes_dead_store_to_unread_local() {
406 let mut func = Function::new("test");
407 let lv = func.local_variables.append(LocalVariable {
408 name: Some("temp".into()),
409 ty: dummy_type_handle(),
410 init: None,
411 });
412 let ptr = func.expressions.append(Expression::LocalVariable(lv));
413 let val = func
414 .expressions
415 .append(Expression::Literal(Literal::F32(42.0)));
416 func.body.push(Statement::Store {
418 pointer: ptr,
419 value: val,
420 });
421 func.body.push(Statement::Return { value: None });
422
423 let changed = run_on_function(&mut func);
424 assert!(changed);
425 assert_eq!(func.body.len(), 1); }
428
429 #[test]
430 fn keeps_store_to_read_local() {
431 let mut func = Function::new("test");
432 let lv = func.local_variables.append(LocalVariable {
433 name: Some("temp".into()),
434 ty: dummy_type_handle(),
435 init: None,
436 });
437 let ptr = func.expressions.append(Expression::LocalVariable(lv));
438 let val = func
439 .expressions
440 .append(Expression::Literal(Literal::F32(42.0)));
441 let loaded = func.expressions.append(Expression::Load { pointer: ptr });
443 func.body.push(Statement::Emit(Range::from_index_range(
444 ptr.index() as u32..val.index() as u32 + 1,
445 )));
446 func.body.push(Statement::Store {
447 pointer: ptr,
448 value: val,
449 });
450 func.body.push(Statement::Return {
451 value: Some(loaded),
452 });
453
454 let changed = run_on_function(&mut func);
455 let store_count = func
456 .body
457 .iter()
458 .filter(|s| matches!(s, Statement::Store { .. }))
459 .count();
460 assert_eq!(store_count, 1);
461 let _ = changed;
462 }
463
464 #[test]
465 fn keeps_store_to_global() {
466 let mut func = Function::new("test");
467 let gv_handle = dummy_gv_handle();
468 let ptr = func
469 .expressions
470 .append(Expression::GlobalVariable(gv_handle));
471 let val = func
472 .expressions
473 .append(Expression::Literal(Literal::F32(42.0)));
474 func.body.push(Statement::Store {
475 pointer: ptr,
476 value: val,
477 });
478 func.body.push(Statement::Return { value: None });
479
480 let changed = run_on_function(&mut func);
481 let store_count = func
482 .body
483 .iter()
484 .filter(|s| matches!(s, Statement::Store { .. }))
485 .count();
486 assert_eq!(store_count, 1);
487 let _ = changed;
488 }
489
490 #[test]
491 fn removes_unreferenced_local() {
492 let mut func = Function::new("test");
493 let _lv_unused = func.local_variables.append(LocalVariable {
495 name: Some("unused".into()),
496 ty: dummy_type_handle(),
497 init: None,
498 });
499 let lv_used = func.local_variables.append(LocalVariable {
500 name: Some("used".into()),
501 ty: dummy_type_handle(),
502 init: None,
503 });
504 let _ptr = func.expressions.append(Expression::LocalVariable(lv_used));
506
507 assert_eq!(func.local_variables.len(), 2);
508 let changed = remove_dead_locals(&mut func);
509 assert!(changed);
510 assert_eq!(func.local_variables.len(), 1);
511 }
512
513 #[test]
514 fn keeps_all_referenced_locals() {
515 let mut func = Function::new("test");
516 let lv_a = func.local_variables.append(LocalVariable {
517 name: Some("a".into()),
518 ty: dummy_type_handle(),
519 init: None,
520 });
521 let lv_b = func.local_variables.append(LocalVariable {
522 name: Some("b".into()),
523 ty: dummy_type_handle(),
524 init: None,
525 });
526 let _ptr_a = func.expressions.append(Expression::LocalVariable(lv_a));
527 let _ptr_b = func.expressions.append(Expression::LocalVariable(lv_b));
528
529 assert_eq!(func.local_variables.len(), 2);
530 let changed = remove_dead_locals(&mut func);
531 assert!(!changed);
532 assert_eq!(func.local_variables.len(), 2);
533 }
534}