//! Dead Code Elimination (pure instruction DCE) //! //! Extracted from the monolithic optimizer to enable modular pass composition. use crate::mir::{MirFunction, MirModule, ValueId}; use std::collections::HashSet; /// Eliminate dead code (unused results of pure instructions) across the module. /// Returns the number of eliminated instructions. pub fn eliminate_dead_code(module: &mut MirModule) -> usize { let mut eliminated_total = 0usize; for (_func_name, func) in &mut module.functions { eliminated_total += eliminate_dead_code_in_function(func); } eliminated_total } fn eliminate_dead_code_in_function(function: &mut MirFunction) -> usize { // Collect values that must be kept (used results + effects) let mut used_values: HashSet = HashSet::new(); // Mark values used by side-effecting instructions and terminators for (_bid, block) in &function.blocks { for instruction in &block.instructions { let has_dst = instruction.dst_value().is_some(); if !instruction.effects().is_pure() || !has_dst { if let Some(dst) = instruction.dst_value() { used_values.insert(dst); } for u in instruction.used_values() { used_values.insert(u); } } } if let Some(term) = &block.terminator { for u in term.used_values() { used_values.insert(u); } } for edge in block.out_edges() { if let Some(args) = edge.args { for u in args.values { used_values.insert(u); } } } } // Backward propagation: if a value is used, mark its operands as used let mut changed = true; while changed { changed = false; for (_bid, block) in &function.blocks { for instruction in &block.instructions { if let Some(dst) = instruction.dst_value() { if used_values.contains(&dst) { for u in instruction.used_values() { if used_values.insert(u) { changed = true; } } } } } } } // Remove unused pure instructions let mut eliminated = 0usize; let dce_trace = std::env::var("NYASH_DCE_TRACE").ok().as_deref() == Some("1"); for (bbid, block) in &mut function.blocks { let insts = std::mem::take(&mut block.instructions); let spans = std::mem::take(&mut block.instruction_spans); let mut kept_insts = Vec::with_capacity(insts.len()); let mut kept_spans = Vec::with_capacity(spans.len()); for (inst, span) in insts.into_iter().zip(spans.into_iter()) { let mut keep = true; if inst.effects().is_pure() { if let Some(dst) = inst.dst_value() { if !used_values.contains(&dst) { if dce_trace { eprintln!( "[dce] Eliminating unused pure instruction in bb{}: %{} = {:?}", bbid.0, dst.0, inst ); } eliminated += 1; keep = false; } } } if keep { kept_insts.push(inst); kept_spans.push(span); } } block.instructions = kept_insts; block.instruction_spans = kept_spans; } if eliminated > 0 { function.update_cfg(); } eliminated } #[cfg(test)] mod tests { use super::*; use crate::ast::Span; use crate::mir::{BasicBlockId, ConstValue, EffectMask, FunctionSignature, MirInstruction, MirType}; #[test] fn test_dce_keeps_edge_args_values() { let mut module = MirModule::new("dce_test".to_string()); let sig = FunctionSignature { name: "test/0".to_string(), params: vec![], return_type: MirType::Void, effects: EffectMask::PURE, }; let mut func = MirFunction::new(sig, BasicBlockId(0)); let v1 = ValueId(1); let v2 = ValueId(2); let v_dead = ValueId(3); let bb1 = BasicBlockId(1); { let bb0 = func.blocks.get_mut(&BasicBlockId(0)).unwrap(); bb0.instructions.push(MirInstruction::Const { dst: v1, value: ConstValue::Integer(123), }); bb0.instruction_spans.push(Span::unknown()); bb0.instructions.push(MirInstruction::Copy { dst: v2, src: v1 }); bb0.instruction_spans.push(Span::unknown()); // This pure instruction should be eliminated. bb0.instructions.push(MirInstruction::Const { dst: v_dead, value: ConstValue::Integer(999), }); bb0.instruction_spans.push(Span::unknown()); // SSOT: edge args are semantic use-sites (ExitLine, continuation args). bb0.set_jump_with_edge_args( bb1, Some(crate::mir::EdgeArgs { layout: crate::mir::join_ir::lowering::inline_boundary::JumpArgsLayout::CarriersOnly, values: vec![v2], }), ); } let mut exit_block = crate::mir::BasicBlock::new(bb1); exit_block.set_terminator(MirInstruction::Return { value: None }); func.add_block(exit_block); module.add_function(func); let eliminated = eliminate_dead_code(&mut module); assert_eq!(eliminated, 1); let func = module.get_function("test/0").unwrap(); let bb0 = func.blocks.get(&BasicBlockId(0)).unwrap(); // Contract: spans must stay aligned with instructions. assert_eq!(bb0.instructions.len(), bb0.instruction_spans.len()); // Contract: values that appear only in edge args must be kept (and their deps). assert!(bb0 .instructions .iter() .any(|inst| matches!(inst, MirInstruction::Copy { dst, .. } if *dst == v2))); assert!(bb0 .instructions .iter() .any(|inst| matches!(inst, MirInstruction::Const { dst, .. } if *dst == v1))); // The unused const must be eliminated. assert!(!bb0 .instructions .iter() .any(|inst| matches!(inst, MirInstruction::Const { dst, .. } if *dst == v_dead))); } }