Skip to main content

solar_codegen/transform/
jump_threading.rs

1//! Jump Threading optimization pass.
2//!
3//! This pass eliminates unnecessary jumps by threading through blocks that only contain
4//! an unconditional jump. Each eliminated JUMP instruction saves 8 gas.
5//!
6//! ## Optimizations performed:
7//!
8//! 1. **JUMP to JUMP threading**: If block A jumps to block B, and B only contains an unconditional
9//!    jump to C, rewrite A to jump directly to C.
10//!
11//! 2. **JUMPI to JUMP threading**: If a conditional branch targets a block that only contains an
12//!    unconditional jump, thread through to the final target.
13//!
14//! 3. **Empty block elimination**: Blocks containing only a JUMPDEST and JUMP are eliminated by
15//!    updating all references to point to the final target.
16
17use crate::{
18    mir::{
19        BlockId, Function, InstKind, Terminator, Value, ValueId, utils::repair_reachability_phis,
20    },
21    pass::FunctionPass,
22};
23use solar_data_structures::map::{FxHashMap, FxHashSet};
24
25/// Statistics from jump threading optimization.
26#[derive(Debug, Default, Clone)]
27pub struct JumpThreadingStats {
28    /// Number of unconditional jumps threaded.
29    pub jumps_threaded: usize,
30    /// Number of conditional branch targets threaded.
31    pub branches_threaded: usize,
32    /// Number of switch case targets threaded.
33    pub switches_threaded: usize,
34    /// Estimated gas saved (8 gas per eliminated jump).
35    pub gas_saved: usize,
36}
37
38impl JumpThreadingStats {
39    /// Returns the total number of threading operations performed.
40    #[must_use]
41    pub fn total_threaded(&self) -> usize {
42        self.jumps_threaded + self.branches_threaded + self.switches_threaded
43    }
44}
45
46/// Jump threading optimization pass.
47#[derive(Debug, Default)]
48pub struct JumpThreader {
49    /// Statistics from the last run.
50    pub stats: JumpThreadingStats,
51}
52
53/// Function pass for jump threading.
54pub struct JumpThreadingPass;
55
56impl FunctionPass for JumpThreadingPass {
57    fn name(&self) -> &str {
58        "jump-threading"
59    }
60
61    fn run_on_function(&mut self, func: &mut Function) -> bool {
62        JumpThreader::new().run_to_fixpoint(func).total_threaded() != 0
63    }
64}
65
66impl JumpThreader {
67    /// Creates a new jump threader.
68    #[must_use]
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Runs jump threading on a function.
74    /// Returns the number of threading operations performed.
75    pub fn run(&mut self, func: &mut Function) -> usize {
76        self.stats = JumpThreadingStats::default();
77        let mut changed = 0;
78
79        // Build a map of blocks that are "forwarders" - blocks that only jump unconditionally
80        let forwarders = self.find_forwarder_blocks(func);
81
82        if !forwarders.is_empty() {
83            // Resolve the final target for each forwarder (following chains)
84            let final_targets = self.resolve_final_targets(&forwarders);
85
86            // Update all terminators to use final targets
87            self.thread_jumps(func, &final_targets);
88            changed += self.stats.total_threaded();
89        }
90
91        changed += self.thread_phi_constant_edges(func);
92
93        if changed == 0 {
94            return 0;
95        }
96
97        // Update predecessor/successor information
98        self.update_cfg_edges(func);
99        repair_reachability_phis(func);
100
101        changed
102    }
103
104    /// Runs jump threading iteratively until no more changes.
105    pub fn run_to_fixpoint(&mut self, func: &mut Function) -> JumpThreadingStats {
106        let mut total_stats = JumpThreadingStats::default();
107        loop {
108            let changed = self.run(func);
109            if changed == 0 {
110                break;
111            }
112            total_stats.jumps_threaded += self.stats.jumps_threaded;
113            total_stats.branches_threaded += self.stats.branches_threaded;
114            total_stats.switches_threaded += self.stats.switches_threaded;
115            total_stats.gas_saved += self.stats.gas_saved;
116        }
117        total_stats
118    }
119
120    /// Finds blocks that only contain an unconditional jump (forwarder blocks).
121    fn find_forwarder_blocks(&self, func: &Function) -> FxHashMap<BlockId, BlockId> {
122        let mut forwarders = FxHashMap::default();
123
124        for (block_id, block) in func.blocks.iter_enumerated() {
125            // Skip the entry block
126            if block_id == func.entry_block {
127                continue;
128            }
129
130            // Only fully empty blocks are forwarders: bypassing a block that
131            // contains a phi would sever the phi's incoming edges.
132            if !block.instructions.is_empty() {
133                continue;
134            }
135
136            // Check if terminator is an unconditional jump
137            if let Some(Terminator::Jump(target)) = &block.terminator {
138                // Don't thread self-loops
139                if *target != block_id {
140                    forwarders.insert(block_id, *target);
141                }
142            }
143        }
144
145        forwarders
146    }
147
148    /// Resolves chains of forwarders to find the final target.
149    fn resolve_final_targets(
150        &self,
151        forwarders: &FxHashMap<BlockId, BlockId>,
152    ) -> FxHashMap<BlockId, BlockId> {
153        let mut final_targets = FxHashMap::default();
154
155        for &block_id in forwarders.keys() {
156            let final_target = self.follow_chain(block_id, forwarders);
157            if final_target != block_id {
158                final_targets.insert(block_id, final_target);
159            }
160        }
161
162        final_targets
163    }
164
165    /// Follows a chain of forwarders to find the final non-forwarder target.
166    fn follow_chain(&self, start: BlockId, forwarders: &FxHashMap<BlockId, BlockId>) -> BlockId {
167        let mut visited = FxHashSet::default();
168        let mut current = start;
169
170        while let Some(&next) = forwarders.get(&current) {
171            if !visited.insert(current) {
172                break;
173            }
174            current = next;
175        }
176
177        current
178    }
179
180    /// Updates all terminators to use the final targets.
181    fn thread_jumps(&mut self, func: &mut Function, final_targets: &FxHashMap<BlockId, BlockId>) {
182        let block_ids: Vec<_> = func.blocks.indices().collect();
183        for block_id in block_ids {
184            let Some(mut term) = func.blocks[block_id].terminator.clone() else {
185                continue;
186            };
187            self.thread_terminator(func, &mut term, final_targets);
188            func.blocks[block_id].terminator = Some(term);
189        }
190    }
191
192    /// Threads a single terminator's targets.
193    fn thread_terminator(
194        &mut self,
195        func: &Function,
196        term: &mut Terminator,
197        final_targets: &FxHashMap<BlockId, BlockId>,
198    ) {
199        match term {
200            Terminator::Jump(target) => {
201                if let Some(final_target) = Self::threaded_target(func, *target, final_targets) {
202                    *target = final_target;
203                    self.stats.jumps_threaded += 1;
204                    self.stats.gas_saved += 8;
205                }
206            }
207
208            Terminator::Branch { then_block, else_block, .. } => {
209                let mut changed = false;
210                if let Some(final_target) = Self::threaded_target(func, *then_block, final_targets)
211                {
212                    *then_block = final_target;
213                    changed = true;
214                }
215                if let Some(final_target) = Self::threaded_target(func, *else_block, final_targets)
216                {
217                    *else_block = final_target;
218                    changed = true;
219                }
220                if changed {
221                    self.stats.branches_threaded += 1;
222                    self.stats.gas_saved += 8;
223                }
224            }
225
226            Terminator::Switch { default, cases, .. } => {
227                let mut changed = false;
228                if let Some(final_target) = Self::threaded_target(func, *default, final_targets) {
229                    *default = final_target;
230                    changed = true;
231                }
232                for (_, target) in cases.iter_mut() {
233                    if let Some(final_target) = Self::threaded_target(func, *target, final_targets)
234                    {
235                        *target = final_target;
236                        changed = true;
237                    }
238                }
239                if changed {
240                    self.stats.switches_threaded += 1;
241                    self.stats.gas_saved += 8;
242                }
243            }
244
245            Terminator::Return { .. }
246            | Terminator::Revert { .. }
247            | Terminator::ReturnData { .. }
248            | Terminator::Stop
249            | Terminator::SelfDestruct { .. }
250            | Terminator::Invalid => {}
251        }
252    }
253
254    fn threaded_target(
255        func: &Function,
256        target: BlockId,
257        final_targets: &FxHashMap<BlockId, BlockId>,
258    ) -> Option<BlockId> {
259        let final_target = final_targets.get(&target).copied()?;
260        (!func.block_has_phi(final_target)).then_some(final_target)
261    }
262
263    fn block_phi_results_have_external_uses(func: &Function, block_id: BlockId) -> bool {
264        let phi_results = func.block_phi_results(block_id);
265        if phi_results.is_empty() {
266            return false;
267        }
268
269        for (other_block, block) in func.blocks.iter_enumerated() {
270            if other_block != block_id {
271                for &inst_id in &block.instructions {
272                    if func.instructions[inst_id]
273                        .kind
274                        .operands()
275                        .iter()
276                        .any(|operand| phi_results.contains(operand))
277                    {
278                        return true;
279                    }
280                }
281            }
282
283            if other_block == block_id {
284                continue;
285            }
286            if let Some(term) = &block.terminator
287                && term.operands().iter().any(|operand| phi_results.contains(operand))
288            {
289                return true;
290            }
291        }
292
293        false
294    }
295
296    fn thread_phi_constant_edges(&mut self, func: &mut Function) -> usize {
297        let mut rewrites = Vec::new();
298        let block_ids: Vec<_> = func.blocks.indices().collect();
299
300        for block_id in block_ids {
301            if !func.block_has_only_phis(block_id) {
302                continue;
303            }
304            if Self::block_phi_results_have_external_uses(func, block_id) {
305                continue;
306            }
307
308            let Some(term) = &func.blocks[block_id].terminator else {
309                continue;
310            };
311            let predecessors = func.blocks[block_id].predecessors.clone();
312            if predecessors.is_empty() {
313                continue;
314            }
315
316            for pred in predecessors {
317                if pred == block_id || Self::successor_count(func, pred, block_id) != 1 {
318                    continue;
319                }
320                let Some(target) = self.phi_constant_target_for_pred(func, block_id, term, pred)
321                else {
322                    continue;
323                };
324                if target == block_id || func.block_has_phi(target) {
325                    continue;
326                }
327                rewrites.push((pred, block_id, target));
328            }
329        }
330
331        let mut threaded = 0;
332        for (pred, old_target, new_target) in rewrites {
333            if Self::replace_successor(func, pred, old_target, new_target) {
334                threaded += 1;
335            }
336        }
337
338        if threaded != 0 {
339            self.stats.branches_threaded += threaded;
340            self.stats.gas_saved += threaded * 8;
341        }
342
343        threaded
344    }
345
346    fn phi_constant_target_for_pred(
347        &self,
348        func: &Function,
349        block_id: BlockId,
350        term: &Terminator,
351        pred: BlockId,
352    ) -> Option<BlockId> {
353        match term {
354            Terminator::Branch { condition, then_block, else_block } => {
355                let incoming = Self::incoming_value_for_pred(func, block_id, *condition, pred)?;
356                let condition = func.value_u256(incoming)?;
357                Some(if condition.is_zero() { *else_block } else { *then_block })
358            }
359            Terminator::Switch { value, default, cases } => {
360                let incoming = Self::incoming_value_for_pred(func, block_id, *value, pred)?;
361                let value = func.value_u256(incoming)?;
362                for (case, target) in cases {
363                    if func.value_u256(*case)? == value {
364                        return Some(*target);
365                    }
366                }
367                Some(*default)
368            }
369            _ => None,
370        }
371    }
372
373    fn incoming_value_for_pred(
374        func: &Function,
375        block_id: BlockId,
376        value: ValueId,
377        pred: BlockId,
378    ) -> Option<ValueId> {
379        let Value::Inst(inst_id) = func.value(value) else {
380            return Some(value);
381        };
382        if !func.blocks[block_id].instructions.contains(inst_id) {
383            return None;
384        }
385        let InstKind::Phi(incoming) = &func.instructions[*inst_id].kind else {
386            return None;
387        };
388        incoming.iter().find_map(|(incoming_block, incoming_value)| {
389            (*incoming_block == pred).then_some(*incoming_value)
390        })
391    }
392
393    fn successor_count(func: &Function, pred: BlockId, target: BlockId) -> usize {
394        func.blocks[pred]
395            .terminator
396            .as_ref()
397            .map(|term| term.successors().into_iter().filter(|&succ| succ == target).count())
398            .unwrap_or_default()
399    }
400
401    fn replace_successor(
402        func: &mut Function,
403        pred: BlockId,
404        old_target: BlockId,
405        new_target: BlockId,
406    ) -> bool {
407        let Some(term) = &mut func.blocks[pred].terminator else {
408            return false;
409        };
410        match term {
411            Terminator::Jump(target) => {
412                if *target == old_target {
413                    *target = new_target;
414                    true
415                } else {
416                    false
417                }
418            }
419            Terminator::Branch { then_block, else_block, .. } => {
420                let mut changed = false;
421                if *then_block == old_target {
422                    *then_block = new_target;
423                    changed = true;
424                }
425                if *else_block == old_target {
426                    *else_block = new_target;
427                    changed = true;
428                }
429                changed
430            }
431            Terminator::Switch { default, cases, .. } => {
432                let mut changed = false;
433                if *default == old_target {
434                    *default = new_target;
435                    changed = true;
436                }
437                for (_, target) in cases {
438                    if *target == old_target {
439                        *target = new_target;
440                        changed = true;
441                    }
442                }
443                changed
444            }
445            Terminator::Return { .. }
446            | Terminator::Revert { .. }
447            | Terminator::ReturnData { .. }
448            | Terminator::Stop
449            | Terminator::SelfDestruct { .. }
450            | Terminator::Invalid => false,
451        }
452    }
453
454    /// Updates CFG edges after threading.
455    fn update_cfg_edges(&self, func: &mut Function) {
456        let block_ids: Vec<_> = func.blocks.indices().collect();
457        for block_id in &block_ids {
458            func.blocks[*block_id].predecessors.clear();
459        }
460
461        for block_id in block_ids {
462            let successors = func.blocks[block_id]
463                .terminator
464                .as_ref()
465                .map(|t| t.successors())
466                .unwrap_or_default();
467
468            for succ in successors {
469                func.blocks[succ].predecessors.push(block_id);
470            }
471        }
472    }
473}