Skip to main content

solar_codegen/transform/
adce.rs

1//! Aggressive dead-code elimination for side-effect-free control regions.
2//!
3//! This pass removes control decisions whose alternatives execute only dead pure
4//! instructions and reconverge at the same phi-free target. It is deliberately
5//! conservative: memory/storage/call effects, phis at the reconvergence target,
6//! and values escaping a candidate dead block all prevent rewriting.
7
8use crate::{
9    mir::{BlockId, Function, InstId, Terminator, ValueId, utils::repair_reachability_phis},
10    pass::FunctionPass,
11    transform::DeadCodeEliminator,
12};
13use solar_data_structures::map::{FxHashMap, FxHashSet};
14
15/// Statistics for aggressive dead-code elimination.
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
17pub struct AdceStats {
18    /// Number of control terminators replaced with unconditional jumps.
19    pub control_edges_removed: usize,
20    /// Number of instructions removed by cleanup DCE after control rewrites.
21    pub instructions_removed: usize,
22}
23
24impl AdceStats {
25    /// Returns the total number of MIR edits made by this pass.
26    pub const fn total(self) -> usize {
27        self.control_edges_removed + self.instructions_removed
28    }
29}
30
31/// Aggressive dead-code eliminator.
32#[derive(Debug, Default)]
33pub struct AggressiveDeadCodeEliminator {
34    stats: AdceStats,
35}
36
37/// Function pass for aggressive dead-code elimination.
38pub struct AdcePass;
39
40impl FunctionPass for AdcePass {
41    fn name(&self) -> &str {
42        "adce"
43    }
44
45    fn run_on_function(&mut self, func: &mut Function) -> bool {
46        let changed = AggressiveDeadCodeEliminator::new().run(func).total() != 0;
47        repair_reachability_phis(func);
48        changed
49    }
50}
51
52#[derive(Debug)]
53struct AdceContext {
54    inst_results: FxHashMap<InstId, ValueId>,
55    value_uses: FxHashMap<ValueId, FxHashSet<BlockId>>,
56}
57
58/// Shared state for one transparent-target search sweep over an unmodified CFG.
59#[derive(Debug, Default)]
60struct TargetSearch {
61    /// Blocks on the current depth-first search path, used to detect cycles.
62    visiting: FxHashSet<BlockId>,
63    /// Memoized transparent target per fully explored block.
64    targets: FxHashMap<BlockId, Option<BlockId>>,
65}
66
67impl AggressiveDeadCodeEliminator {
68    /// Creates a new ADCE pass.
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Returns the statistics from the most recent run.
74    pub const fn stats(&self) -> AdceStats {
75        self.stats
76    }
77
78    /// Runs aggressive dead-code elimination once to a fixed point.
79    pub fn run(&mut self, func: &mut Function) -> AdceStats {
80        self.stats = AdceStats::default();
81
82        loop {
83            let ctx = AdceContext::new(func);
84            let rewrites = self.rewrite_dead_control(func, &ctx);
85            if rewrites == 0 {
86                break;
87            }
88            self.stats.control_edges_removed += rewrites;
89            repair_reachability_phis(func);
90        }
91
92        let removed = DeadCodeEliminator::new().run_to_fixpoint(func);
93        self.stats.instructions_removed += removed;
94        self.stats
95    }
96
97    fn rewrite_dead_control(&self, func: &mut Function, ctx: &AdceContext) -> usize {
98        let mut rewrites = Vec::new();
99        let mut search = TargetSearch::default();
100        for block_id in func.blocks.indices() {
101            let Some(term) = &func.blocks[block_id].terminator else {
102                continue;
103            };
104            if !matches!(term, Terminator::Branch { .. } | Terminator::Switch { .. }) {
105                continue;
106            }
107            let Some(target) =
108                self.common_transparent_target(func, ctx, term.successors(), &mut search)
109            else {
110                continue;
111            };
112            if target == block_id || func.block_has_phi(target) {
113                continue;
114            }
115            rewrites.push((block_id, target));
116        }
117
118        for (block_id, target) in &rewrites {
119            self.rewrite_to_jump(func, *block_id, *target);
120        }
121
122        rewrites.len()
123    }
124
125    fn common_transparent_target(
126        &self,
127        func: &Function,
128        ctx: &AdceContext,
129        successors: impl IntoIterator<Item = BlockId>,
130        search: &mut TargetSearch,
131    ) -> Option<BlockId> {
132        let mut common = None;
133        for successor in successors {
134            let target = self.transparent_target(func, ctx, successor, search)?;
135            match common {
136                Some(existing) if existing != target => return None,
137                Some(_) => {}
138                None => common = Some(target),
139            }
140        }
141        common
142    }
143
144    fn transparent_target(
145        &self,
146        func: &Function,
147        ctx: &AdceContext,
148        block_id: BlockId,
149        search: &mut TargetSearch,
150    ) -> Option<BlockId> {
151        if let Some(&target) = search.targets.get(&block_id) {
152            return target;
153        }
154        // Re-entry along the current search path means a pure cycle: there is
155        // no reconvergence target, so the cycle result is not memoized.
156        if !search.visiting.insert(block_id) {
157            return None;
158        }
159        let target = self.compute_transparent_target(func, ctx, block_id, search);
160        search.visiting.remove(&block_id);
161        search.targets.insert(block_id, target);
162        target
163    }
164
165    fn compute_transparent_target(
166        &self,
167        func: &Function,
168        ctx: &AdceContext,
169        block_id: BlockId,
170        search: &mut TargetSearch,
171    ) -> Option<BlockId> {
172        if func.block_has_phi(block_id)
173            || self.block_has_effect(func, block_id)
174            || self.block_def_escapes(func, ctx, block_id)
175        {
176            return Some(block_id);
177        }
178
179        let term = func.blocks[block_id].terminator.as_ref()?;
180        match term {
181            Terminator::Jump(target) => self.transparent_target(func, ctx, *target, search),
182            Terminator::Branch { .. } | Terminator::Switch { .. } => {
183                self.common_transparent_target(func, ctx, term.successors(), search)
184            }
185            Terminator::Return { .. }
186            | Terminator::Revert { .. }
187            | Terminator::ReturnData { .. }
188            | Terminator::Stop
189            | Terminator::SelfDestruct { .. }
190            | Terminator::Invalid => Some(block_id),
191        }
192    }
193
194    fn block_has_effect(&self, func: &Function, block_id: BlockId) -> bool {
195        func.blocks[block_id]
196            .instructions
197            .iter()
198            .any(|&inst_id| func.instructions[inst_id].kind.has_side_effects())
199    }
200
201    fn block_def_escapes(&self, func: &Function, ctx: &AdceContext, block_id: BlockId) -> bool {
202        func.blocks[block_id].instructions.iter().any(|&inst_id| {
203            let Some(&value) = ctx.inst_results.get(&inst_id) else {
204                return false;
205            };
206            ctx.value_uses
207                .get(&value)
208                .is_some_and(|uses| uses.iter().any(|&use_block| use_block != block_id))
209        })
210    }
211
212    fn rewrite_to_jump(&self, func: &mut Function, block_id: BlockId, target: BlockId) {
213        let old_successors = func.blocks[block_id]
214            .terminator
215            .as_ref()
216            .map(|term| term.successors())
217            .unwrap_or_default();
218
219        for successor in old_successors {
220            func.blocks[successor].predecessors.retain(|pred| *pred != block_id);
221        }
222        if !func.blocks[target].predecessors.contains(&block_id) {
223            func.blocks[target].predecessors.push(block_id);
224        }
225
226        func.blocks[block_id].terminator = Some(Terminator::Jump(target));
227    }
228}
229
230impl AdceContext {
231    fn new(func: &Function) -> Self {
232        let inst_results = func.inst_results();
233        let value_uses = Self::value_uses(func);
234        Self { inst_results, value_uses }
235    }
236
237    fn value_uses(func: &Function) -> FxHashMap<ValueId, FxHashSet<BlockId>> {
238        let mut uses: FxHashMap<ValueId, FxHashSet<BlockId>> = FxHashMap::default();
239        for (block_id, block) in func.blocks.iter_enumerated() {
240            for &inst_id in &block.instructions {
241                for operand in func.instructions[inst_id].kind.operands() {
242                    uses.entry(operand).or_default().insert(block_id);
243                }
244            }
245            if let Some(term) = &block.terminator {
246                for operand in term.operands() {
247                    uses.entry(operand).or_default().insert(block_id);
248                }
249            }
250        }
251        uses
252    }
253}