Skip to main content

solar_codegen/transform/
sccp.rs

1//! Sparse Conditional Constant Propagation (SCCP).
2//!
3//! Implements the Wegman-Zadeck SCCP algorithm on MIR. This is more powerful
4//! than simple constant folding because it:
5//! - Propagates constants through the CFG using SSA def-use chains
6//! - Evaluates branch conditions to discover unreachable paths
7//! - Folds phi nodes when all executable incoming values agree
8//!
9//! The algorithm uses a three-valued lattice per SSA value:
10//! - **Top** (⊤): not yet evaluated
11//! - **Constant(v)**: known compile-time constant
12//! - **Bottom** (⊥): overdefined (not a constant)
13//!
14//! After reaching a fixed point, the rewrite phase replaces constant values
15//! with immediates and rewrites branches with known-constant conditions.
16
17use crate::{
18    mir::{
19        BlockId, Function, Immediate, InstId, InstKind, MirType, Terminator, Value, ValueId,
20        utils::{self as mir_utils, repair_reachability_phis},
21    },
22    pass::FunctionPass,
23    utils::evm_word,
24};
25use alloy_primitives::U256;
26use solar_data_structures::map::{FxHashMap, FxHashSet};
27use std::collections::VecDeque;
28
29/// Lattice element for a single SSA value.
30#[derive(Clone, Debug, PartialEq, Eq)]
31enum LatticeValue {
32    /// Not yet evaluated.
33    Top,
34    /// Known constant.
35    Constant(U256),
36    /// Overdefined — not a constant.
37    Bottom,
38}
39
40impl LatticeValue {
41    /// Meet operation: merges two lattice values.
42    /// Top ∧ x = x, Bottom ∧ x = Bottom, Const(a) ∧ Const(b) = if a==b Const(a) else Bottom.
43    fn meet(&self, other: &Self) -> Self {
44        match (self, other) {
45            (Self::Top, x) | (x, Self::Top) => x.clone(),
46            (Self::Bottom, _) | (_, Self::Bottom) => Self::Bottom,
47            (Self::Constant(a), Self::Constant(b)) => {
48                if a == b {
49                    Self::Constant(*a)
50                } else {
51                    Self::Bottom
52                }
53            }
54        }
55    }
56}
57
58/// SCCP statistics.
59#[derive(Debug, Default, Clone)]
60pub struct SccpStats {
61    /// Number of instructions replaced with constants.
62    pub constants_folded: usize,
63    /// Number of branches replaced with unconditional jumps.
64    pub branches_folded: usize,
65    /// Number of switches replaced with unconditional jumps.
66    pub switches_folded: usize,
67    /// Number of unreachable blocks emptied and marked invalid.
68    pub blocks_invalidated: usize,
69}
70
71/// Sparse Conditional Constant Propagation pass.
72#[derive(Debug, Default)]
73pub struct SccpPass {
74    /// Statistics from the last run.
75    pub stats: SccpStats,
76}
77
78/// Function pass adapter for sparse conditional constant propagation.
79pub struct SccpTransformPass;
80
81impl FunctionPass for SccpTransformPass {
82    fn name(&self) -> &str {
83        "sccp"
84    }
85
86    fn run_on_function(&mut self, func: &mut Function) -> bool {
87        SccpPass::new().run(func) != 0
88    }
89}
90
91impl SccpPass {
92    /// Creates a new SCCP pass.
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    /// Runs SCCP on a function. Returns the total number of mutations,
98    /// including unreachable-block cleanup and phi repairs.
99    pub fn run(&mut self, func: &mut Function) -> usize {
100        self.stats = SccpStats::default();
101
102        let num_values = func.values.len();
103
104        // Precompute InstId → ValueId map.
105        let inst_to_value: FxHashMap<InstId, ValueId> = func
106            .values
107            .iter_enumerated()
108            .filter_map(
109                |(vid, val)| {
110                    if let Value::Inst(iid) = val { Some((*iid, vid)) } else { None }
111                },
112            )
113            .collect();
114
115        // Initialize lattice: all values start as Top.
116        let mut lattice: Vec<LatticeValue> = vec![LatticeValue::Top; num_values];
117
118        // Arguments are overdefined (we don't know their runtime values).
119        for (vid, val) in func.values.iter_enumerated() {
120            match val {
121                Value::Arg { .. } => lattice[vid.index()] = LatticeValue::Bottom,
122                Value::Immediate(imm) => {
123                    if let Some(v) = imm.as_u256() {
124                        lattice[vid.index()] = LatticeValue::Constant(v);
125                    } else {
126                        lattice[vid.index()] = LatticeValue::Bottom;
127                    }
128                }
129                Value::Undef(_) | Value::Error(_) => lattice[vid.index()] = LatticeValue::Bottom,
130                Value::Inst(_) => {} // stays Top
131            }
132        }
133
134        // Track which blocks are executable.
135        let mut executable_blocks: FxHashSet<BlockId> = FxHashSet::default();
136        // Track which CFG edges have been taken.
137        let mut executable_edges: FxHashSet<(BlockId, BlockId)> = FxHashSet::default();
138
139        // Two worklists.
140        let mut cfg_worklist: VecDeque<(BlockId, BlockId)> = VecDeque::new(); // (from, to) edges
141        let mut ssa_worklist: VecDeque<ValueId> = VecDeque::new();
142
143        // Seed: entry block is executable.
144        executable_blocks.insert(func.entry_block);
145        self.evaluate_phis_in_block(
146            func,
147            func.entry_block,
148            &inst_to_value,
149            &mut lattice,
150            &executable_edges,
151            &mut ssa_worklist,
152        );
153        // Evaluate all instructions in the entry block.
154        self.evaluate_block(
155            func,
156            func.entry_block,
157            &inst_to_value,
158            &mut lattice,
159            &executable_blocks,
160            &executable_edges,
161            &mut cfg_worklist,
162            &mut ssa_worklist,
163        );
164
165        // Main loop: process both worklists until empty.
166        loop {
167            let mut made_progress = false;
168
169            // Process CFG edges.
170            while let Some((from, to)) = cfg_worklist.pop_front() {
171                if !executable_edges.insert((from, to)) {
172                    continue; // Already processed this edge.
173                }
174                made_progress = true;
175
176                let newly_executable = executable_blocks.insert(to);
177
178                // Re-evaluate phi-like values in the target block.
179                self.evaluate_phis_in_block(
180                    func,
181                    to,
182                    &inst_to_value,
183                    &mut lattice,
184                    &executable_edges,
185                    &mut ssa_worklist,
186                );
187
188                if newly_executable {
189                    // First time this block is executable — evaluate all its instructions.
190                    self.evaluate_block(
191                        func,
192                        to,
193                        &inst_to_value,
194                        &mut lattice,
195                        &executable_blocks,
196                        &executable_edges,
197                        &mut cfg_worklist,
198                        &mut ssa_worklist,
199                    );
200                }
201            }
202
203            // Process SSA value changes.
204            while let Some(vid) = ssa_worklist.pop_front() {
205                made_progress = true;
206                // Find all users of this value and re-evaluate them.
207                self.propagate_value(
208                    func,
209                    vid,
210                    &inst_to_value,
211                    &mut lattice,
212                    &executable_blocks,
213                    &executable_edges,
214                    &mut cfg_worklist,
215                    &mut ssa_worklist,
216                );
217            }
218
219            if !made_progress {
220                break;
221            }
222        }
223
224        // Rewrite phase: apply the lattice results to the function.
225        self.rewrite(func, &lattice, &inst_to_value, &executable_blocks, &executable_edges)
226    }
227
228    /// Evaluates all instructions in a block.
229    #[allow(clippy::too_many_arguments)]
230    fn evaluate_block(
231        &self,
232        func: &Function,
233        block_id: BlockId,
234        inst_to_value: &FxHashMap<InstId, ValueId>,
235        lattice: &mut [LatticeValue],
236        _executable_blocks: &FxHashSet<BlockId>,
237        _executable_edges: &FxHashSet<(BlockId, BlockId)>,
238        cfg_worklist: &mut VecDeque<(BlockId, BlockId)>,
239        ssa_worklist: &mut VecDeque<ValueId>,
240    ) {
241        let block = &func.blocks[block_id];
242
243        for &inst_id in &block.instructions {
244            if matches!(func.instructions[inst_id].kind, InstKind::Phi(_)) {
245                continue;
246            }
247            if let Some(&vid) = inst_to_value.get(&inst_id) {
248                let new_val =
249                    self.evaluate_instruction(func, &func.instructions[inst_id].kind, lattice);
250                if self.update_lattice(lattice, vid, new_val) {
251                    ssa_worklist.push_back(vid);
252                }
253            }
254        }
255
256        // Evaluate the terminator to determine outgoing edges.
257        if let Some(term) = &block.terminator {
258            self.evaluate_terminator(term, block_id, lattice, cfg_worklist);
259        }
260    }
261
262    /// Evaluates phi instructions (`InstKind::Phi`) at the entry of a block.
263    fn evaluate_phis_in_block(
264        &self,
265        func: &Function,
266        block_id: BlockId,
267        inst_to_value: &FxHashMap<InstId, ValueId>,
268        lattice: &mut [LatticeValue],
269        executable_edges: &FxHashSet<(BlockId, BlockId)>,
270        ssa_worklist: &mut VecDeque<ValueId>,
271    ) {
272        let block = &func.blocks[block_id];
273        for &inst_id in &block.instructions {
274            let inst = &func.instructions[inst_id];
275            if let InstKind::Phi(incoming) = &inst.kind
276                && let Some(&vid) = inst_to_value.get(&inst_id)
277            {
278                // Meet over all executable incoming edges.
279                let mut result = LatticeValue::Top;
280                for &(pred, operand) in incoming {
281                    if executable_edges.contains(&(pred, block_id)) {
282                        result = result.meet(&lattice[operand.index()]);
283                    }
284                }
285                if self.update_lattice(lattice, vid, result) {
286                    ssa_worklist.push_back(vid);
287                }
288            }
289        }
290    }
291
292    /// Evaluates a single instruction and returns its lattice value.
293    fn evaluate_instruction(
294        &self,
295        _func: &Function,
296        kind: &InstKind,
297        lattice: &[LatticeValue],
298    ) -> LatticeValue {
299        // Helper: get the constant value of a ValueId, or None if not constant.
300        let get_const = |v: ValueId| -> Option<U256> {
301            match &lattice[v.index()] {
302                LatticeValue::Constant(c) => Some(*c),
303                _ => None,
304            }
305        };
306
307        match kind {
308            // Arithmetic — fold if both operands are constant.
309            InstKind::Add(a, b) => match (get_const(*a), get_const(*b)) {
310                (Some(a), Some(b)) => LatticeValue::Constant(a.wrapping_add(b)),
311                _ => self.check_any_bottom(&[*a, *b], lattice),
312            },
313            InstKind::Sub(a, b) => match (get_const(*a), get_const(*b)) {
314                (Some(a), Some(b)) => LatticeValue::Constant(a.wrapping_sub(b)),
315                _ => self.check_any_bottom(&[*a, *b], lattice),
316            },
317            InstKind::Mul(a, b) => match (get_const(*a), get_const(*b)) {
318                (Some(a), Some(b)) => LatticeValue::Constant(a.wrapping_mul(b)),
319                _ => self.check_any_bottom(&[*a, *b], lattice),
320            },
321            // A known-zero divisor folds to 0 even when the dividend is unknown.
322            InstKind::Div(a, b) => match (get_const(*a), get_const(*b)) {
323                (_, Some(b)) if b.is_zero() => LatticeValue::Constant(U256::ZERO),
324                (Some(a), Some(b)) => LatticeValue::Constant(a / b),
325                _ => self.check_any_bottom(&[*a, *b], lattice),
326            },
327            InstKind::SDiv(a, b) => match (get_const(*a), get_const(*b)) {
328                (_, Some(b)) if b.is_zero() => LatticeValue::Constant(U256::ZERO),
329                (Some(a), Some(b)) => LatticeValue::Constant(evm_word::signed_div(a, b)),
330                _ => self.check_any_bottom(&[*a, *b], lattice),
331            },
332            InstKind::Mod(a, b) => match (get_const(*a), get_const(*b)) {
333                (_, Some(b)) if b.is_zero() => LatticeValue::Constant(U256::ZERO),
334                (Some(a), Some(b)) => LatticeValue::Constant(a % b),
335                _ => self.check_any_bottom(&[*a, *b], lattice),
336            },
337            InstKind::SMod(a, b) => match (get_const(*a), get_const(*b)) {
338                (_, Some(b)) if b.is_zero() => LatticeValue::Constant(U256::ZERO),
339                (Some(a), Some(b)) => LatticeValue::Constant(evm_word::signed_mod(a, b)),
340                _ => self.check_any_bottom(&[*a, *b], lattice),
341            },
342            // A known-zero modulus folds to 0 even when the operands are unknown.
343            InstKind::AddMod(a, b, n) => match (get_const(*a), get_const(*b), get_const(*n)) {
344                (_, _, Some(n)) if n.is_zero() => LatticeValue::Constant(U256::ZERO),
345                (Some(a), Some(b), Some(n)) => LatticeValue::Constant(a.add_mod(b, n)),
346                _ => self.check_any_bottom(&[*a, *b, *n], lattice),
347            },
348            InstKind::MulMod(a, b, n) => match (get_const(*a), get_const(*b), get_const(*n)) {
349                (_, _, Some(n)) if n.is_zero() => LatticeValue::Constant(U256::ZERO),
350                (Some(a), Some(b), Some(n)) => LatticeValue::Constant(a.mul_mod(b, n)),
351                _ => self.check_any_bottom(&[*a, *b, *n], lattice),
352            },
353            InstKind::Exp(a, b) => match (get_const(*a), get_const(*b)) {
354                (Some(base), Some(exp)) => {
355                    // Use wrapping exponentiation.
356                    LatticeValue::Constant(base.wrapping_pow(exp))
357                }
358                _ => self.check_any_bottom(&[*a, *b], lattice),
359            },
360
361            // Comparison — fold to bool (0 or 1).
362            InstKind::Lt(a, b) => match (get_const(*a), get_const(*b)) {
363                (Some(a), Some(b)) => LatticeValue::Constant(U256::from(a < b)),
364                _ => self.check_any_bottom(&[*a, *b], lattice),
365            },
366            InstKind::Gt(a, b) => match (get_const(*a), get_const(*b)) {
367                (Some(a), Some(b)) => LatticeValue::Constant(U256::from(a > b)),
368                _ => self.check_any_bottom(&[*a, *b], lattice),
369            },
370            InstKind::SLt(a, b) => match (get_const(*a), get_const(*b)) {
371                (Some(a), Some(b)) => LatticeValue::Constant(U256::from(evm_word::signed_lt(a, b))),
372                _ => self.check_any_bottom(&[*a, *b], lattice),
373            },
374            InstKind::SGt(a, b) => match (get_const(*a), get_const(*b)) {
375                (Some(a), Some(b)) => LatticeValue::Constant(U256::from(evm_word::signed_gt(a, b))),
376                _ => self.check_any_bottom(&[*a, *b], lattice),
377            },
378            InstKind::Eq(a, b) => match (get_const(*a), get_const(*b)) {
379                (Some(a), Some(b)) => LatticeValue::Constant(U256::from(a == b)),
380                _ => self.check_any_bottom(&[*a, *b], lattice),
381            },
382            InstKind::IsZero(a) => match get_const(*a) {
383                Some(a) => LatticeValue::Constant(U256::from(a.is_zero())),
384                None => self.check_any_bottom(&[*a], lattice),
385            },
386
387            // Bitwise.
388            InstKind::And(a, b) => match (get_const(*a), get_const(*b)) {
389                (Some(a), Some(b)) => LatticeValue::Constant(a & b),
390                _ => self.check_any_bottom(&[*a, *b], lattice),
391            },
392            InstKind::Or(a, b) => match (get_const(*a), get_const(*b)) {
393                (Some(a), Some(b)) => LatticeValue::Constant(a | b),
394                _ => self.check_any_bottom(&[*a, *b], lattice),
395            },
396            InstKind::Xor(a, b) => match (get_const(*a), get_const(*b)) {
397                (Some(a), Some(b)) => LatticeValue::Constant(a ^ b),
398                _ => self.check_any_bottom(&[*a, *b], lattice),
399            },
400            InstKind::Not(a) => match get_const(*a) {
401                Some(a) => LatticeValue::Constant(!a),
402                None => self.check_any_bottom(&[*a], lattice),
403            },
404            InstKind::Shl(shift, val) => match (get_const(*shift), get_const(*val)) {
405                (Some(s), Some(v)) => {
406                    if s >= U256::from(256) {
407                        LatticeValue::Constant(U256::ZERO)
408                    } else {
409                        LatticeValue::Constant(v << s.to::<usize>())
410                    }
411                }
412                _ => self.check_any_bottom(&[*shift, *val], lattice),
413            },
414            InstKind::Shr(shift, val) => match (get_const(*shift), get_const(*val)) {
415                (Some(s), Some(v)) => {
416                    if s >= U256::from(256) {
417                        LatticeValue::Constant(U256::ZERO)
418                    } else {
419                        LatticeValue::Constant(v >> s.to::<usize>())
420                    }
421                }
422                _ => self.check_any_bottom(&[*shift, *val], lattice),
423            },
424            InstKind::Sar(shift, val) => match (get_const(*shift), get_const(*val)) {
425                (Some(s), Some(v)) => LatticeValue::Constant(evm_word::sar(v, s)),
426                _ => self.check_any_bottom(&[*shift, *val], lattice),
427            },
428            InstKind::Byte(index, val) => match (get_const(*index), get_const(*val)) {
429                (Some(i), Some(v)) => LatticeValue::Constant(evm_word::byte(i, v)),
430                _ => self.check_any_bottom(&[*index, *val], lattice),
431            },
432            InstKind::SignExtend(size, val) => match (get_const(*size), get_const(*val)) {
433                (Some(s), Some(v)) => LatticeValue::Constant(evm_word::signextend(s, v)),
434                _ => self.check_any_bottom(&[*size, *val], lattice),
435            },
436
437            InstKind::Select(condition, then_value, else_value) => {
438                match &lattice[condition.index()] {
439                    LatticeValue::Constant(c) => {
440                        let chosen = if c.is_zero() { *else_value } else { *then_value };
441                        lattice[chosen.index()].clone()
442                    }
443                    // Unknown condition: the result is whatever both arms agree on.
444                    LatticeValue::Bottom => {
445                        lattice[then_value.index()].meet(&lattice[else_value.index()])
446                    }
447                    LatticeValue::Top => match (get_const(*then_value), get_const(*else_value)) {
448                        (Some(t), Some(e)) if t == e => LatticeValue::Constant(t),
449                        _ => LatticeValue::Top,
450                    },
451                }
452            }
453
454            // Everything else (memory, storage, calls, environment, etc.) is
455            // conservatively overdefined — we can't evaluate them at compile time.
456            _ => LatticeValue::Bottom,
457        }
458    }
459
460    /// Helper: if any operand is Bottom, return Bottom; otherwise Top (still waiting).
461    fn check_any_bottom(&self, operands: &[ValueId], lattice: &[LatticeValue]) -> LatticeValue {
462        for &op in operands {
463            if matches!(lattice[op.index()], LatticeValue::Bottom) {
464                return LatticeValue::Bottom;
465            }
466        }
467        LatticeValue::Top
468    }
469
470    /// Evaluates a terminator to determine which outgoing edges are taken.
471    fn evaluate_terminator(
472        &self,
473        term: &Terminator,
474        block_id: BlockId,
475        lattice: &[LatticeValue],
476        cfg_worklist: &mut VecDeque<(BlockId, BlockId)>,
477    ) {
478        match term {
479            Terminator::Jump(target) => {
480                cfg_worklist.push_back((block_id, *target));
481            }
482            Terminator::Branch { condition, then_block, else_block } => {
483                match &lattice[condition.index()] {
484                    LatticeValue::Constant(v) => {
485                        if !v.is_zero() {
486                            cfg_worklist.push_back((block_id, *then_block));
487                        } else {
488                            cfg_worklist.push_back((block_id, *else_block));
489                        }
490                    }
491                    LatticeValue::Bottom => {
492                        // Both edges might be taken.
493                        cfg_worklist.push_back((block_id, *then_block));
494                        cfg_worklist.push_back((block_id, *else_block));
495                    }
496                    LatticeValue::Top => {}
497                }
498            }
499            Terminator::Switch { value, default, cases } => {
500                match &lattice[value.index()] {
501                    LatticeValue::Constant(v) => {
502                        // Cases are tested in order at runtime, so a constant
503                        // case match is definitive only if every earlier case
504                        // is a known constant that differs from the scrutinee.
505                        // Overdefined earlier cases stay feasible; an
506                        // unresolved case defers the remaining edges.
507                        for &(case_val, target) in cases {
508                            match &lattice[case_val.index()] {
509                                LatticeValue::Constant(cv) if cv == v => {
510                                    cfg_worklist.push_back((block_id, target));
511                                    return;
512                                }
513                                LatticeValue::Constant(_) => {}
514                                LatticeValue::Bottom => {
515                                    cfg_worklist.push_back((block_id, target));
516                                }
517                                LatticeValue::Top => return,
518                            }
519                        }
520                        cfg_worklist.push_back((block_id, *default));
521                    }
522                    LatticeValue::Bottom => {
523                        // All edges might be taken.
524                        cfg_worklist.push_back((block_id, *default));
525                        for &(_, target) in cases {
526                            cfg_worklist.push_back((block_id, target));
527                        }
528                    }
529                    LatticeValue::Top => {}
530                }
531            }
532            Terminator::Return { .. }
533            | Terminator::Revert { .. }
534            | Terminator::ReturnData { .. }
535            | Terminator::Stop
536            | Terminator::SelfDestruct { .. }
537            | Terminator::Invalid => {
538                // No outgoing edges.
539            }
540        }
541    }
542
543    /// Updates the lattice value for a ValueId. Returns true if it changed.
544    /// Lattice values can only move downward: Top → Constant → Bottom.
545    fn update_lattice(
546        &self,
547        lattice: &mut [LatticeValue],
548        vid: ValueId,
549        new_val: LatticeValue,
550    ) -> bool {
551        let old = &lattice[vid.index()];
552        let merged = old.meet(&new_val);
553        if merged != *old {
554            lattice[vid.index()] = merged;
555            true
556        } else {
557            false
558        }
559    }
560
561    /// Propagates a value change to all users of that value.
562    #[allow(clippy::too_many_arguments)]
563    fn propagate_value(
564        &self,
565        func: &Function,
566        vid: ValueId,
567        inst_to_value: &FxHashMap<InstId, ValueId>,
568        lattice: &mut [LatticeValue],
569        executable_blocks: &FxHashSet<BlockId>,
570        executable_edges: &FxHashSet<(BlockId, BlockId)>,
571        cfg_worklist: &mut VecDeque<(BlockId, BlockId)>,
572        ssa_worklist: &mut VecDeque<ValueId>,
573    ) {
574        for block_id in executable_blocks {
575            self.evaluate_phis_in_block(
576                func,
577                *block_id,
578                inst_to_value,
579                lattice,
580                executable_edges,
581                ssa_worklist,
582            );
583        }
584
585        // Find all instructions that use this value and re-evaluate them.
586        for (block_id, block) in func.blocks.iter_enumerated() {
587            if !executable_blocks.contains(&block_id) {
588                continue;
589            }
590            for &inst_id in &block.instructions {
591                let inst = &func.instructions[inst_id];
592                if matches!(inst.kind, InstKind::Phi(_)) {
593                    continue;
594                }
595                let operands = inst.kind.operands();
596                if operands.contains(&vid)
597                    && let Some(&result_vid) = inst_to_value.get(&inst_id)
598                {
599                    let new_val = self.evaluate_instruction(func, &inst.kind, lattice);
600                    if self.update_lattice(lattice, result_vid, new_val) {
601                        ssa_worklist.push_back(result_vid);
602                    }
603                }
604            }
605            // Re-evaluate terminators that use this value.
606            if let Some(term) = &block.terminator {
607                let term_ops = term.operands();
608                if term_ops.contains(&vid) {
609                    self.evaluate_terminator(term, block_id, lattice, cfg_worklist);
610                }
611            }
612        }
613    }
614
615    /// Rewrite phase: replace constant values with immediates and fold branches.
616    fn rewrite(
617        &mut self,
618        func: &mut Function,
619        lattice: &[LatticeValue],
620        inst_to_value: &FxHashMap<InstId, ValueId>,
621        executable_blocks: &FxHashSet<BlockId>,
622        executable_edges: &FxHashSet<(BlockId, BlockId)>,
623    ) -> usize {
624        // Phase 1: Replace instructions whose results are constant with
625        // immediate values, and remove the instruction from the block.
626        let mut const_values: FxHashMap<ValueId, ValueId> = FxHashMap::default();
627        let mut dead_insts: FxHashSet<InstId> = FxHashSet::default();
628
629        for (&inst_id, &vid) in inst_to_value {
630            if let LatticeValue::Constant(c) = &lattice[vid.index()] {
631                // Don't fold side-effecting instructions.
632                if func.instructions[inst_id].kind.has_side_effects() {
633                    continue;
634                }
635                // Create an immediate replacement of the instruction's result type.
636                let imm = immediate_for_type(func.instructions[inst_id].result_ty, *c);
637                let imm_vid = func.alloc_value(Value::Immediate(imm));
638                const_values.insert(vid, imm_vid);
639                dead_insts.insert(inst_id);
640                self.stats.constants_folded += 1;
641            }
642        }
643
644        // Phase 2: Collect branch rewrites BEFORE operand replacement, because
645        // replacement may allocate new ValueIds that don't have lattice entries.
646        let block_ids: Vec<BlockId> = func.blocks.indices().collect();
647        let mut control_rewrites: Vec<(BlockId, BlockId)> = Vec::new();
648        for &block_id in &block_ids {
649            if !executable_blocks.contains(&block_id) {
650                continue;
651            }
652            let Some(term) = &func.blocks[block_id].terminator else {
653                continue;
654            };
655            if !matches!(term, Terminator::Branch { .. } | Terminator::Switch { .. }) {
656                continue;
657            }
658
659            let executable_successors: FxHashSet<_> = term
660                .successors()
661                .into_iter()
662                .filter(|&successor| executable_edges.contains(&(block_id, successor)))
663                .collect();
664            if executable_successors.len() == 1 {
665                let target = executable_successors.into_iter().next().expect("checked len");
666                control_rewrites.push((block_id, target));
667            }
668        }
669
670        // Phase 3: Replace all uses of folded values with immediates.
671        if !const_values.is_empty() {
672            let all_insts: Vec<InstId> = func
673                .blocks
674                .iter()
675                .flat_map(|block| block.instructions.iter().copied())
676                .filter(|id| !dead_insts.contains(id))
677                .collect();
678            for inst_id in all_insts {
679                mir_utils::replace_inst_uses(&mut func.instructions[inst_id].kind, &const_values);
680            }
681            for &block_id in &block_ids {
682                if let Some(term) = &mut func.blocks[block_id].terminator {
683                    mir_utils::replace_terminator_uses(term, &const_values);
684                }
685            }
686        }
687
688        // Phase 4: Remove dead (folded) instructions from blocks.
689        for &block_id in &block_ids {
690            func.blocks[block_id].instructions.retain(|id| !dead_insts.contains(id));
691        }
692
693        // Phase 5: Apply branch/switch rewrites.
694        for (block_id, target) in control_rewrites {
695            let old_successors = func.blocks[block_id]
696                .terminator
697                .as_ref()
698                .map(Terminator::successors)
699                .unwrap_or_default();
700            let was_switch =
701                matches!(func.blocks[block_id].terminator, Some(Terminator::Switch { .. }));
702            for successor in old_successors {
703                func.blocks[successor].predecessors.retain(|pred| *pred != block_id);
704            }
705            if !func.blocks[target].predecessors.contains(&block_id) {
706                func.blocks[target].predecessors.push(block_id);
707            }
708            func.blocks[block_id].terminator = Some(Terminator::Jump(target));
709            if was_switch {
710                self.stats.switches_folded += 1;
711            } else {
712                self.stats.branches_folded += 1;
713            }
714        }
715
716        // Phase 6: Mark non-executable blocks as invalid.
717        for &block_id in &block_ids {
718            if executable_blocks.contains(&block_id) {
719                continue;
720            }
721            let block = &mut func.blocks[block_id];
722            // Predecessor lists are rebuilt from terminators by
723            // `repair_reachability_phis` below, so a never-taken switch target
724            // keeps a predecessor entry; checking it here would re-count the
725            // block as invalidated on every run.
726            let already_invalid = block.instructions.is_empty()
727                && matches!(block.terminator, Some(Terminator::Invalid));
728            if already_invalid {
729                continue;
730            }
731            block.instructions.clear();
732            block.terminator = Some(Terminator::Invalid);
733            block.predecessors.clear();
734            self.stats.blocks_invalidated += 1;
735        }
736
737        let phis_repaired = repair_reachability_phis(func);
738
739        self.stats.constants_folded
740            + self.stats.branches_folded
741            + self.stats.switches_folded
742            + self.stats.blocks_invalidated
743            + usize::from(phis_repaired)
744    }
745}
746
747/// Builds an immediate carrying `value` with the type the folded instruction
748/// produced, falling back to `uint256` for types whose payload is not a plain
749/// integer or whose range cannot represent the folded value (the lattice folds
750/// at 256 bits, so a narrow-typed op can produce an out-of-range word). The
751/// numeric value is identical in all cases.
752fn immediate_for_type(ty: Option<MirType>, value: U256) -> Immediate {
753    match ty {
754        Some(MirType::Bool) if value <= U256::from(1) => Immediate::Bool(!value.is_zero()),
755        Some(MirType::UInt(bits)) if fits_unsigned(value, bits) => Immediate::UInt(value, bits),
756        Some(MirType::Int(bits)) if fits_signed(value, bits) => Immediate::Int(value, bits),
757        _ => Immediate::uint256(value),
758    }
759}
760
761fn fits_unsigned(value: U256, bits: u16) -> bool {
762    bits >= 256 || value.bit_len() <= usize::from(bits)
763}
764
765fn fits_signed(value: U256, bits: u16) -> bool {
766    if bits >= 256 || bits == 0 {
767        return bits >= 256;
768    }
769    let bits = usize::from(bits);
770    // Representable iff bits 255..=bits-1 of the 256-bit two's-complement word
771    // all equal the sign bit.
772    if value.bit(bits - 1) { (!value).bit_len() < bits } else { value.bit_len() < bits }
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778
779    #[test]
780    fn immediate_for_type_preserves_result_types() {
781        let one = U256::from(1);
782        assert_eq!(immediate_for_type(Some(MirType::Bool), one), Immediate::Bool(true));
783        assert_eq!(immediate_for_type(Some(MirType::Bool), U256::ZERO), Immediate::Bool(false));
784        assert_eq!(immediate_for_type(Some(MirType::Int(256)), one), Immediate::Int(one, 256));
785        assert_eq!(immediate_for_type(Some(MirType::UInt(64)), one), Immediate::UInt(one, 64));
786        // Non-integer payloads and missing types fall back to uint256.
787        assert_eq!(immediate_for_type(Some(MirType::Address), one), Immediate::uint256(one));
788        assert_eq!(immediate_for_type(None, one), Immediate::uint256(one));
789        // A bool-typed result that is not 0/1 keeps its numeric value.
790        let two = U256::from(2);
791        assert_eq!(immediate_for_type(Some(MirType::Bool), two), Immediate::uint256(two));
792        // Out-of-range values fall back to uint256 instead of lying about the width.
793        let wide = U256::from(0x1ff);
794        assert_eq!(immediate_for_type(Some(MirType::UInt(8)), wide), Immediate::uint256(wide));
795        assert_eq!(immediate_for_type(Some(MirType::Int(8)), wide), Immediate::uint256(wide));
796        // Negative values are representable when the upper bits match the sign bit.
797        let minus_one = U256::MAX;
798        assert_eq!(
799            immediate_for_type(Some(MirType::Int(8)), minus_one),
800            Immediate::Int(minus_one, 8)
801        );
802        let i8_min = U256::MAX - U256::from(0x7f);
803        assert_eq!(immediate_for_type(Some(MirType::Int(8)), i8_min), Immediate::Int(i8_min, 8));
804        let i8_under = i8_min - U256::from(1);
805        assert_eq!(
806            immediate_for_type(Some(MirType::Int(8)), i8_under),
807            Immediate::uint256(i8_under)
808        );
809    }
810}