Skip to main content

solar_codegen/transform/
dce.rs

1//! Dead Code Elimination (DCE) optimization pass.
2//!
3//! This pass removes MIR instructions whose results are never used and have no side effects.
4
5use crate::{
6    analysis::CfgInfo,
7    mir::{BlockId, Function, InstId, Terminator, Value, ValueId, utils::repair_reachability_phis},
8    pass::FunctionPass,
9};
10use solar_data_structures::map::{FxHashMap, FxHashSet};
11
12/// Dead Code Elimination pass.
13///
14/// Removes instructions that:
15/// 1. Have a result that is never used
16/// 2. Have no side effects
17/// 3. Are in unreachable blocks
18/// 4. Are instructions after a terminator (unreachable code)
19///
20/// Side-effect instructions (SSTORE, MSTORE, CALL, LOG, etc.) are always kept.
21#[derive(Debug, Default)]
22pub struct DeadCodeEliminator {
23    /// Number of instructions eliminated in the last run.
24    pub eliminated_count: usize,
25    /// Number of unreachable blocks removed.
26    pub blocks_removed: usize,
27    /// Number of unused parameters detected.
28    pub unused_params: usize,
29}
30
31/// Statistics for a DCE run.
32#[derive(Debug, Default, Clone, Copy)]
33pub struct DceStats {
34    /// Instructions eliminated (unused results).
35    pub dead_instructions: usize,
36    /// Unreachable blocks removed.
37    pub unreachable_blocks: usize,
38    /// Unused function parameters detected.
39    pub unused_parameters: usize,
40}
41
42/// Function pass for dead code elimination.
43pub struct DcePass;
44
45impl FunctionPass for DcePass {
46    fn name(&self) -> &str {
47        "dce"
48    }
49
50    fn run_on_function(&mut self, func: &mut Function) -> bool {
51        let changed = DeadCodeEliminator::new().run_to_fixpoint(func) != 0;
52        repair_reachability_phis(func);
53        changed
54    }
55}
56
57impl DceStats {
58    /// Returns total eliminations.
59    pub fn total(&self) -> usize {
60        self.dead_instructions + self.unreachable_blocks + self.unused_parameters
61    }
62}
63
64impl DeadCodeEliminator {
65    /// Creates a new dead code eliminator.
66    pub fn new() -> Self {
67        Self::default()
68    }
69
70    /// Runs dead code elimination on a function.
71    /// Returns the number of instructions eliminated.
72    pub fn run(&mut self, func: &mut Function) -> usize {
73        self.eliminated_count = 0;
74        self.blocks_removed = 0;
75        self.unused_params = 0;
76
77        // Phase 1: Remove unreachable blocks
78        self.eliminate_unreachable_blocks(func);
79
80        // Phase 2: Find and count unused parameters
81        self.unused_params = self.find_unused_parameters(func).len();
82
83        // Phase 3: Precompute InstId → ValueId map (O(V) once, replaces
84        // the O(V)-per-instruction linear scan in find_result_value).
85        let inst_to_value: FxHashMap<InstId, ValueId> = func
86            .values
87            .iter_enumerated()
88            .filter_map(
89                |(vid, val)| {
90                    if let Value::Inst(iid) = val { Some((*iid, vid)) } else { None }
91                },
92            )
93            .collect();
94
95        // Phase 4: Find all used values
96        let used_values = self.collect_used_values(func);
97
98        // Phase 5: Find dead instructions
99        let dead_instructions = self.find_dead_instructions(func, &used_values, &inst_to_value);
100
101        // Remove dead instructions from blocks
102        for (block_id, inst_id) in &dead_instructions {
103            let block = func.block_mut(*block_id);
104            block.instructions.retain(|&id| id != *inst_id);
105            self.eliminated_count += 1;
106        }
107
108        self.eliminated_count
109    }
110
111    /// Runs dead code elimination with full statistics.
112    pub fn run_with_stats(&mut self, func: &mut Function) -> DceStats {
113        let eliminated = self.run(func);
114        DceStats {
115            dead_instructions: eliminated,
116            unreachable_blocks: self.blocks_removed,
117            unused_parameters: self.unused_params,
118        }
119    }
120
121    /// Runs dead code elimination iteratively until no more changes.
122    pub fn run_to_fixpoint(&mut self, func: &mut Function) -> usize {
123        let mut total_eliminated = 0;
124        loop {
125            let eliminated = self.run(func);
126            if eliminated == 0 {
127                break;
128            }
129            total_eliminated += eliminated;
130        }
131        total_eliminated
132    }
133
134    /// Eliminates unreachable blocks using CFG reachability analysis.
135    fn eliminate_unreachable_blocks(&mut self, func: &mut Function) {
136        let cfg = CfgInfo::new(func);
137        let reachable = cfg.reachable();
138
139        // Collect unreachable block IDs
140        let unreachable: Vec<BlockId> = func
141            .blocks
142            .iter_enumerated()
143            .filter_map(|(id, _)| if !reachable.contains(&id) { Some(id) } else { None })
144            .collect();
145
146        self.blocks_removed = unreachable.len();
147
148        // Clear unreachable blocks (we can't actually remove from IndexVec,
149        // but we can clear their contents to prevent codegen)
150        for block_id in &unreachable {
151            let block = func.block_mut(*block_id);
152            block.instructions.clear();
153            block.terminator = Some(Terminator::Invalid);
154            block.predecessors.clear();
155        }
156    }
157
158    /// Finds unused function parameters.
159    /// Returns the indices of parameters that are never used.
160    pub fn find_unused_parameters(&self, func: &Function) -> Vec<u32> {
161        // Collect all used argument indices
162        let mut used_args = FxHashSet::default();
163
164        // Collect from all values used in instructions
165        for (_, block) in func.blocks.iter_enumerated() {
166            for &inst_id in &block.instructions {
167                let inst = &func.instructions[inst_id];
168                for val_id in inst.kind.operands() {
169                    if let Value::Arg { index, .. } = &func.values[val_id] {
170                        used_args.insert(*index);
171                    }
172                }
173            }
174
175            // Collect from terminators
176            if let Some(ref term) = block.terminator {
177                for val_id in self.get_terminator_operands(term) {
178                    if let Value::Arg { index, .. } = &func.values[val_id] {
179                        used_args.insert(*index);
180                    }
181                }
182            }
183        }
184
185        // Find unused parameter indices
186        (0..func.params.len() as u32).filter(|idx| !used_args.contains(idx)).collect()
187    }
188
189    /// Gets operands from a terminator.
190    fn get_terminator_operands(&self, term: &Terminator) -> Vec<ValueId> {
191        match term {
192            Terminator::Jump(_) | Terminator::Stop | Terminator::Invalid => vec![],
193            Terminator::Branch { condition, .. } => vec![*condition],
194            Terminator::Switch { value, cases, .. } => {
195                let mut ops = vec![*value];
196                for (case_val, _) in cases {
197                    ops.push(*case_val);
198                }
199                ops
200            }
201            Terminator::Return { values } => values.to_vec(),
202            Terminator::Revert { offset, size } | Terminator::ReturnData { offset, size } => {
203                vec![*offset, *size]
204            }
205            Terminator::SelfDestruct { recipient } => vec![*recipient],
206        }
207    }
208
209    /// Collects all values that are used (appear in instructions or terminators).
210    fn collect_used_values(&self, func: &Function) -> FxHashSet<ValueId> {
211        let mut used = FxHashSet::default();
212
213        // Add values used in terminators
214        for (_, block) in func.blocks.iter_enumerated() {
215            if let Some(term) = &block.terminator {
216                self.collect_terminator_uses(term, &mut used);
217            }
218        }
219
220        // Add values used as operands in instructions
221        for (_, block) in func.blocks.iter_enumerated() {
222            for &inst_id in &block.instructions {
223                let inst = &func.instructions[inst_id];
224                for val in inst.kind.operands() {
225                    used.insert(val);
226                }
227            }
228        }
229
230        used
231    }
232
233    /// Collects values used by a terminator.
234    fn collect_terminator_uses(&self, term: &Terminator, used: &mut FxHashSet<ValueId>) {
235        match term {
236            Terminator::Jump(_) | Terminator::Stop | Terminator::Invalid => {}
237            Terminator::Branch { condition, .. } => {
238                used.insert(*condition);
239            }
240            Terminator::Switch { value, cases, .. } => {
241                used.insert(*value);
242                for (case_val, _) in cases {
243                    used.insert(*case_val);
244                }
245            }
246            Terminator::Return { values } => {
247                for val in values {
248                    used.insert(*val);
249                }
250            }
251            Terminator::Revert { offset, size } | Terminator::ReturnData { offset, size } => {
252                used.insert(*offset);
253                used.insert(*size);
254            }
255            Terminator::SelfDestruct { recipient } => {
256                used.insert(*recipient);
257            }
258        }
259    }
260
261    /// Finds instructions that are dead (unused result, no side effects).
262    fn find_dead_instructions(
263        &self,
264        func: &Function,
265        used_values: &FxHashSet<ValueId>,
266        inst_to_value: &FxHashMap<InstId, ValueId>,
267    ) -> Vec<(BlockId, InstId)> {
268        let mut dead = Vec::new();
269
270        for (block_id, block) in func.blocks.iter_enumerated() {
271            for &inst_id in &block.instructions {
272                let inst = &func.instructions[inst_id];
273
274                // Instructions with side effects are always kept.
275                if inst.kind.has_side_effects() {
276                    continue;
277                }
278
279                // O(1) lookup via precomputed map (was O(V) linear scan).
280                if let Some(&result) = inst_to_value.get(&inst_id)
281                    && !used_values.contains(&result)
282                {
283                    dead.push((block_id, inst_id));
284                }
285            }
286        }
287
288        dead
289    }
290}