//! IR simplification pass: constant folding, algebraic identities, copy propagation, //! and dead code elimination. //! //! This pass runs after inlining to clean up the IR: //! 1. **Constant folding**: Binary/unary ops on literals → literal result //! 2. **Algebraic identities**: `add(x, 0) → x`, `mul(x, 1) → x`, etc. //! 3. **Copy propagation**: `let x = y` → replace all uses of x with y //! 4. **Dead code elimination**: Remove unused Let bindings //! //! All arithmetic follows EVM semantics (unsigned 256-bit, wrapping). use std::collections::{BTreeMap, BTreeSet}; use num::{BigUint, One, ToPrimitive, Zero}; use crate::ir::{ for_each_statement_mut, BinaryOperation, BitWidth, Block, CallKind, Expression, Function, FunctionId, MemoryRegion, Object, Region, Statement, SwitchCase, Type, UnaryOperation, Value, ValueId, }; /// Maximum value for 256-bit unsigned integer (2^256 - 1). fn max_u256() -> BigUint { (BigUint::one() << 256) - BigUint::one() } /// The modulus for 256-bit wrapping arithmetic (2^256). fn modulus_u256() -> BigUint { BigUint::one() << 256 } /// Results of the simplification pass. #[derive(Clone, Debug, Default)] pub struct SimplifyResults { /// Number of expressions constant-folded. pub constants_folded: usize, /// Number of algebraic identity simplifications. pub identities_simplified: usize, /// Number of copy propagations. pub copies_propagated: usize, /// Number of dead Let bindings removed. pub dead_bindings_removed: usize, /// Number of constant branches eliminated. pub branches_eliminated: usize, /// Number of environment reads eliminated by CSE. pub env_reads_eliminated: usize, } /// Categories of pure environment reads that can be CSE'd. /// /// These are nullary expressions whose value is fixed for the duration of a /// single contract invocation, so reads of the same kind in the same dominator /// scope can share a binding. /// /// Deliberately excluded — these *look* nullary but are observably variable: /// - `Gas`: gas remaining, decreases on every opcode. /// - `MSize`: memory size, grows when memory expands. /// - `ReturnDataSize`: resets after each external call. /// - `SelfBalance`: changes when the contract sends or receives value. /// /// Hash expressions (`Keccak256*`) take operands and so aren't `EnvRead` /// candidates by shape; their common-subexpression elimination is handled /// separately. #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] enum EnvRead { Address, BaseFee, BlobBaseFee, CallDataSize, CallValue, Caller, ChainId, CodeSize, Coinbase, Difficulty, GasLimit, GasPrice, Number, Origin, Timestamp, } /// Returns the `EnvRead` kind for an expression if it is a pure environment read. fn env_read_kind(expression: &Expression) -> Option { match expression { Expression::Address => Some(EnvRead::Address), Expression::BaseFee => Some(EnvRead::BaseFee), Expression::BlobBaseFee => Some(EnvRead::BlobBaseFee), Expression::CallDataSize => Some(EnvRead::CallDataSize), Expression::CallValue => Some(EnvRead::CallValue), Expression::Caller => Some(EnvRead::Caller), Expression::ChainId => Some(EnvRead::ChainId), Expression::CodeSize => Some(EnvRead::CodeSize), Expression::Coinbase => Some(EnvRead::Coinbase), Expression::Difficulty => Some(EnvRead::Difficulty), Expression::GasLimit => Some(EnvRead::GasLimit), Expression::GasPrice => Some(EnvRead::GasPrice), Expression::Number => Some(EnvRead::Number), Expression::Origin => Some(EnvRead::Origin), Expression::Timestamp => Some(EnvRead::Timestamp), _ => None, } } /// IR simplification pass. pub struct Simplifier { /// Maps ValueId → constant BigUint for known constants. constants: BTreeMap, /// Maps ValueId → ValueId for copy propagation (let x = y → x maps to y). copies: BTreeMap, /// Maps ValueId → (UnaryOperation, operand ValueId) for unary expression tracking. /// Used to simplify patterns like not(not(x)) → x. unary_definitions: BTreeMap, /// Maps ValueId → (BinaryOperation, lhs ValueId, rhs ValueId) for binary /// expression tracking. Used to simplify patterns like `sub(add(x, y), x) → y`. binary_definitions: BTreeMap, /// Counter for fresh value IDs when creating new bindings (strength reduction). next_value_id: ValueId, /// CSE cache for pure environment reads (calldatasize, caller, etc.). /// Maps the read category to the first ValueId that bound this expression. /// Saved/restored in region scopes to ensure LLVM SSA domination correctness: /// a binding from one branch must not be referenced from a sibling branch. env_reads: BTreeMap, /// Statistics. statistics: SimplifyResults, } impl Default for Simplifier { fn default() -> Self { Self::new() } } impl Simplifier { /// Creates a new simplifier. pub fn new() -> Self { Simplifier { constants: BTreeMap::new(), copies: BTreeMap::new(), unary_definitions: BTreeMap::new(), binary_definitions: BTreeMap::new(), next_value_id: ValueId(0), env_reads: BTreeMap::new(), statistics: SimplifyResults::default(), } } /// Allocates a fresh ValueId. fn fresh_id(&mut self) -> ValueId { self.next_value_id.fresh() } /// Resolves the memory region for a value if its offset is a known constant. fn resolve_region(&self, value: &Value) -> MemoryRegion { if let Some(address) = self.constants.get(&value.id.0) { MemoryRegion::from_address(address) } else { MemoryRegion::Unknown } } /// Clears the per-function analysis state before simplifying the next function. /// /// The value-id-keyed caches (`constants`, `copies`, `unary_definitions`, /// `binary_definitions`) cannot collide across functions because `ValueId`s are /// globally unique, but the `env_reads` cache is keyed by [`EnvRead`] and *would* /// leak an out-of-scope value into the next function if it were not reset. Clearing /// all of them keeps the intent explicit: any new per-function map added to /// `Simplifier` belongs here too. `next_value_id` is deliberately not reset (it must /// keep advancing to preserve globally unique ids) and `statistics` accumulates over /// the whole object. fn clear_per_function_state(&mut self) { self.constants.clear(); self.copies.clear(); self.unary_definitions.clear(); self.binary_definitions.clear(); self.env_reads.clear(); } /// Simplifies an entire object in place. pub fn simplify_object(&mut self, object: &mut Object) -> SimplifyResults { self.next_value_id = ValueId(object.find_max_value_id() + 1); self.simplify_block(&mut object.code); self.statistics.dead_bindings_removed += eliminate_dead_code_in_statements(&mut object.code.statements, &BTreeSet::new()); for function in object.functions.values_mut() { self.clear_per_function_state(); function.body.statements = self.simplify_statements(std::mem::take(&mut function.body.statements)); Self::reconcile_dangling_return_values(function); let mut extra_used = BTreeSet::new(); for return_value_id in &function.return_values { extra_used.insert(return_value_id.0); } self.statistics.dead_bindings_removed += eliminate_dead_code_in_statements(&mut function.body.statements, &extra_used); } std::mem::take(&mut self.statistics) } /// Reconciles fall-through return values left dangling by simplification. /// /// Folding an always-true guard can collapse a function body to an unconditional /// `leave`, after which the dead fall-through code that defined some `return_values` /// is removed. Those `return_values` then reference deleted definitions, which the IR /// validator rejects as "used before definition" (codegen would zero-init them, but /// only the unreachable fall-through path observes that). Each now-undefined return /// value is pointed at its entry `return_values_initial` slot — defined at function /// entry, and sound because the fall-through that would observe it is unreachable. /// Return values still defined in the body are left untouched, so functions that /// legitimately end in a `leave` are unaffected. fn reconcile_dangling_return_values(function: &mut Function) { let mut defined_ids: BTreeSet = BTreeSet::new(); for (id, _ty) in &function.parameters { defined_ids.insert(id.0); } for id in &function.return_values_initial { defined_ids.insert(id.0); } crate::ir::for_each_statement(&function.body.statements, &mut |statement| { statement.for_each_value_id_def(&mut |id| { defined_ids.insert(id.0); }); }); for (index, return_value) in function.return_values.iter_mut().enumerate() { if !defined_ids.contains(&return_value.0) { if let Some(initial) = function.return_values_initial.get(index) { *return_value = *initial; } } } } /// Simplifies a block in place. fn simplify_block(&mut self, block: &mut Block) { block.statements = self.simplify_statements(std::mem::take(&mut block.statements)); } /// Simplifies a list of statements, returning the simplified list. /// /// Object code has no yields; a function body's return values are repaired separately by /// `reconcile_dangling_return_values`, so no rescue set is needed here. fn simplify_statements(&mut self, statements: Vec) -> Vec { let outer_constants = self.constants.clone(); let outer_copies = self.copies.clone(); let mut result = Vec::with_capacity(statements.len()); for statement in statements { let simplified = self.simplify_statement(statement); result.extend(simplified); } result = outline_panic_patterns(result, &self.constants, &BTreeSet::new()); result = outline_custom_error_patterns(result, &self.constants); self.constants = outer_constants; self.copies = outer_copies; result } /// Simplifies a single statement. /// Returns a vec of replacement statements (empty = remove, one = replace, multiple = expand). fn simplify_statement(&mut self, statement: Statement) -> Vec { match statement { Statement::Let { bindings, value } => { let simplified_expr = self.simplify_expression(value); if bindings.len() == 1 { if let Some(statements) = self.try_strength_reduce(&bindings, &simplified_expr) { return statements; } } if bindings.len() == 1 { if let Expression::Literal { ref value, .. } = simplified_expr { self.constants.insert(bindings[0].0, value.clone()); } if let Expression::Var(source_id) = &simplified_expr { let resolved = self.resolve_copy(*source_id); self.copies.insert(bindings[0].0, resolved); if let Some(constant) = self.constants.get(&resolved.0).cloned() { self.constants.insert(bindings[0].0, constant); } } if let Some(kind) = env_read_kind(&simplified_expr) { self.env_reads.entry(kind).or_insert(bindings[0]); } if let Expression::Unary { operation, operand } = &simplified_expr { self.unary_definitions .insert(bindings[0].0, (*operation, operand.id)); } if let Expression::Binary { operation, lhs, rhs, } = &simplified_expr { self.binary_definitions .insert(bindings[0].0, (*operation, lhs.id, rhs.id)); } } vec![Statement::Let { bindings, value: simplified_expr, }] } Statement::MStore { offset, value, region, } => { let offset = self.resolve_value(offset); let region = if region == MemoryRegion::Unknown { self.resolve_region(&offset) } else { region }; vec![Statement::MStore { offset, value: self.resolve_value(value), region, }] } Statement::MStore8 { offset, value, region, } => { let offset = self.resolve_value(offset); let region = if region == MemoryRegion::Unknown { self.resolve_region(&offset) } else { region }; vec![Statement::MStore8 { offset, value: self.resolve_value(value), region, }] } Statement::If { condition, inputs, then_region, else_region, outputs, } => { let condition = self.resolve_value(condition); let condition_value = self.try_get_const(&condition); if let Some(condition_const) = condition_value { let is_true = !condition_const.is_zero(); self.statistics.branches_eliminated += 1; if is_true { let then_region = self.simplify_region(then_region); let mut result = Vec::new(); result.extend(then_region.statements); for (output_id, yield_value) in outputs.iter().zip(then_region.yields.iter()) { result.push(Statement::Let { bindings: vec![*output_id], value: Expression::Var(yield_value.id), }); } return result; } else if let Some(else_region) = else_region { let else_region = self.simplify_region(else_region); let mut result = Vec::new(); result.extend(else_region.statements); for (output_id, yield_value) in outputs.iter().zip(else_region.yields.iter()) { result.push(Statement::Let { bindings: vec![*output_id], value: Expression::Var(yield_value.id), }); } return result; } else { let inputs: Vec = inputs .into_iter() .map(|input| self.resolve_value(input)) .collect(); let mut result = vec![]; for (output_id, input_value) in outputs.iter().zip(inputs.iter()) { result.push(Statement::Let { bindings: vec![*output_id], value: Expression::Var(input_value.id), }); } return result; } } let inputs: Vec = inputs .into_iter() .map(|input| self.resolve_value(input)) .collect(); let then_region = self.simplify_region(then_region); let else_region = else_region.map(|region| self.simplify_region(region)); vec![Statement::If { condition, inputs, then_region, else_region, outputs, }] } Statement::Switch { scrutinee, inputs, cases, default, outputs, } => { let scrutinee = self.resolve_value(scrutinee); let scrutinee_value = self.try_get_const(&scrutinee); if let Some(scrutinee_const) = scrutinee_value { let matching_case = cases.into_iter().find(|case| case.value == scrutinee_const); let taken_region = if let Some(case) = matching_case { self.simplify_region(case.body) } else if let Some(default_region) = default { self.simplify_region(default_region) } else { let inputs: Vec = inputs .into_iter() .map(|input| self.resolve_value(input)) .collect(); let mut result = vec![]; for (output_id, input_value) in outputs.iter().zip(inputs.iter()) { result.push(Statement::Let { bindings: vec![*output_id], value: Expression::Var(input_value.id), }); } self.statistics.branches_eliminated += 1; return result; }; let mut result = Vec::new(); result.extend(taken_region.statements); for (output_id, yield_value) in outputs.iter().zip(taken_region.yields.iter()) { result.push(Statement::Let { bindings: vec![*output_id], value: Expression::Var(yield_value.id), }); } self.statistics.branches_eliminated += 1; return result; } let inputs: Vec = inputs .into_iter() .map(|input| self.resolve_value(input)) .collect(); let mut cases: Vec = cases .into_iter() .map(|case| SwitchCase { value: case.value, body: self.simplify_region(case.body), }) .collect(); let mut default = default.map(|region| self.simplify_region(region)); let mut hoisted = Vec::new(); if !cases.is_empty() { if callvalue_check_fully_hoistable(&cases, &default) { let hoisted_callvalue = callvalue_binding_id(&cases[0].body.statements); hoisted.push(cases[0].body.statements[0].clone()); hoisted.push(cases[0].body.statements[1].clone()); for case in &mut cases { drain_callvalue_prefix(&mut case.body.statements, hoisted_callvalue); } if let Some(ref mut default_region) = default { drain_callvalue_prefix( &mut default_region.statements, hoisted_callvalue, ); } } else { const PARTIAL_HOIST_THRESHOLD: usize = 3; let callvalue_case_count = cases .iter() .filter(|case| starts_with_callvalue_let(&case.body.statements)) .count(); let default_has_callvalue = default.as_ref().is_some_and(|default_region| { starts_with_callvalue_let(&default_region.statements) }); let total_callvalue_count = callvalue_case_count + if default_has_callvalue { 1 } else { 0 }; if total_callvalue_count >= PARTIAL_HOIST_THRESHOLD { let hoisted_callvalue_id = self.fresh_id(); hoisted.push(Statement::Let { bindings: vec![hoisted_callvalue_id], value: Expression::CallValue, }); for case in &mut cases { replace_leading_callvalue_with_var( &mut case.body.statements, hoisted_callvalue_id, ); } if let Some(ref mut default_region) = default { replace_leading_callvalue_with_var( &mut default_region.statements, hoisted_callvalue_id, ); } } } } hoisted.push(Statement::Switch { scrutinee, inputs, cases, default, outputs, }); hoisted } Statement::For { initial_values, loop_variables, condition_statements, condition, body, post_input_variables, post, outputs, } => { let initial_values: Vec = initial_values .into_iter() .map(|value| self.resolve_value(value)) .collect(); let saved_constants = self.constants.clone(); let saved_copies = self.copies.clone(); let condition_statements = self.simplify_statements(condition_statements); let condition = self.simplify_expression(condition); let body = self.simplify_region(body); let post = self.simplify_region(post); self.constants = saved_constants; self.copies = saved_copies; vec![Statement::For { initial_values, loop_variables, condition_statements, condition, body, post_input_variables, post, outputs, }] } Statement::Block(region) => vec![Statement::Block(self.simplify_region(region))], Statement::Expression(expression) => { vec![Statement::Expression(self.simplify_expression(expression))] } Statement::Stop | Statement::Invalid | Statement::PanicRevert { .. } | Statement::ErrorStringRevert { .. } | Statement::CustomErrorRevert { .. } => vec![statement], mut other @ (Statement::MCopy { .. } | Statement::SStore { .. } | Statement::TStore { .. } | Statement::MappingSStore { .. } | Statement::Revert { .. } | Statement::Return { .. } | Statement::ExternalCall { .. } | Statement::Create { .. } | Statement::Log { .. } | Statement::CodeCopy { .. } | Statement::ExtCodeCopy { .. } | Statement::ReturnDataCopy { .. } | Statement::DataCopy { .. } | Statement::CallDataCopy { .. } | Statement::SetImmutable { .. } | Statement::Leave { .. } | Statement::Break { .. } | Statement::Continue { .. } | Statement::SelfDestruct { .. }) => { other.for_each_value_id_mut(&mut |id| *id = self.resolve_copy(*id)); vec![other] } } } /// Simplifies a region in place. /// /// Yields are resolved before outlining so a panic-pattern truncation can rescue any pure /// binding they still reference (otherwise the yield would dangle). fn simplify_region(&mut self, region: Region) -> Region { let outer_constants = self.constants.clone(); let outer_copies = self.copies.clone(); let outer_env_reads = self.env_reads.clone(); let mut statements = Vec::with_capacity(region.statements.len()); for statement in region.statements { let simplified = self.simplify_statement(statement); statements.extend(simplified); } let yields: Vec = region .yields .into_iter() .map(|yielded_value| self.resolve_value(yielded_value)) .collect(); let yielded = yields_as_used(&yields); statements = outline_panic_patterns(statements, &self.constants, &yielded); statements = outline_custom_error_patterns(statements, &self.constants); self.constants = outer_constants; self.copies = outer_copies; self.env_reads = outer_env_reads; Region { statements, yields } } /// Simplifies an expression, performing constant folding, algebraic identities, /// and copy propagation on operands. /// /// Among the algebraic identities are ones that recurse through a previously-recorded binary /// definition: `sub(add(a, b), a) → b`, `sub(add(a, b), b) → a`, `add(sub(a, b), b) → a`, and /// `add(b, sub(a, b)) → a`. These come up wherever a helper computes `tail = base + 32` and the /// caller takes `length = tail - base` (or the symmetric forms); recognising them lets the /// return_word peephole match against Solidity's inline encode helper output. fn simplify_expression(&mut self, expression: Expression) -> Expression { match expression { Expression::Binary { operation, lhs, rhs, } => { let lhs = self.resolve_value(lhs); let rhs = self.resolve_value(rhs); let lhs_value = self.try_get_const(&lhs); let rhs_value = self.try_get_const(&rhs); if let (Some(lhs_constant), Some(rhs_constant)) = (&lhs_value, &rhs_value) { if let Some(result) = fold_binary(operation, lhs_constant, rhs_constant) { self.statistics.constants_folded += 1; return Expression::Literal { value: result, value_type: result_type(operation), }; } } if let Some(simplified) = simplify_binary(operation, &lhs, &rhs, &lhs_value, &rhs_value) { self.statistics.identities_simplified += 1; return simplified; } if operation == BinaryOperation::Sub { if let Some(&(lhs_op, lhs_a, lhs_b)) = self.binary_definitions.get(&lhs.id.0) { if lhs_op == BinaryOperation::Add { let resolved_a = self.resolve_copy(lhs_a); let resolved_b = self.resolve_copy(lhs_b); if resolved_a == rhs.id { self.statistics.identities_simplified += 1; return Expression::Var(resolved_b); } if resolved_b == rhs.id { self.statistics.identities_simplified += 1; return Expression::Var(resolved_a); } } } } if operation == BinaryOperation::Add { if let Some(&(lhs_op, lhs_a, lhs_b)) = self.binary_definitions.get(&lhs.id.0) { if lhs_op == BinaryOperation::Sub { let resolved_a = self.resolve_copy(lhs_a); let resolved_b = self.resolve_copy(lhs_b); if resolved_b == rhs.id { self.statistics.identities_simplified += 1; return Expression::Var(resolved_a); } } } if let Some(&(rhs_op, rhs_a, rhs_b)) = self.binary_definitions.get(&rhs.id.0) { if rhs_op == BinaryOperation::Sub { let resolved_a = self.resolve_copy(rhs_a); let resolved_b = self.resolve_copy(rhs_b); if resolved_b == lhs.id { self.statistics.identities_simplified += 1; return Expression::Var(resolved_a); } } } } Expression::Binary { operation, lhs, rhs, } } Expression::Unary { operation, operand } => { let operand = self.resolve_value(operand); let operand_value = self.try_get_const(&operand); if let Some(constant) = &operand_value { if let Some(result) = fold_unary(operation, constant) { self.statistics.constants_folded += 1; return Expression::Literal { value: result, value_type: unary_result_type(operation), }; } } if operation == UnaryOperation::Not { if let Some((UnaryOperation::Not, inner)) = self.unary_definitions.get(&operand.id.0) { self.statistics.identities_simplified += 1; return Expression::Var(*inner); } } Expression::Unary { operation, operand } } Expression::Ternary { operation, a, b, n } => { let a = self.resolve_value(a); let b = self.resolve_value(b); let n = self.resolve_value(n); let a_value = self.try_get_const(&a); let b_value = self.try_get_const(&b); let n_value = self.try_get_const(&n); if let (Some(a_constant), Some(b_constant), Some(n_constant)) = (&a_value, &b_value, &n_value) { if let Some(result) = fold_ternary(operation, a_constant, b_constant, n_constant) { self.statistics.constants_folded += 1; return Expression::Literal { value: result, value_type: Type::Int(BitWidth::I256), }; } } Expression::Ternary { operation, a, b, n } } Expression::Var(id) => { let resolved = self.resolve_copy(id); Expression::Var(resolved) } Expression::MLoad { offset, region } => { let offset = self.resolve_value(offset); let region = if region == MemoryRegion::Unknown { self.resolve_region(&offset) } else { region }; Expression::MLoad { offset, region } } Expression::Address => self.cse_env_read(EnvRead::Address, expression), Expression::BaseFee => self.cse_env_read(EnvRead::BaseFee, expression), Expression::BlobBaseFee => self.cse_env_read(EnvRead::BlobBaseFee, expression), Expression::CallDataSize => self.cse_env_read(EnvRead::CallDataSize, expression), Expression::CallValue => self.cse_env_read(EnvRead::CallValue, expression), Expression::Caller => self.cse_env_read(EnvRead::Caller, expression), Expression::ChainId => self.cse_env_read(EnvRead::ChainId, expression), Expression::CodeSize => self.cse_env_read(EnvRead::CodeSize, expression), Expression::Coinbase => self.cse_env_read(EnvRead::Coinbase, expression), Expression::Difficulty => self.cse_env_read(EnvRead::Difficulty, expression), Expression::GasLimit => self.cse_env_read(EnvRead::GasLimit, expression), Expression::GasPrice => self.cse_env_read(EnvRead::GasPrice, expression), Expression::Number => self.cse_env_read(EnvRead::Number, expression), Expression::Origin => self.cse_env_read(EnvRead::Origin, expression), Expression::Timestamp => self.cse_env_read(EnvRead::Timestamp, expression), Expression::Keccak256Single { word0 } => { let word0 = self.resolve_value(word0); if let Some(constant) = self.try_get_const(&word0) { let result = fold_keccak256_single(&constant); self.statistics.constants_folded += 1; Expression::Literal { value: result, value_type: Type::Int(BitWidth::I256), } } else { Expression::Keccak256Single { word0 } } } Expression::Keccak256Pair { word0, word1 } => { let word0 = self.resolve_value(word0); let word1 = self.resolve_value(word1); if let (Some(constant0), Some(constant1)) = (self.try_get_const(&word0), self.try_get_const(&word1)) { let result = fold_keccak256_pair(&constant0, &constant1); self.statistics.constants_folded += 1; Expression::Literal { value: result, value_type: Type::Int(BitWidth::I256), } } else { Expression::Keccak256Pair { word0, word1 } } } Expression::MappingSLoad { key, slot } => Expression::MappingSLoad { key: self.resolve_value(key), slot: self.resolve_value(slot), }, other => other, } } /// Checks if an environment read has been cached and returns a Var reference /// to the first binding if so. Otherwise returns the original expression. fn cse_env_read(&mut self, kind: EnvRead, original: Expression) -> Expression { if let Some(&cached_id) = self.env_reads.get(&kind) { self.statistics.env_reads_eliminated += 1; Expression::Var(cached_id) } else { original } } /// Resolves a Value through copy propagation. fn resolve_value(&self, value: Value) -> Value { let resolved = self.resolve_copy(value.id); if resolved != value.id { Value { id: resolved, ..value } } else { value } } /// Resolves a ValueId through the copy chain. fn resolve_copy(&self, id: ValueId) -> ValueId { let mut current = id; for _ in 0..32 { if let Some(&target) = self.copies.get(¤t.0) { if target == current { break; } current = target; } else { break; } } current } /// Tries to get the constant value for a Value. fn try_get_const(&self, value: &Value) -> Option { let resolved = self.resolve_copy(value.id); self.constants.get(&resolved.0).cloned() } /// Emits the two-`Let` strength-reduction template: /// `let helper = const_value; let result = target_op(, )` /// where one operand of `target_op` is the freshly-bound `helper` and the /// other is `other_operand` (its position controlled by `helper_on_lhs`). fn emit_strength_reduce( &mut self, bindings: &[ValueId], target_op: BinaryOperation, const_value: BigUint, helper_on_lhs: bool, other_operand: Value, ) -> Vec { let helper_id = self.fresh_id(); self.constants.insert(helper_id.0, const_value.clone()); self.statistics.identities_simplified += 1; let helper_value = Value::int(helper_id); let (lhs, rhs) = if helper_on_lhs { (helper_value, other_operand) } else { (other_operand, helper_value) }; vec![ Statement::Let { bindings: vec![helper_id], value: Expression::Literal { value: const_value, value_type: Type::Int(BitWidth::I256), }, }, Statement::Let { bindings: bindings.to_vec(), value: Expression::Binary { operation: target_op, lhs, rhs, }, }, ] } /// Attempts strength reduction on a Let binding. Transforms: /// - `mul(x, 2^k)` or `mul(2^k, x)` → `shl(k, x)` /// - `div(x, 2^k)` → `shr(k, x)` (unsigned only) /// - `mod(x, 2^k)` → `and(x, 2^k - 1)` fn try_strength_reduce( &mut self, bindings: &[ValueId], expression: &Expression, ) -> Option> { let (operation, lhs, rhs) = match expression { Expression::Binary { operation, lhs, rhs, } => (*operation, *lhs, *rhs), _ => return None, }; let lhs_value = self.try_get_const(&lhs); let rhs_value = self.try_get_const(&rhs); let in_range = |exponent: u32| (1..256).contains(&exponent); match operation { BinaryOperation::Mul => { let (exponent, value) = if let Some(exponent) = rhs_value.as_ref().and_then(log2_exact) { (exponent, lhs) } else if let Some(exponent) = lhs_value.as_ref().and_then(log2_exact) { (exponent, rhs) } else { return None; }; in_range(exponent).then(|| { self.emit_strength_reduce( bindings, BinaryOperation::Shl, BigUint::from(exponent), true, value, ) }) } BinaryOperation::Div => { let exponent = rhs_value.as_ref().and_then(log2_exact)?; in_range(exponent).then(|| { self.emit_strength_reduce( bindings, BinaryOperation::Shr, BigUint::from(exponent), true, lhs, ) }) } BinaryOperation::Mod => { let exponent = rhs_value.as_ref().and_then(log2_exact)?; in_range(exponent).then(|| { let mask = (BigUint::one() << exponent) - BigUint::one(); self.emit_strength_reduce(bindings, BinaryOperation::And, mask, false, lhs) }) } _ => None, } } } /// Detects and replaces the Solidity panic revert pattern in a statement list. /// /// The pattern is a sequence of statements ending with: /// let bindings for constants, mstore(0, 0x4e487b71...), more let bindings, /// mstore(4, error_code), more let bindings, revert(0, 0x24) /// /// Each match is replaced with `PanicRevert { code }`, and the preceding /// Let bindings used only by the panic are left for DCE to remove. /// /// This function merges the caller's constant map with local Let-literal bindings /// to correctly resolve constants defined either in the current scope or in outer scopes /// (after copy propagation may have replaced local definitions with outer references). /// `extra_used` holds value ids that must stay defined even though they are not referenced by the /// statements themselves — an enclosing region's resolved `yields`. A pure binding inside the /// collapsed window may define such a value; truncating it would leave the yield dangling and fail /// SSA validation, so each is re-bound to zero before the [`Statement::PanicRevert`] (the binding is /// dead — the panic diverges — but dominates the yield, mirroring `eliminate_dead_code_in_statements`). fn outline_panic_patterns( statements: Vec, scope_constants: &BTreeMap, extra_used: &BTreeSet, ) -> Vec { let has_revert = statements .iter() .any(|statement| matches!(statement, Statement::Revert { .. })); if !has_revert { return statements; } let mut constants: BTreeMap = scope_constants.clone(); for statement in &statements { if let Statement::Let { bindings, value: Expression::Literal { value, .. }, } = statement { if bindings.len() == 1 { constants.insert(bindings[0].0, value.clone()); } } } let mut result = Vec::with_capacity(statements.len()); for statement in statements { if let Statement::Revert { ref offset, ref length, } = statement { if is_const_value(offset.id, 0, &constants) && is_const_value(length.id, 0x24, &constants) { if let Some((panic_start, error_code)) = find_panic_pattern_backwards(&result, &constants) { let mut rescued: Vec = Vec::new(); for statement in &result[panic_start..] { statement.for_each_value_id_def(&mut |id| { if extra_used.contains(&id.0) { rescued.push(id); } }); } result.truncate(panic_start); for id in rescued { result.push(Statement::Let { bindings: vec![id], value: Expression::Literal { value: BigUint::zero(), value_type: Type::Int(BitWidth::I256), }, }); } result.push(Statement::PanicRevert { code: error_code }); continue; } } } result.push(statement); } result } /// Looks backwards from the end of a statement list to find the panic pattern: /// mstore(0, panic_selector), [let bindings]*, mstore(4, code), [let bindings]* /// /// Returns `(start_index, error_code)` if the pattern is found. /// /// The simplifier emits `Statement::PanicRevert { code: u8 }` which encodes Solidity's canonical /// 36-byte panic revert with `code` zero-padded into the last byte. Only fold when the original /// mstored value actually fits in `u8` — i.e. ALL bits above the low byte are zero. Using /// `to_u64_digits().first()` here was unsound: it returned only the lowest u64 digit, so values /// like `2^124 + 0x42` were mis-classified as `code = 0x42` and the high bits (which the EVM emits /// literally into the revert data) were silently dropped. /// /// Last-write-wins: the matched selector/code stores must be the final writes to offsets 0/4, or /// the revert payload differs from canonical Panic. fn find_panic_pattern_backwards( statements: &[Statement], constants: &BTreeMap, ) -> Option<(usize, u8)> { let len = statements.len(); if len < 2 { return None; } let mut mstore4_index = None; let mut error_code = None; let search_limit = len.saturating_sub(10); for statement_index in (search_limit..len).rev() { match &statements[statement_index] { Statement::MStore { offset, value, .. } => { if is_const_value(offset.id, 4, constants) { if let Some(code_value) = constants.get(&value.id.0) { if code_value.bits() <= 8 { let code_u8 = code_value.to_u64_digits().first().copied().unwrap_or(0) as u8; mstore4_index = Some(statement_index); error_code = Some(code_u8); break; } } } } Statement::Let { .. } | Statement::Expression(..) => continue, _ => break, } } let mstore4_index = mstore4_index?; let error_code = error_code?; let search_limit2 = mstore4_index.saturating_sub(10); for statement_index in (search_limit2..mstore4_index).rev() { match &statements[statement_index] { Statement::MStore { offset, value, .. } => { if is_const_value(offset.id, 0, constants) { if let Some(selector_value) = constants.get(&value.id.0) { let selector_hex = format!("{selector_value:064x}"); if selector_hex == revive_common::PANIC_UINT256_SELECTOR_WORD_HEX && statements[statement_index..] .iter() .all(|statement| panic_window_droppable(statement, constants)) && !any_mstore_to_offset( &statements[statement_index + 1..], 0, constants, ) && !any_mstore_to_offset(&statements[mstore4_index + 1..], 4, constants) { return Some((statement_index, error_code)); } } } } Statement::Let { .. } | Statement::Expression(..) => continue, _ => break, } } None } /// Whether `statement` may be dropped when collapsing a panic-revert window into a `PanicRevert`. /// /// - An `MStore` writes 32 bytes at `[p, p + 32)`; for `p < 0x24` those overlap the revert range /// `[0, 0x24)` that the EVM emits as revert data. The canonical panic shape stores only at `p = 0` /// (selector) and `p = 4` (code); any other in-range store (e.g. `mstore(7, …)`) would corrupt the /// payload, so it blocks the collapse. Stores at `p >= 0x24` don't escape and are safe. /// - A `Let`/`Expression` is droppable only if its value is pure. A side-effecting value — an /// internal call that can revert differently / never return / write scratch, or a memory/storage /// read — is observable, so dropping it would change behavior. This matches `eliminate_dead_code`, /// which never removes effectful statements. A dropped pure binding that an enclosing yield still /// needs is zero-rebound by [`outline_panic_patterns`]. fn panic_window_droppable(statement: &Statement, constants: &BTreeMap) -> bool { match statement { Statement::MStore { offset, .. } => { is_const_value(offset.id, 0, constants) || is_const_value(offset.id, 4, constants) || constants.get(&offset.id.0).is_some_and(|offset_value| { offset_value .to_u64() .is_some_and(|offset_u64| offset_u64 >= 0x24) }) } Statement::Let { value, .. } | Statement::Expression(value) => { !expr_has_side_effects(value) } _ => false, } } /// Whether any of `statements` is an `mstore` to the constant byte `offset`. Used to confirm a /// matched panic selector/code store is the last write to its offset (last-write-wins). fn any_mstore_to_offset( statements: &[Statement], offset: u64, constants: &BTreeMap, ) -> bool { statements.iter().any(|statement| { matches!(statement, Statement::MStore { offset: store_offset, .. } if is_const_value(store_offset.id, offset, constants)) }) } /// Checks if a ValueId maps to a specific constant value. fn is_const_value(id: ValueId, expected: u64, constants: &BTreeMap) -> bool { constants .get(&id.0) .is_some_and(|value| *value == BigUint::from(expected)) } /// Outlines custom error revert patterns in a statement list. /// /// Detects the pattern: mstore(0, selector) [+ mstore(4, arg0) + mstore(0x24, arg1) + ...] + revert(0, 4+32*N) /// where the selector is a constant and the revert uses scratch space (offset 0). /// Replaces matched patterns with `CustomErrorRevert { selector, arguments }`. fn outline_custom_error_patterns( statements: Vec, scope_constants: &BTreeMap, ) -> Vec { let has_revert = statements .iter() .any(|statement| matches!(statement, Statement::Revert { .. })); if !has_revert { return statements; } let mut constants: BTreeMap = scope_constants.clone(); for statement in &statements { if let Statement::Let { bindings, value: Expression::Literal { value, .. }, } = statement { if bindings.len() == 1 { constants.insert(bindings[0].0, value.clone()); } } } let zero = BigUint::ZERO; let mut result = Vec::with_capacity(statements.len()); for statement in statements { if let Statement::Revert { ref offset, ref length, } = statement { if constants .get(&offset.id.0) .is_some_and(|offset_constant| *offset_constant == zero) { if let Some(total_len) = constants .get(&length.id.0) .and_then(|length_constant| length_constant.to_u64()) { let num_arguments = if total_len == 4 { Some(0usize) } else if total_len >= 0x24 && (total_len - 4) % 0x20 == 0 { Some(((total_len - 4) / 0x20) as usize) } else { None }; if let Some(num_arguments) = num_arguments { if let Some((start_index, selector, arguments)) = find_custom_error_pattern_backwards(&result, &constants, num_arguments) { let mut kept = Vec::new(); for statement in result.drain(start_index..) { match statement { Statement::MStore { .. } => {} _ => kept.push(statement), } } result.extend(kept); result.push(Statement::CustomErrorRevert { selector, arguments, }); continue; } } } } } result.push(statement); } result } /// Looks backwards from the end of a statement list to find the custom error pattern. /// /// Searches for: /// 1. mstore(0, selector) at scratch space — selector is a constant /// 2. mstore(4, arg0) — first argument (optional) /// 3. mstore(0x24, arg1) — second argument (optional) /// 4. mstore(0x44, arg2) — third argument (optional) /// /// Returns `(start_index, selector, arguments)` if found. /// /// The scan records each payload offset's store and tracks `earliest_index` (the start of the matched /// window). It deliberately does NOT try to keep the latest store per offset: that would be unsound /// for overlapping unaligned stores, and it would also skip earlier duplicates so `earliest_index` /// would no longer cover them. Instead the exactly-once check rejects any window with a duplicate /// payload-offset store, so a recorded value is only ever used when its offset has a single store. /// /// The revert range is `[0, 4 + 32 * num_arguments)`. The canonical Solidity custom-error shape writes /// at exactly the offsets `0`, `4`, `0x24`, … `4 + 32 * (num_arguments - 1)`. Any in-range mstore at a /// different offset corrupts the revert payload — we must NOT collapse the sequence into /// `Statement::CustomErrorRevert`. Out-of-range mstores (offset ≥ revert_length) are fine; they /// don't escape. /// /// Last-write-wins: each payload offset (selector at 0, arguments at 4, 0x24, …) must be written /// exactly once in the collapsed window. A repeated store means a later write overwrites an earlier /// one — so the value the backward scan matched is not what the EVM revert emits — and the sequence /// must not be collapsed into a `CustomErrorRevert`. fn find_custom_error_pattern_backwards( statements: &[Statement], constants: &BTreeMap, num_arguments: usize, ) -> Option<(usize, BigUint, Vec)> { let len = statements.len(); if len < 1 + num_arguments { return None; } let mut found_selector: Option = None; let mut arguments: Vec> = vec![None; num_arguments]; let mut earliest_index = len; let zero = BigUint::ZERO; let four = BigUint::from(4u32); let search_limit = len.saturating_sub(20); for statement_index in (search_limit..len).rev() { match &statements[statement_index] { Statement::MStore { offset, value, .. } => { if let Some(offset_value) = constants.get(&offset.id.0) { if *offset_value == zero { if let Some(selector_constant) = constants.get(&value.id.0) { found_selector = Some(selector_constant.clone()); earliest_index = earliest_index.min(statement_index); } } else if *offset_value == four && num_arguments >= 1 { arguments[0] = Some(*value); earliest_index = earliest_index.min(statement_index); } else if let Some(offset_u64) = offset_value.to_u64() { if offset_u64 >= 0x24 && (offset_u64 - 4) % 0x20 == 0 { let argument_index = ((offset_u64 - 4) / 0x20) as usize; if argument_index < num_arguments { arguments[argument_index] = Some(*value); earliest_index = earliest_index.min(statement_index); } } } } } Statement::Let { .. } | Statement::Expression(..) => continue, _ => break, } } let selector = found_selector?; let arguments: Vec = arguments.into_iter().collect::>>()?; let revert_length = 4 + (num_arguments as u64) * 0x20; let all_safe = statements[earliest_index..] .iter() .all(|statement| match statement { Statement::MStore { offset, .. } => { constants.get(&offset.id.0).is_some_and(|offset_value| { if let Some(offset_u64) = offset_value.to_u64() { if offset_u64 >= revert_length { return true; } if offset_u64 == 0 { return true; } offset_u64 >= 4 && (offset_u64 - 4) % 0x20 == 0 } else { false } }) } Statement::Let { .. } | Statement::Expression(..) => true, _ => false, }); if !all_safe { return None; } let payload_offsets = std::iter::once(0u64).chain((0..num_arguments as u64).map(|index| 4 + index * 0x20)); for payload_offset in payload_offsets { let writes = statements[earliest_index..] .iter() .filter(|statement| { matches!(statement, Statement::MStore { offset, .. } if is_const_value(offset.id, payload_offset, constants)) }) .count(); if writes != 1 { return None; } } Some((earliest_index, selector, arguments)) } /// Returns the result type for a binary operation. fn result_type(operation: BinaryOperation) -> Type { match operation { BinaryOperation::Lt | BinaryOperation::Gt | BinaryOperation::Slt | BinaryOperation::Sgt | BinaryOperation::Eq => Type::Int(BitWidth::I256), _ => Type::Int(BitWidth::I256), } } /// Returns the result type for a unary operation. fn unary_result_type(operation: UnaryOperation) -> Type { match operation { UnaryOperation::IsZero => Type::Int(BitWidth::I256), UnaryOperation::Not | UnaryOperation::Clz => Type::Int(BitWidth::I256), } } /// Folds a binary operation on two constant values. /// Returns None if the operation cannot be folded. fn fold_binary(operation: BinaryOperation, a: &BigUint, b: &BigUint) -> Option { let modulus = modulus_u256(); let max = max_u256(); Some(match operation { BinaryOperation::Add => (a + b) % &modulus, BinaryOperation::Sub => { if a >= b { a - b } else { &modulus - (b - a) } } BinaryOperation::Mul => (a * b) % &modulus, BinaryOperation::Div => { if b.is_zero() { BigUint::zero() } else { a / b } } BinaryOperation::SDiv => { if b.is_zero() { BigUint::zero() } else { fold_sdiv(a, b, &modulus)? } } BinaryOperation::Mod => { if b.is_zero() { BigUint::zero() } else { a % b } } BinaryOperation::SMod => { if b.is_zero() { BigUint::zero() } else { fold_smod(a, b, &modulus)? } } BinaryOperation::Exp => a.modpow(b, &modulus), BinaryOperation::And => a & b, BinaryOperation::Or => a | b, BinaryOperation::Xor => a ^ b, BinaryOperation::Shl => { if *a >= BigUint::from(256u32) { BigUint::zero() } else { let shift = a.to_u64_digits().first().copied().unwrap_or(0) as usize; (b << shift) % &modulus } } BinaryOperation::Shr => { if *a >= BigUint::from(256u32) { BigUint::zero() } else { let shift = a.to_u64_digits().first().copied().unwrap_or(0) as usize; b >> shift } } BinaryOperation::Sar => fold_sar(b, a, &modulus, &max)?, BinaryOperation::Lt => bool_to_u256(a < b), BinaryOperation::Gt => bool_to_u256(a > b), BinaryOperation::Eq => bool_to_u256(a == b), BinaryOperation::Slt => { let a_signed = is_negative(a, &modulus); let b_signed = is_negative(b, &modulus); match (a_signed, b_signed) { (true, false) => bool_to_u256(true), (false, true) => bool_to_u256(false), _ => bool_to_u256(a < b), } } BinaryOperation::Sgt => { let a_signed = is_negative(a, &modulus); let b_signed = is_negative(b, &modulus); match (a_signed, b_signed) { (true, false) => bool_to_u256(false), (false, true) => bool_to_u256(true), _ => bool_to_u256(a > b), } } BinaryOperation::Byte => { if *a >= BigUint::from(32u32) { BigUint::zero() } else { let byte_index = a.to_u64_digits().first().copied().unwrap_or(0) as usize; let shift = (31 - byte_index) * 8; (b >> shift) & BigUint::from(0xffu32) } } BinaryOperation::SignExtend => fold_signextend(a, b, &max)?, BinaryOperation::AddMod | BinaryOperation::MulMod => return None, }) } /// Folds a unary operation on a constant value. fn fold_unary(operation: UnaryOperation, a: &BigUint) -> Option { Some(match operation { UnaryOperation::IsZero => bool_to_u256(a.is_zero()), UnaryOperation::Not => &max_u256() ^ a, UnaryOperation::Clz => { if a.is_zero() { BigUint::from(256u32) } else { let bits = a.bits(); BigUint::from(256u64 - bits) } } }) } /// Folds a ternary operation (addmod, mulmod). fn fold_ternary( operation: BinaryOperation, a: &BigUint, b: &BigUint, n: &BigUint, ) -> Option { if n.is_zero() { return Some(BigUint::zero()); } Some(match operation { BinaryOperation::AddMod => (a + b) % n, BinaryOperation::MulMod => (a * b) % n, _ => return None, }) } /// Encodes a BigUint as a 32-byte big-endian buffer (left-padded with zeros). fn biguint_to_be32(value: &BigUint) -> [u8; 32] { let bytes = value.to_bytes_be(); let mut buffer = [0u8; 32]; let start = 32usize.saturating_sub(bytes.len()); buffer[start..].copy_from_slice(&bytes[bytes.len().saturating_sub(32)..]); buffer } /// Computes keccak256 of a single 256-bit word at compile time. fn fold_keccak256_single(word0: &BigUint) -> BigUint { let buffer = biguint_to_be32(word0); let hash = revive_common::Keccak256::from_slice(&buffer); BigUint::from_bytes_be(hash.as_bytes()) } /// Computes keccak256 of two 256-bit words at compile time. fn fold_keccak256_pair(word0: &BigUint, word1: &BigUint) -> BigUint { let mut buffer = [0u8; 64]; buffer[..32].copy_from_slice(&biguint_to_be32(word0)); buffer[32..].copy_from_slice(&biguint_to_be32(word1)); let hash = revive_common::Keccak256::from_slice(&buffer); BigUint::from_bytes_be(hash.as_bytes()) } /// Returns the power-of-2 exponent if the value is a power of 2. /// E.g., 1 -> Some(0), 2 -> Some(1), 4 -> Some(2), 32 -> Some(5), 256 -> Some(8). fn log2_exact(value: &BigUint) -> Option { if value.is_zero() || !((value - BigUint::one()) & value).is_zero() { return None; } Some((value.bits() - 1) as u32) } /// Applies algebraic identity simplifications. /// Returns Some(simplified_expr) if an identity applies, None otherwise. /// /// `div(x, x) → 1` and `sdiv(x, x) → 1` are unsound when `x == 0`. EVM defines division by zero to /// return 0, so `div(0, 0) = 0`, not 1. We can only fold the same-operand case when we statically /// know `x != 0`. fn simplify_binary( operation: BinaryOperation, lhs: &Value, rhs: &Value, lhs_value: &Option, rhs_value: &Option, ) -> Option { let i256 = Type::Int(BitWidth::I256); let literal = |value: BigUint| Expression::Literal { value, value_type: i256, }; let var_l = || Expression::Var(lhs.id); let var_r = || Expression::Var(rhs.id); let same = lhs.id == rhs.id; let l_is = |value: &BigUint| lhs_value.as_ref() == Some(value); let r_is = |value: &BigUint| rhs_value.as_ref() == Some(value); let zero = BigUint::zero(); let one = BigUint::one(); match operation { BinaryOperation::Add => { if r_is(&zero) { Some(var_l()) } else if l_is(&zero) { Some(var_r()) } else { None } } BinaryOperation::Sub if r_is(&zero) => Some(var_l()), BinaryOperation::Sub if same => Some(literal(zero)), BinaryOperation::Sub => None, BinaryOperation::Mul if r_is(&zero) || l_is(&zero) => Some(literal(zero)), BinaryOperation::Mul if r_is(&one) => Some(var_l()), BinaryOperation::Mul if l_is(&one) => Some(var_r()), BinaryOperation::Mul => None, BinaryOperation::Div | BinaryOperation::SDiv if r_is(&one) => Some(var_l()), BinaryOperation::Div | BinaryOperation::SDiv if l_is(&zero) => Some(literal(zero)), BinaryOperation::Div | BinaryOperation::SDiv if same && lhs_value.as_ref().is_some_and(|value| !value.is_zero()) => { Some(literal(one)) } BinaryOperation::Div | BinaryOperation::SDiv => None, BinaryOperation::Mod | BinaryOperation::SMod if r_is(&one) || l_is(&zero) || same => { Some(literal(zero)) } BinaryOperation::Mod | BinaryOperation::SMod => None, BinaryOperation::And if r_is(&zero) || l_is(&zero) => Some(literal(zero)), BinaryOperation::And if r_is(&max_u256()) || same => Some(var_l()), BinaryOperation::And if l_is(&max_u256()) => Some(var_r()), BinaryOperation::And => None, BinaryOperation::Or if r_is(&zero) || same => Some(var_l()), BinaryOperation::Or if l_is(&zero) => Some(var_r()), BinaryOperation::Or if r_is(&max_u256()) || l_is(&max_u256()) => Some(literal(max_u256())), BinaryOperation::Or => None, BinaryOperation::Xor if r_is(&zero) => Some(var_l()), BinaryOperation::Xor if l_is(&zero) => Some(var_r()), BinaryOperation::Xor if same => Some(literal(zero)), BinaryOperation::Xor => None, BinaryOperation::Shl | BinaryOperation::Shr | BinaryOperation::Sar if l_is(&zero) => { Some(var_r()) } BinaryOperation::Shl | BinaryOperation::Shr | BinaryOperation::Sar => None, BinaryOperation::Eq if same => Some(literal(one)), BinaryOperation::Eq => None, BinaryOperation::Lt | BinaryOperation::Gt | BinaryOperation::Slt | BinaryOperation::Sgt if same => { Some(literal(zero)) } BinaryOperation::Lt | BinaryOperation::Gt | BinaryOperation::Slt | BinaryOperation::Sgt => { None } _ => None, } } /// Helper: checks if a 256-bit value is negative in two's complement. fn is_negative(value: &BigUint, modulus: &BigUint) -> bool { *value >= (modulus >> 1) } /// Helper: converts a boolean to a 256-bit value (0 or 1). fn bool_to_u256(b: bool) -> BigUint { if b { BigUint::one() } else { BigUint::zero() } } /// Helper: signed division for 256-bit values. fn fold_sdiv(a: &BigUint, b: &BigUint, modulus: &BigUint) -> Option { let half = modulus >> 1; let a_neg = *a >= half; let b_neg = *b >= half; let abs_a = if a_neg { modulus - a } else { a.clone() }; let abs_b = if b_neg { modulus - b } else { b.clone() }; if abs_b.is_zero() { return Some(BigUint::zero()); } let result = &abs_a / &abs_b; if a_neg != b_neg && !result.is_zero() { Some(modulus - &result) } else { Some(result) } } /// Helper: signed modulo for 256-bit values. fn fold_smod(a: &BigUint, b: &BigUint, modulus: &BigUint) -> Option { let half = modulus >> 1; let a_neg = *a >= half; let b_neg = *b >= half; let abs_a = if a_neg { modulus - a } else { a.clone() }; let abs_b = if b_neg { modulus - b } else { b.clone() }; if abs_b.is_zero() { return Some(BigUint::zero()); } let result = &abs_a % &abs_b; if a_neg && !result.is_zero() { Some(modulus - &result) } else { Some(result) } } /// Helper: arithmetic shift right for 256-bit values. fn fold_sar(a: &BigUint, b: &BigUint, modulus: &BigUint, max: &BigUint) -> Option { if *b >= BigUint::from(256u32) { let half = modulus >> 1; return if *a >= half { Some(max.clone()) } else { Some(BigUint::zero()) }; } let shift = b.to_u64_digits().first().copied().unwrap_or(0) as usize; let half = modulus >> 1; if *a >= half { let shifted = a >> shift; let fill = (max >> (256 - shift)) << (256 - shift); Some((shifted | fill) % modulus) } else { Some(a >> shift) } } /// Helper: EVM signextend(b, x) operation. fn fold_signextend(b: &BigUint, x: &BigUint, max: &BigUint) -> Option { if *b >= BigUint::from(31u32) { return Some(x.clone()); } let byte_position = b.to_u64_digits().first().copied().unwrap_or(0) as usize; let bit_position = byte_position * 8 + 7; let sign_bit = (x >> bit_position) & BigUint::one(); if sign_bit.is_one() { let mask = max << (bit_position + 1); Some((x | mask) & max) } else { let mask = (BigUint::one() << (bit_position + 1)) - BigUint::one(); Some(x & mask) } } /// Checks if an expression has side effects (cannot be dead-code eliminated). fn expr_has_side_effects(expression: &Expression) -> bool { matches!( expression, Expression::Call { .. } | Expression::Keccak256 { .. } | Expression::Keccak256Pair { .. } | Expression::Keccak256Single { .. } | Expression::MLoad { .. } | Expression::SLoad { .. } | Expression::TLoad { .. } | Expression::MappingSLoad { .. } | Expression::MSize ) } /// Eliminates dead Let bindings from a list of statements. /// Uses bottom-up recursive DCE: first cleans nested regions, then this level. /// Iterates at each level until fixpoint (no more removals). /// /// `extra_used` contains ValueIds that must be preserved even if not referenced /// by the statements themselves (e.g., function return values, region yields). /// /// Statements following an unconditional terminator are unreachable and get /// truncated. Some of them may still define a value that an enclosing `yield` or /// return references through `extra_used`: folding a constant `if` whose taken /// branch ends in a `leave` appends that branch's output binding after the `leave`, /// and the enclosing region's `yield` keeps pointing at it. Truncating the binding /// would leave that reference dangling and fail SSA validation ("used before /// definition"). Each such value is therefore re-bound to zero just before the /// terminator. The binding lies on a provably unreachable path (it follows an /// unconditional terminator), so the zero is never observed and LLVM discards it. fn eliminate_dead_code_in_statements( statements: &mut Vec, extra_used: &BTreeSet, ) -> usize { let mut total_removed = 0; if let Some(terminator_position) = statements.iter().position(is_terminator) { let unreachable_count = statements.len() - terminator_position - 1; if unreachable_count > 0 { let mut rescued: Vec = Vec::new(); for statement in &statements[terminator_position + 1..] { statement.for_each_value_id_def(&mut |id| { if extra_used.contains(&id.0) { rescued.push(id); } }); } statements.truncate(terminator_position + 1); for id in rescued { statements.insert( terminator_position, Statement::Let { bindings: vec![id], value: Expression::Literal { value: BigUint::zero(), value_type: Type::Int(BitWidth::I256), }, }, ); } total_removed += unreachable_count; } } for statement in statements.iter_mut() { total_removed += eliminate_dead_code_in_nested(statement, extra_used); } loop { let mut used = extra_used.clone(); for statement in statements.iter() { statement.for_each_value_id(&mut |id| { used.insert(id.0); }); } let before = statements.len(); statements.retain(|statement| { if let Statement::Let { bindings, value } = statement { let all_unused = bindings.iter().all(|id| !used.contains(&id.0)); if all_unused && !expr_has_side_effects(value) { return false; } } if let Statement::Expression(expression) = statement { if !expr_has_side_effects(expression) { return false; } } true }); let removed = before - statements.len(); total_removed += removed; if removed == 0 { break; } } total_removed } /// Recursively DCE inside nested regions of a statement. /// `parent_extra_used` contains ValueIds from the parent scope that must be preserved /// (e.g., function return values). These are propagated into Block regions because /// Blocks in Yul can define values that are referenced by the parent scope. fn eliminate_dead_code_in_nested( statement: &mut Statement, parent_extra_used: &BTreeSet, ) -> usize { match statement { Statement::If { then_region, else_region, .. } => { let mut removed = 0; let extra = yields_as_used(&then_region.yields); removed += eliminate_dead_code_in_statements(&mut then_region.statements, &extra); if let Some(else_region_body) = else_region { let extra = yields_as_used(&else_region_body.yields); removed += eliminate_dead_code_in_statements(&mut else_region_body.statements, &extra); } removed } Statement::Switch { cases, default, .. } => { let mut removed = 0; for case in cases { let extra = yields_as_used(&case.body.yields); removed += eliminate_dead_code_in_statements(&mut case.body.statements, &extra); } if let Some(default_region) = default { let extra = yields_as_used(&default_region.yields); removed += eliminate_dead_code_in_statements(&mut default_region.statements, &extra); } removed } Statement::Block(region) => { let mut extra = yields_as_used(®ion.yields); extra.extend(parent_extra_used); eliminate_dead_code_in_statements(&mut region.statements, &extra) } _ => 0, } } /// Checks if a statement is a control-flow terminator (no fall-through). fn is_terminator(statement: &Statement) -> bool { matches!( statement, Statement::Revert { .. } | Statement::Return { .. } | Statement::Stop | Statement::Invalid | Statement::PanicRevert { .. } | Statement::ErrorStringRevert { .. } | Statement::CustomErrorRevert { .. } | Statement::Leave { .. } | Statement::SelfDestruct { .. } ) } /// Checks if a statement list starts with the two-statement pattern: /// `let tmp = callvalue()` /// `if tmp { ... revert(...) ... }` /// This pattern is generated by Solidity for non-payable function checks. /// The then_region must contain a Revert (possibly preceded by Let bindings /// for the offset/length arguments and followed by an unreachable marker). fn has_callvalue_revert_prefix(statements: &[Statement]) -> bool { if statements.len() < 2 { return false; } let callvalue_id = match &statements[0] { Statement::Let { bindings, value: Expression::CallValue, } if bindings.len() == 1 => Some(bindings[0]), _ => None, }; let Some(callvalue_binding) = callvalue_id else { return false; }; match &statements[1] { Statement::If { condition, inputs, then_region, else_region, outputs, } => { if condition.id != callvalue_binding { return false; } if !inputs.is_empty() || !outputs.is_empty() { return false; } if else_region.is_some() { return false; } then_region .statements .iter() .any(|statement| matches!(statement, Statement::Revert { .. })) } _ => false, } } /// Whether the leading callvalue-revert check may be hoisted above a switch and dropped from every /// branch (replacing N per-branch checks with one). Sound only when: /// 1. every case AND the default already start with the check. Otherwise the no-match path — a /// present default, or, when none, the post-switch fall-through — would gain a spurious /// callvalue revert it never performed (e.g. an all-nonpayable dispatch with a payable /// fallback). A missing or non-checking default is exactly such a path. /// 2. every branch reverts with identical data, so the one hoisted revert serves all selectors; /// otherwise a matched selector would receive the wrong revert payload. Compared via the /// canonical (SSA-renamed, literal-preserving) prefix encoding. fn callvalue_check_fully_hoistable(cases: &[SwitchCase], default: &Option) -> bool { if cases.is_empty() { return false; } let all_branches_check = cases .iter() .all(|case| has_callvalue_revert_prefix(&case.body.statements)) && default .as_ref() .is_some_and(|default_region| has_callvalue_revert_prefix(&default_region.statements)); if !all_branches_check { return false; } let reference = canonical_callvalue_prefix(&cases[0].body.statements); cases .iter() .all(|case| canonical_callvalue_prefix(&case.body.statements) == reference) && default.as_ref().is_some_and(|default_region| { canonical_callvalue_prefix(&default_region.statements) == reference }) } /// Returns the value id bound by the leading `let vN = callvalue()`. /// /// The caller must have established [`has_callvalue_revert_prefix`] for `statements`. fn callvalue_binding_id(statements: &[Statement]) -> ValueId { match &statements[0] { Statement::Let { bindings, .. } => bindings[0], _ => unreachable!("ICE: callvalue prefix guaranteed by has_callvalue_revert_prefix"), } } /// Canonical encoding (SSA ids renamed to first-use order, literal values preserved) of the /// two-statement callvalue-revert prefix. Two prefixes with equal encodings revert with identical /// data, so hoisting one above a switch in place of all of them is observationally sound. /// /// The caller must have established [`has_callvalue_revert_prefix`] for `statements`. fn canonical_callvalue_prefix(statements: &[Statement]) -> Vec { let mut canon = Canonicalizer::new(); let mut buffer = Vec::new(); canon.encode_statement(&statements[0], &mut buffer); canon.encode_statement(&statements[1], &mut buffer); buffer } /// Drops the leading two-statement callvalue-revert prefix and rewrites any remaining use of its /// (now-undefined) callvalue binding to `hoisted_callvalue`, the binding hoisted above the switch. /// /// Environment CSE may have rewritten a later `callvalue()` in this branch to reuse the prefix's /// binding; after the prefix is drained that use would reference an undefined value and trip the /// validator unless it is redirected to the hoisted binding. The caller must have established /// [`has_callvalue_revert_prefix`] for `statements`. fn drain_callvalue_prefix(statements: &mut Vec, hoisted_callvalue: ValueId) { let local_callvalue = callvalue_binding_id(statements); statements.drain(0..2); if local_callvalue != hoisted_callvalue { for statement in statements.iter_mut() { statement.for_each_value_id_mut(&mut |id| { if *id == local_callvalue { *id = hoisted_callvalue; } }); } } } /// Checks if a statement list starts with `let vN = callvalue()`. /// Used for partial callvalue read hoisting: we only need the first statement /// to be a callvalue binding, not the full callvalue-revert pattern. fn starts_with_callvalue_let(statements: &[Statement]) -> bool { matches!( statements.first(), Some(Statement::Let { bindings, value: Expression::CallValue, }) if bindings.len() == 1 ) } /// Replaces the first statement's `callvalue()` expression with `Var(replacement_id)` /// if the first statement is `let vN = callvalue()`. This turns the syscall read /// into a copy of the already-hoisted value. fn replace_leading_callvalue_with_var(statements: &mut [Statement], replacement_id: ValueId) { if let Some(Statement::Let { bindings, value: value @ Expression::CallValue, }) = statements.first_mut() { if bindings.len() == 1 { *value = Expression::Var(replacement_id); } } } /// Collects ValueIds from region yields into a "used" set. fn yields_as_used(yields: &[Value]) -> BTreeSet { let mut used = BTreeSet::new(); for yielded_value in yields { used.insert(yielded_value.id.0); } used } /// Minimum function size for exact dedup. A body this small lowers to a /// handful of instructions — comparable to a call's own per-site overhead — /// so collapsing duplicates into a shared callable would cost more than it /// saves. const MIN_DEDUP_SIZE: usize = 4; /// Deduplicates functions with identical bodies in an object. /// /// Two functions are considered duplicates if they have: /// - Same number and types of parameters /// - Same number and types of return values /// - Structurally identical bodies (alpha-equivalent, ignoring ValueId numbering) /// /// When duplicates are found, all calls to the duplicate are redirected to the /// canonical (first-seen) function, and the duplicate is removed. /// /// Returns the number of functions removed. pub fn deduplicate_functions(object: &mut Object) -> usize { if object.functions.len() < 2 { return 0; } let mut canonical_to_id: BTreeMap, FunctionId> = BTreeMap::new(); let mut redirects: BTreeMap = BTreeMap::new(); for function in object.functions.values() { if function.size_estimate < MIN_DEDUP_SIZE { continue; } let signature = ( function .parameters .iter() .map(|(_, value_type)| *value_type) .collect::>(), function.returns.clone(), ); let canonical = canonicalize_function(function, &signature.0, &signature.1); if let Some(&canonical_id) = canonical_to_id.get(&canonical) { redirects.insert(function.id, canonical_id); } else { canonical_to_id.insert(canonical, function.id); } } if redirects.is_empty() { return 0; } let removed_count = redirects.len(); redirect_calls_in_block(&mut object.code, &redirects); for function in object.functions.values_mut() { redirect_calls_in_block(&mut function.body, &redirects); } // Fold each duplicate's call sites into its canonical so call_count stays accurate // for the NoInline decision in set_inline_attributes. let mut merged_calls: BTreeMap = BTreeMap::new(); for (duplicate_id, canonical_id) in &redirects { let calls = object .functions .get(duplicate_id) .map_or(0, |function| function.call_count); *merged_calls.entry(*canonical_id).or_default() += calls; } for (canonical_id, calls) in merged_calls { if let Some(canonical) = object.functions.get_mut(&canonical_id) { canonical.call_count += calls; } } for duplicate_id in redirects.keys() { object.functions.remove(duplicate_id); } removed_count } /// Produces a canonical byte representation of a function body for comparison. /// ValueIds are renumbered sequentially (0, 1, 2, ...) in order of first occurrence. /// FunctionIds are preserved (they're global references, not local SSA). fn canonicalize_function( function: &crate::ir::Function, parameter_types: &[Type], return_types: &[Type], ) -> Vec { let mut canon = Canonicalizer::new(); let mut buffer = Vec::new(); buffer.push(parameter_types.len() as u8); for value_type in parameter_types { buffer.push(type_tag(value_type)); } buffer.push(return_types.len() as u8); for value_type in return_types { buffer.push(type_tag(value_type)); } for (parameter_id, _) in &function.parameters { canon.get_or_insert(*parameter_id); } for return_value_id in &function.return_values_initial { canon.get_or_insert(*return_value_id); } for statement in &function.body.statements { canon.encode_statement(statement, &mut buffer); } buffer.push(0xFE); for return_value_id in &function.return_values { canon.encode_value_id(*return_value_id, &mut buffer); } buffer } struct Canonicalizer { id_map: BTreeMap, next_id: u32, } impl Canonicalizer { fn new() -> Self { Canonicalizer { id_map: BTreeMap::new(), next_id: 0, } } fn get_or_insert(&mut self, id: ValueId) -> u32 { *self.id_map.entry(id.0).or_insert_with(|| { let assigned_index = self.next_id; self.next_id += 1; assigned_index }) } fn encode_value_id(&mut self, id: ValueId, buffer: &mut Vec) { let canonical = self.get_or_insert(id); buffer.extend_from_slice(&canonical.to_le_bytes()); } fn encode_value(&mut self, value: &Value, buffer: &mut Vec) { self.encode_value_id(value.id, buffer); buffer.push(type_tag(&value.value_type)); } fn encode_expression(&mut self, expression: &Expression, buffer: &mut Vec) { match expression { Expression::Literal { value, value_type } => { buffer.push(0x01); let bytes = value.to_bytes_le(); buffer.extend_from_slice(&(bytes.len() as u16).to_le_bytes()); buffer.extend_from_slice(&bytes); buffer.push(type_tag(value_type)); } Expression::Var(id) => { buffer.push(0x02); self.encode_value_id(*id, buffer); } Expression::Binary { operation, lhs, rhs, } => { buffer.push(0x03); buffer.push(binop_tag(*operation)); self.encode_value(lhs, buffer); self.encode_value(rhs, buffer); } Expression::Ternary { operation, a, b, n } => { buffer.push(0x04); buffer.push(binop_tag(*operation)); self.encode_value(a, buffer); self.encode_value(b, buffer); self.encode_value(n, buffer); } Expression::Unary { operation, operand } => { buffer.push(0x05); buffer.push(unaryop_tag(*operation)); self.encode_value(operand, buffer); } Expression::Call { function, arguments, } => { buffer.push(0x06); buffer.extend_from_slice(&function.0.to_le_bytes()); buffer.push(arguments.len() as u8); for argument in arguments { self.encode_value(argument, buffer); } } Expression::CallDataLoad { offset } => { buffer.push(0x10); self.encode_value(offset, buffer); } Expression::MLoad { offset, region } => { buffer.push(0x11); self.encode_value(offset, buffer); buffer.push(region_tag(region)); } Expression::SLoad { key, static_slot } => { buffer.push(0x12); self.encode_value(key, buffer); if let Some(slot) = static_slot { buffer.push(1); let bytes = slot.to_bytes_le(); buffer.extend_from_slice(&(bytes.len() as u16).to_le_bytes()); buffer.extend_from_slice(&bytes); } else { buffer.push(0); } } Expression::TLoad { key } => { buffer.push(0x13); self.encode_value(key, buffer); } Expression::Keccak256 { offset, length } => { buffer.push(0x14); self.encode_value(offset, buffer); self.encode_value(length, buffer); } Expression::Keccak256Pair { word0, word1 } => { buffer.push(0x24); self.encode_value(word0, buffer); self.encode_value(word1, buffer); } Expression::Keccak256Single { word0 } => { buffer.push(0x25); self.encode_value(word0, buffer); } Expression::MappingSLoad { key, slot } => { buffer.push(0x27); self.encode_value(key, buffer); self.encode_value(slot, buffer); } Expression::Truncate { value, to } => { buffer.push(0x15); self.encode_value(value, buffer); buffer.push(bitwidth_tag(*to)); } Expression::ZeroExtend { value, to } => { buffer.push(0x16); self.encode_value(value, buffer); buffer.push(bitwidth_tag(*to)); } Expression::SignExtendTo { value, to } => { buffer.push(0x17); self.encode_value(value, buffer); buffer.push(bitwidth_tag(*to)); } Expression::ExtCodeSize { address } | Expression::ExtCodeHash { address } | Expression::Balance { address } => { buffer.push(match expression { Expression::ExtCodeSize { .. } => 0x20, Expression::ExtCodeHash { .. } => 0x21, Expression::Balance { .. } => 0x22, _ => unreachable!(), }); self.encode_value(address, buffer); } Expression::BlockHash { number } => { buffer.push(0x23); self.encode_value(number, buffer); } Expression::BlobHash { index } => { buffer.push(0x26); self.encode_value(index, buffer); } Expression::DataOffset { id } => { buffer.push(0x30); buffer.extend_from_slice(&(id.len() as u16).to_le_bytes()); buffer.extend_from_slice(id.as_bytes()); } Expression::DataSize { id } => { buffer.push(0x31); buffer.extend_from_slice(&(id.len() as u16).to_le_bytes()); buffer.extend_from_slice(id.as_bytes()); } Expression::LoadImmutable { key } => { buffer.push(0x32); buffer.extend_from_slice(&(key.len() as u16).to_le_bytes()); buffer.extend_from_slice(key.as_bytes()); } Expression::LinkerSymbol { path } => { buffer.push(0x33); buffer.extend_from_slice(&(path.len() as u16).to_le_bytes()); buffer.extend_from_slice(path.as_bytes()); } _ => { buffer.push(nullary_expr_tag(expression)); } } } fn encode_region(&mut self, region: &Region, buffer: &mut Vec) { buffer.extend_from_slice(&(region.statements.len() as u32).to_le_bytes()); for statement in ®ion.statements { self.encode_statement(statement, buffer); } buffer.push(region.yields.len() as u8); for yielded_value in ®ion.yields { self.encode_value(yielded_value, buffer); } } fn encode_statement(&mut self, statement: &Statement, buffer: &mut Vec) { match statement { Statement::Let { bindings, value } => { buffer.push(0x80); buffer.push(bindings.len() as u8); for binding in bindings { self.encode_value_id(*binding, buffer); } self.encode_expression(value, buffer); } Statement::MStore { offset, value, region, } => { buffer.push(0x81); self.encode_value(offset, buffer); self.encode_value(value, buffer); buffer.push(region_tag(region)); } Statement::MStore8 { offset, value, region, } => { buffer.push(0x82); self.encode_value(offset, buffer); self.encode_value(value, buffer); buffer.push(region_tag(region)); } Statement::MCopy { destination, source, length, } => { buffer.push(0x83); self.encode_value(destination, buffer); self.encode_value(source, buffer); self.encode_value(length, buffer); } Statement::SStore { key, value, static_slot, } => { buffer.push(0x84); self.encode_value(key, buffer); self.encode_value(value, buffer); if let Some(slot) = static_slot { buffer.push(1); let bytes = slot.to_bytes_le(); buffer.extend_from_slice(&(bytes.len() as u16).to_le_bytes()); buffer.extend_from_slice(&bytes); } else { buffer.push(0); } } Statement::TStore { key, value } => { buffer.push(0x85); self.encode_value(key, buffer); self.encode_value(value, buffer); } Statement::MappingSStore { key, slot, value } => { buffer.push(0xA5); self.encode_value(key, buffer); self.encode_value(slot, buffer); self.encode_value(value, buffer); } Statement::If { condition, inputs, then_region, else_region, outputs, } => { buffer.push(0x86); self.encode_value(condition, buffer); buffer.push(inputs.len() as u8); for input in inputs { self.encode_value(input, buffer); } self.encode_region(then_region, buffer); if let Some(else_region_body) = else_region { buffer.push(1); self.encode_region(else_region_body, buffer); } else { buffer.push(0); } buffer.push(outputs.len() as u8); for output in outputs { self.encode_value_id(*output, buffer); } } Statement::Switch { scrutinee, inputs, cases, default, outputs, } => { buffer.push(0x87); self.encode_value(scrutinee, buffer); buffer.push(inputs.len() as u8); for input in inputs { self.encode_value(input, buffer); } buffer.extend_from_slice(&(cases.len() as u32).to_le_bytes()); for case in cases { let case_bytes = case.value.to_bytes_le(); buffer.extend_from_slice(&(case_bytes.len() as u16).to_le_bytes()); buffer.extend_from_slice(&case_bytes); self.encode_region(&case.body, buffer); } if let Some(default_region) = default { buffer.push(1); self.encode_region(default_region, buffer); } else { buffer.push(0); } buffer.push(outputs.len() as u8); for output in outputs { self.encode_value_id(*output, buffer); } } Statement::For { initial_values, loop_variables, condition_statements, condition, body, post_input_variables, post, outputs, } => { buffer.push(0x88); buffer.push(initial_values.len() as u8); for value in initial_values { self.encode_value(value, buffer); } buffer.push(loop_variables.len() as u8); for loop_variable_id in loop_variables { self.encode_value_id(*loop_variable_id, buffer); } buffer.extend_from_slice(&(condition_statements.len() as u32).to_le_bytes()); for statement in condition_statements { self.encode_statement(statement, buffer); } self.encode_expression(condition, buffer); self.encode_region(body, buffer); buffer.push(post_input_variables.len() as u8); for post_input_variable_id in post_input_variables { self.encode_value_id(*post_input_variable_id, buffer); } self.encode_region(post, buffer); buffer.push(outputs.len() as u8); for output in outputs { self.encode_value_id(*output, buffer); } } Statement::Block(region) => { buffer.push(0x89); self.encode_region(region, buffer); } Statement::Revert { offset, length } => { buffer.push(0x90); self.encode_value(offset, buffer); self.encode_value(length, buffer); } Statement::Return { offset, length } => { buffer.push(0x91); self.encode_value(offset, buffer); self.encode_value(length, buffer); } Statement::Stop => buffer.push(0x92), Statement::Invalid => buffer.push(0x93), Statement::PanicRevert { code } => { buffer.push(0xA2); buffer.push(*code); } Statement::ErrorStringRevert { length, data } => { buffer.push(0xA3); buffer.push(*length); buffer.push(data.len() as u8); for word in data { for byte in word.to_bytes_be() { buffer.push(byte); } } } Statement::CustomErrorRevert { selector, arguments, } => { buffer.push(0xA4); buffer.push(arguments.len() as u8); for byte in selector.to_bytes_be() { buffer.push(byte); } for argument in arguments { self.encode_value(argument, buffer); } } Statement::ExternalCall { kind, gas, address, value, args_offset, args_length, ret_offset, ret_length, result, } => { buffer.push(0x94); buffer.push(callkind_tag(*kind)); self.encode_value(gas, buffer); self.encode_value(address, buffer); if let Some(call_value) = value { buffer.push(1); self.encode_value(call_value, buffer); } else { buffer.push(0); } self.encode_value(args_offset, buffer); self.encode_value(args_length, buffer); self.encode_value(ret_offset, buffer); self.encode_value(ret_length, buffer); self.encode_value_id(*result, buffer); } Statement::Create { kind, value, offset, length, salt, result, } => { buffer.push(0x95); buffer.push(createkind_tag(*kind)); self.encode_value(value, buffer); self.encode_value(offset, buffer); self.encode_value(length, buffer); if let Some(salt_value) = salt { buffer.push(1); self.encode_value(salt_value, buffer); } else { buffer.push(0); } self.encode_value_id(*result, buffer); } Statement::Log { offset, length, topics, } => { buffer.push(0x96); self.encode_value(offset, buffer); self.encode_value(length, buffer); buffer.push(topics.len() as u8); for topic in topics { self.encode_value(topic, buffer); } } Statement::CodeCopy { destination, offset, length, } => { buffer.push(0x97); self.encode_value(destination, buffer); self.encode_value(offset, buffer); self.encode_value(length, buffer); } Statement::ExtCodeCopy { address, destination, offset, length, } => { buffer.push(0x98); self.encode_value(address, buffer); self.encode_value(destination, buffer); self.encode_value(offset, buffer); self.encode_value(length, buffer); } Statement::ReturnDataCopy { destination, offset, length, } => { buffer.push(0x99); self.encode_value(destination, buffer); self.encode_value(offset, buffer); self.encode_value(length, buffer); } Statement::DataCopy { destination, offset, length, } => { buffer.push(0x9A); self.encode_value(destination, buffer); self.encode_value(offset, buffer); self.encode_value(length, buffer); } Statement::CallDataCopy { destination, offset, length, } => { buffer.push(0x9B); self.encode_value(destination, buffer); self.encode_value(offset, buffer); self.encode_value(length, buffer); } Statement::SetImmutable { key, value } => { buffer.push(0x9C); buffer.extend_from_slice(&(key.len() as u16).to_le_bytes()); buffer.extend_from_slice(key.as_bytes()); self.encode_value(value, buffer); } Statement::Leave { return_values } => { buffer.push(0x9D); buffer.push(return_values.len() as u8); for value in return_values { self.encode_value(value, buffer); } } Statement::Expression(expression) => { buffer.push(0x9E); self.encode_expression(expression, buffer); } Statement::SelfDestruct { address } => { buffer.push(0x9F); self.encode_value(address, buffer); } Statement::Break { values } => { buffer.push(0xA0); buffer.push(values.len() as u8); for value in values { self.encode_value(value, buffer); } } Statement::Continue { values } => { buffer.push(0xA1); buffer.push(values.len() as u8); for value in values { self.encode_value(value, buffer); } } } } } fn type_tag(value_type: &Type) -> u8 { match value_type { Type::Int(bit_width) => bitwidth_tag(*bit_width), Type::Ptr(address_space) => match address_space { crate::ir::AddressSpace::Heap => 0xE0, crate::ir::AddressSpace::Stack => 0xE1, crate::ir::AddressSpace::Storage => 0xE2, crate::ir::AddressSpace::Code => 0xE3, }, Type::Void => 0xFF, } } fn bitwidth_tag(bit_width: BitWidth) -> u8 { match bit_width { BitWidth::I1 => 1, BitWidth::I8 => 8, BitWidth::I32 => 32, BitWidth::I64 => 64, BitWidth::I128 => 128, BitWidth::I160 => 160, BitWidth::I256 => 0, } } fn binop_tag(operation: BinaryOperation) -> u8 { match operation { BinaryOperation::Add => 0, BinaryOperation::Sub => 1, BinaryOperation::Mul => 2, BinaryOperation::Div => 3, BinaryOperation::SDiv => 4, BinaryOperation::Mod => 5, BinaryOperation::SMod => 6, BinaryOperation::Exp => 7, BinaryOperation::AddMod => 8, BinaryOperation::MulMod => 9, BinaryOperation::And => 10, BinaryOperation::Or => 11, BinaryOperation::Xor => 12, BinaryOperation::Shl => 13, BinaryOperation::Shr => 14, BinaryOperation::Sar => 15, BinaryOperation::Lt => 16, BinaryOperation::Gt => 17, BinaryOperation::Slt => 18, BinaryOperation::Sgt => 19, BinaryOperation::Eq => 20, BinaryOperation::Byte => 21, BinaryOperation::SignExtend => 22, } } fn unaryop_tag(operation: UnaryOperation) -> u8 { match operation { UnaryOperation::IsZero => 0, UnaryOperation::Not => 1, UnaryOperation::Clz => 2, } } fn region_tag(region: &crate::ir::MemoryRegion) -> u8 { match region { crate::ir::MemoryRegion::Scratch => 0, crate::ir::MemoryRegion::FreePointerSlot => 1, crate::ir::MemoryRegion::Dynamic => 2, crate::ir::MemoryRegion::Unknown => 3, } } fn callkind_tag(kind: CallKind) -> u8 { match kind { CallKind::Call => 0, CallKind::CallCode => 1, CallKind::DelegateCall => 2, CallKind::StaticCall => 3, } } fn createkind_tag(kind: crate::ir::CreateKind) -> u8 { match kind { crate::ir::CreateKind::Create => 0, crate::ir::CreateKind::Create2 => 1, } } fn nullary_expr_tag(expression: &Expression) -> u8 { match expression { Expression::CallValue => 0x40, Expression::Caller => 0x41, Expression::Origin => 0x42, Expression::CallDataSize => 0x43, Expression::CodeSize => 0x44, Expression::GasPrice => 0x45, Expression::ReturnDataSize => 0x46, Expression::Coinbase => 0x47, Expression::Timestamp => 0x48, Expression::Number => 0x49, Expression::Difficulty => 0x4A, Expression::GasLimit => 0x4B, Expression::ChainId => 0x4C, Expression::SelfBalance => 0x4D, Expression::BaseFee => 0x4E, Expression::BlobBaseFee => 0x4F, Expression::Gas => 0x50, Expression::MSize => 0x51, Expression::Address => 0x52, _ => 0x00, } } /// Maximum number of differing literal positions allowed for fuzzy dedup. /// Each differing literal becomes a new i256 parameter, so keep this small. const MAX_FUZZY_LITERAL_DIFFS: usize = 4; /// Minimum function size (in IR statements) for fuzzy dedup when any /// differing-literal parameter is wider than `i64`. Wider new parameters /// cost more to pass at every call site, so the function body needs to be /// large enough that the dedup savings cover that overhead. const MIN_FUZZY_DEDUP_SIZE: usize = 20; /// Minimum function size for fuzzy dedup when every differing-literal /// parameter fits in `i64`. Cheap parameters tolerate smaller bodies because /// the per-call overhead is a single push on the PolkaVM `rv64e` target. const MIN_FUZZY_DEDUP_SIZE_NARROW_PARAMS: usize = 10; /// Deduplicates functions that are structurally identical except for literal constants. /// /// When two functions have the same structure but differ only in literal values, /// the duplicate is removed and its call sites are redirected to the canonical /// function with the differing literals passed as additional arguments. /// /// Returns the number of functions removed. pub fn deduplicate_functions_fuzzy(object: &mut Object) -> usize { if object.functions.len() < 2 { return 0; } let mut fuzzy_groups: BTreeMap, Vec> = BTreeMap::new(); for function in object.functions.values() { if function.size_estimate < MIN_FUZZY_DEDUP_SIZE_NARROW_PARAMS { continue; } let fuzzy_hash = fuzzy_canonicalize_function(function); fuzzy_groups .entry(fuzzy_hash) .or_default() .push(function.id); } let mut total_removed = 0; let mut next_value_id = ValueId(object.find_max_value_id() + 1); for group in fuzzy_groups.values() { if group.len() < 2 { continue; } let mut group_literals: Vec<(FunctionId, Vec)> = Vec::new(); for &function_id in group { let function = &object.functions[&function_id]; let literals = collect_literals_ordered(function); group_literals.push((function_id, literals)); } let literal_count = group_literals[0].1.len(); if group_literals .iter() .any(|(_, literals)| literals.len() != literal_count) { continue; } let mut differing_positions: Vec = Vec::new(); for position in 0..literal_count { let first_value = &group_literals[0].1[position]; if group_literals .iter() .any(|(_, literals)| &literals[position] != first_value) { differing_positions.push(position); } } if differing_positions.is_empty() { continue; } let mut value_sig_to_group: BTreeMap>, Vec> = BTreeMap::new(); for &position in &differing_positions { let sig: Vec> = group_literals .iter() .map(|(_, literals)| literals[position].to_bytes_le()) .collect(); value_sig_to_group.entry(sig).or_default().push(position); } let unique_parameter_count = value_sig_to_group.len(); if unique_parameter_count > MAX_FUZZY_LITERAL_DIFFS { continue; } let mut parameter_groups: Vec> = value_sig_to_group.into_values().collect(); parameter_groups.sort_by_key(|group| group[0]); let mut position_to_parameter_index: BTreeMap = BTreeMap::new(); for (parameter_index, positions) in parameter_groups.iter().enumerate() { for &position in positions { position_to_parameter_index.insert(position, parameter_index); } } let canonical_id = group[0]; let canonical_func = object.functions.get(&canonical_id).unwrap().clone(); let canonical_parameter_count = canonical_func.parameters.len(); let canonical_returns = &canonical_func.returns; let all_compatible = group.iter().skip(1).all(|&function_id| { let function = &object.functions[&function_id]; function.parameters.len() == canonical_parameter_count && &function.returns == canonical_returns && function .parameters .iter() .zip(canonical_func.parameters.iter()) .all(|((_, type1), (_, type2))| type1 == type2) }); if !all_compatible { continue; } let mut new_parameter_ids: Vec = Vec::new(); for _ in 0..unique_parameter_count { new_parameter_ids.push(next_value_id.fresh()); } let position_parameter_ids: Vec<(usize, ValueId)> = differing_positions .iter() .map(|&position| { ( position, new_parameter_ids[position_to_parameter_index[&position]], ) }) .collect(); let parameter_types: Vec = parameter_groups .iter() .map(|positions| { let max_value = group_literals .iter() .flat_map(|(_, literals)| { positions .iter() .map(move |&position| literals[position].clone()) }) .max() .unwrap_or_else(BigUint::zero); Type::Int(BitWidth::from_max_value(&max_value)) }) .collect(); let all_parameters_narrow = parameter_types.iter().all( |parameter_type| matches!(parameter_type, Type::Int(width) if *width <= BitWidth::I64), ); let effective_min_size = if all_parameters_narrow { MIN_FUZZY_DEDUP_SIZE_NARROW_PARAMS } else { MIN_FUZZY_DEDUP_SIZE }; if canonical_func.size_estimate < effective_min_size { continue; } let mut parameterized = canonical_func.clone(); for (&value_id, parameter_type) in new_parameter_ids.iter().zip(parameter_types.iter()) { parameterized.parameters.push((value_id, *parameter_type)); } let canonical_literals = &group_literals[0].1; replace_literals_with_parameters(&mut parameterized.body, &position_parameter_ids); object.functions.insert(canonical_id, parameterized); let canonical_extra_arguments: Vec = parameter_groups .iter() .map(|positions| canonical_literals[positions[0]].clone()) .collect(); for &function_id in group { let extra_arguments: Vec = if function_id == canonical_id { canonical_extra_arguments.clone() } else { let literals = &group_literals .iter() .find(|(id, _)| *id == function_id) .unwrap() .1; parameter_groups .iter() .map(|positions| literals[positions[0]].clone()) .collect() }; update_call_sites_with_extra_arguments( &mut object.code, function_id, canonical_id, &extra_arguments, ¶meter_types, &mut next_value_id, ); for function in object.functions.values_mut() { update_call_sites_with_extra_arguments( &mut function.body, function_id, canonical_id, &extra_arguments, ¶meter_types, &mut next_value_id, ); } } // Fold the removed duplicates' call sites into the canonical so call_count stays // accurate for the NoInline decision in set_inline_attributes. let merged_calls: usize = group .iter() .skip(1) .filter_map(|function_id| object.functions.get(function_id)) .map(|function| function.call_count) .sum(); if let Some(canonical) = object.functions.get_mut(&canonical_id) { canonical.call_count += merged_calls; } for &function_id in group.iter().skip(1) { object.functions.remove(&function_id); total_removed += 1; } } total_removed } /// Produces a fuzzy canonical form where literal values are replaced with /// position indices. Two functions with the same fuzzy form are structurally /// identical except for their literal constant values. fn fuzzy_canonicalize_function(function: &crate::ir::Function) -> Vec { let mut canon = Canonicalizer::new(); let mut buffer = Vec::new(); buffer.push(function.parameters.len() as u8); for (_, value_type) in &function.parameters { buffer.push(type_tag(value_type)); } buffer.push(function.returns.len() as u8); for value_type in &function.returns { buffer.push(type_tag(value_type)); } for (parameter_id, _) in &function.parameters { canon.get_or_insert(*parameter_id); } for return_value_id in &function.return_values_initial { canon.get_or_insert(*return_value_id); } let mut literal_counter = 0u32; for statement in &function.body.statements { fuzzy_encode_statement(&mut canon, statement, &mut buffer, &mut literal_counter); } buffer.push(0xFE); for return_value_id in &function.return_values { canon.encode_value_id(*return_value_id, &mut buffer); } buffer } fn fuzzy_encode_expression( canon: &mut Canonicalizer, expression: &Expression, buffer: &mut Vec, literal_counter: &mut u32, ) { match expression { Expression::Literal { value_type, .. } => { buffer.push(0x01); buffer.push(0xFF); buffer.extend_from_slice(&literal_counter.to_le_bytes()); *literal_counter += 1; buffer.push(type_tag(value_type)); } Expression::Var(id) => { buffer.push(0x02); canon.encode_value_id(*id, buffer); } Expression::Binary { operation, lhs, rhs, } => { buffer.push(0x03); buffer.push(binop_tag(*operation)); canon.encode_value(lhs, buffer); canon.encode_value(rhs, buffer); } Expression::Ternary { operation, a, b, n } => { buffer.push(0x04); buffer.push(binop_tag(*operation)); canon.encode_value(a, buffer); canon.encode_value(b, buffer); canon.encode_value(n, buffer); } Expression::Unary { operation, operand } => { buffer.push(0x05); buffer.push(unaryop_tag(*operation)); canon.encode_value(operand, buffer); } Expression::Call { function, arguments, } => { buffer.push(0x06); buffer.extend_from_slice(&function.0.to_le_bytes()); buffer.push(arguments.len() as u8); for argument in arguments { canon.encode_value(argument, buffer); } } Expression::SLoad { key, static_slot } => { buffer.push(0x12); canon.encode_value(key, buffer); if static_slot.is_some() { buffer.push(1); buffer.push(0xFF); buffer.extend_from_slice(&literal_counter.to_le_bytes()); *literal_counter += 1; } else { buffer.push(0); } } _ => { canon.encode_expression(expression, buffer); } } } /// Encodes a statement into the fuzzy form used for function deduplication. /// /// A `switch` case match value is encoded CONCRETELY (not as an abstracted literal position). A /// `switch` case label must be a compile-time constant, so it cannot be replaced by a parameter; /// abstracting it would let two functions that differ only in their case labels share a fuzzy form /// and be merged, after which `replace_literals_with_parameters` (which never substitutes a case value) /// leaves the canonical function's labels in place — a miscompile of the removed function's callers. fn fuzzy_encode_statement( canon: &mut Canonicalizer, statement: &Statement, buffer: &mut Vec, literal_counter: &mut u32, ) { match statement { Statement::Let { bindings, value } => { buffer.push(0x80); buffer.push(bindings.len() as u8); for binding in bindings { canon.encode_value_id(*binding, buffer); } fuzzy_encode_expression(canon, value, buffer, literal_counter); } Statement::SStore { key, value, static_slot, } => { buffer.push(0x84); canon.encode_value(key, buffer); canon.encode_value(value, buffer); if static_slot.is_some() { buffer.push(1); buffer.push(0xFF); buffer.extend_from_slice(&literal_counter.to_le_bytes()); *literal_counter += 1; } else { buffer.push(0); } } Statement::If { condition, inputs, then_region, else_region, outputs, } => { buffer.push(0x85); canon.encode_value(condition, buffer); buffer.push(inputs.len() as u8); for value in inputs { canon.encode_value(value, buffer); } fuzzy_encode_region(canon, then_region, buffer, literal_counter); if let Some(else_region_body) = else_region { buffer.push(1); fuzzy_encode_region(canon, else_region_body, buffer, literal_counter); } else { buffer.push(0); } buffer.push(outputs.len() as u8); for output in outputs { canon.encode_value_id(*output, buffer); } } Statement::Switch { scrutinee, inputs, cases, default, outputs, } => { buffer.push(0x86); canon.encode_value(scrutinee, buffer); buffer.push(inputs.len() as u8); for value in inputs { canon.encode_value(value, buffer); } buffer.push(cases.len() as u8); for case in cases { let case_bytes = case.value.to_bytes_le(); buffer.extend_from_slice(&(case_bytes.len() as u16).to_le_bytes()); buffer.extend_from_slice(&case_bytes); fuzzy_encode_region(canon, &case.body, buffer, literal_counter); } if let Some(default_region) = default { buffer.push(1); fuzzy_encode_region(canon, default_region, buffer, literal_counter); } else { buffer.push(0); } buffer.push(outputs.len() as u8); for output in outputs { canon.encode_value_id(*output, buffer); } } Statement::For { initial_values, loop_variables, condition_statements, condition, body, post, outputs, .. } => { buffer.push(0x87); buffer.push(initial_values.len() as u8); for value in initial_values { canon.encode_value(value, buffer); } buffer.push(loop_variables.len() as u8); for loop_variable_id in loop_variables { canon.encode_value_id(*loop_variable_id, buffer); } buffer.push(condition_statements.len() as u8); for statement in condition_statements { fuzzy_encode_statement(canon, statement, buffer, literal_counter); } fuzzy_encode_expression(canon, condition, buffer, literal_counter); fuzzy_encode_region(canon, body, buffer, literal_counter); fuzzy_encode_region(canon, post, buffer, literal_counter); buffer.push(outputs.len() as u8); for output in outputs { canon.encode_value_id(*output, buffer); } } Statement::Block(region) => { buffer.push(0x88); fuzzy_encode_region(canon, region, buffer, literal_counter); } Statement::Expression(expression) => { buffer.push(0x89); fuzzy_encode_expression(canon, expression, buffer, literal_counter); } _ => { canon.encode_statement(statement, buffer); } } } fn fuzzy_encode_region( canon: &mut Canonicalizer, region: &Region, buffer: &mut Vec, literal_counter: &mut u32, ) { buffer.extend_from_slice(&(region.statements.len() as u32).to_le_bytes()); for statement in ®ion.statements { fuzzy_encode_statement(canon, statement, buffer, literal_counter); } buffer.push(region.yields.len() as u8); for yielded_value in ®ion.yields { canon.encode_value(yielded_value, buffer); } } /// Collects all literal values from a function in walk order. Both this and /// [`replace_literals_with_parameters`] must use the **identical** traversal so /// position indices align — keep them in sync if either changes. /// /// Counted positions: `Expression::Literal` value, `Expression::SLoad::static_slot`, and /// `Statement::SStore::static_slot`. /// /// Switch case match values are NOT parameterizable literals (a case label must be a compile-time /// constant); they are encoded concretely in the fuzzy form, so they are not counted here. Keep /// this in sync with `replace_literals_with_parameters`. fn collect_literals_ordered(function: &crate::ir::Function) -> Vec { fn from_expr(expression: &Expression, literals: &mut Vec) { match expression { Expression::Literal { value, .. } => literals.push(value.clone()), Expression::SLoad { static_slot: Some(slot), .. } => literals.push(slot.clone()), _ => {} } } fn walk(statements: &[Statement], literals: &mut Vec) { for statement in statements { match statement { Statement::Let { value, .. } | Statement::Expression(value) => { from_expr(value, literals) } Statement::SStore { static_slot: Some(slot), .. } => literals.push(slot.clone()), Statement::If { then_region, else_region, .. } => { walk(&then_region.statements, literals); if let Some(else_region_body) = else_region { walk(&else_region_body.statements, literals); } } Statement::Switch { cases, default, .. } => { for case in cases { walk(&case.body.statements, literals); } if let Some(default_region) = default { walk(&default_region.statements, literals); } } Statement::For { condition_statements, condition, body, post, .. } => { walk(condition_statements, literals); from_expr(condition, literals); walk(&body.statements, literals); walk(&post.statements, literals); } Statement::Block(region) => walk(®ion.statements, literals), _ => {} } } } let mut literals = Vec::new(); walk(&function.body.statements, &mut literals); literals } /// Replaces literal values at differing positions with `Var` references to new /// parameters. Walk order **must** match [`collect_literals_ordered`] so the /// position indices align. /// /// Switch case match values are not parameterizable and are not counted by /// [`collect_literals_ordered`], so the position counter is not advanced for them here. fn replace_literals_with_parameters( block: &mut Block, position_parameter_ids: &[(usize, ValueId)], ) { fn from_expr( expression: &mut Expression, positions: &BTreeMap, counter: &mut usize, ) { match expression { Expression::Literal { .. } => { if let Some(¶meter_value_id) = positions.get(counter) { *expression = Expression::Var(parameter_value_id); } *counter += 1; } Expression::SLoad { static_slot: slot @ Some(_), .. } => { if positions.contains_key(counter) { *slot = None; } *counter += 1; } _ => {} } } fn walk( statements: &mut [Statement], positions: &BTreeMap, counter: &mut usize, ) { for statement in statements { match statement { Statement::Let { value, .. } | Statement::Expression(value) => { from_expr(value, positions, counter) } Statement::SStore { static_slot: slot @ Some(_), .. } => { if positions.contains_key(counter) { *slot = None; } *counter += 1; } Statement::If { then_region, else_region, .. } => { walk(&mut then_region.statements, positions, counter); if let Some(else_region_body) = else_region { walk(&mut else_region_body.statements, positions, counter); } } Statement::Switch { cases, default, .. } => { for case in cases { walk(&mut case.body.statements, positions, counter); } if let Some(default_region) = default { walk(&mut default_region.statements, positions, counter); } } Statement::For { condition_statements, condition, body, post, .. } => { walk(condition_statements, positions, counter); from_expr(condition, positions, counter); walk(&mut body.statements, positions, counter); walk(&mut post.statements, positions, counter); } Statement::Block(region) => walk(&mut region.statements, positions, counter), _ => {} } } } let positions: BTreeMap = position_parameter_ids.iter().copied().collect(); let mut counter = 0usize; walk(&mut block.statements, &positions, &mut counter); } /// Updates call sites in a block, changing calls to `old_id` into calls to /// `new_id` with extra literal arguments appended. For each matching call, /// emits one `Let` binding per extra argument immediately before the call site /// (allocating fresh `ValueId`s through `next_value_id`). fn update_call_sites_with_extra_arguments( block: &mut Block, old_id: FunctionId, new_id: FunctionId, extra_arguments: &[BigUint], parameter_types: &[Type], next_value_id: &mut ValueId, ) { fn rewrite( statements: &mut Vec, old_id: FunctionId, new_id: FunctionId, extra_arguments: &[BigUint], parameter_types: &[Type], next_id: &mut ValueId, ) { let mut index = 0; while index < statements.len() { match &mut statements[index] { Statement::If { then_region, else_region, .. } => { rewrite( &mut then_region.statements, old_id, new_id, extra_arguments, parameter_types, next_id, ); if let Some(else_region_body) = else_region { rewrite( &mut else_region_body.statements, old_id, new_id, extra_arguments, parameter_types, next_id, ); } } Statement::Switch { cases, default, .. } => { for case in cases.iter_mut() { rewrite( &mut case.body.statements, old_id, new_id, extra_arguments, parameter_types, next_id, ); } if let Some(default_region) = default { rewrite( &mut default_region.statements, old_id, new_id, extra_arguments, parameter_types, next_id, ); } } Statement::For { condition_statements, body, post, .. } => { rewrite( condition_statements, old_id, new_id, extra_arguments, parameter_types, next_id, ); rewrite( &mut body.statements, old_id, new_id, extra_arguments, parameter_types, next_id, ); rewrite( &mut post.statements, old_id, new_id, extra_arguments, parameter_types, next_id, ); } Statement::Block(region) => { rewrite( &mut region.statements, old_id, new_id, extra_arguments, parameter_types, next_id, ); } _ => {} } let is_target = matches!(&statements[index], Statement::Let { value: Expression::Call { function, .. }, .. } | Statement::Expression(Expression::Call { function, .. }) if *function == old_id); if is_target { let mut extra_values = Vec::with_capacity(extra_arguments.len()); for (argument_value, parameter_type) in extra_arguments.iter().zip(parameter_types.iter()) { let value_id = next_id.fresh(); statements.insert( index, Statement::Let { bindings: vec![value_id], value: Expression::Literal { value: argument_value.clone(), value_type: *parameter_type, }, }, ); index += 1; extra_values.push(Value { id: value_id, value_type: *parameter_type, }); } match &mut statements[index] { Statement::Let { value: Expression::Call { function, arguments, }, .. } | Statement::Expression(Expression::Call { function, arguments, }) => { *function = new_id; arguments.extend(extra_values); } _ => unreachable!("is_target check above just matched a Call"), } } index += 1; } } rewrite( &mut block.statements, old_id, new_id, extra_arguments, parameter_types, next_value_id, ); } /// Redirects function calls in a block, replacing old function IDs with new ones. fn redirect_calls_in_block(block: &mut Block, redirects: &BTreeMap) { for_each_statement_mut(&mut block.statements, &mut |statement| { statement.for_each_expression_mut(&mut |expression| { if let Expression::Call { function, .. } = expression { if let Some(&new_id) = redirects.get(function) { *function = new_id; } } }); }); } /// Folds constant `Keccak256Single` and `Keccak256Pair` expressions in an object. /// /// This is a targeted pass designed to run after the mem_opt pass, which creates /// `Keccak256Single`/`Keccak256Pair` nodes from `mstore + keccak256` patterns. /// When the argument(s) are compile-time constants, the hash is precomputed. /// /// **Known gap (deliberate).** The fused `Keccak256Pair`/`Keccak256Single` helper writes its inputs /// back to scratch `heap[0..0x40)`/`[0..0x20)`, and the mem_opt fusion *dead-eliminates the original /// `mstore`s on the strength of that write-back* (see the fusion comment in `mem_opt`). Folding the /// fused node to a literal removes the helper — and with it the write-back — so the scratch is left /// unwritten. A later `mload` from `[0, 0x40)` that mem_opt's forwarding cannot reach (across a /// region/call boundary) would then read stale memory instead of the hashed inputs. /// /// This gap is NOT closed because every sound fix is a regression — the missing write means the /// fold's current output is already short the `mstore`s, so re-emitting them necessarily adds code /// (or disabling the fold falls back to the runtime keccak helper: +2.6 KB / +0.78 % on the OZ /// corpus), and there is no mem_opt run after the fold to dead-store-eliminate re-emitted writes. It /// is solc-unreachable: solc treats scratch as volatile and never re-reads `[0, 0x40)` as data after /// a keccak, so the dropped write-back is always dead in compiler-generated code. Only hand-written /// Yul that reads scratch after a constant-operand keccak across a boundary can observe it. pub fn fold_constant_keccak(object: &mut Object) { fold_keccak_in_block(&mut object.code); for function in object.functions.values_mut() { fold_keccak_in_block(&mut function.body); } } /// Walks a block's statements and folds constant keccak256 expressions. fn fold_keccak_in_block(block: &mut Block) { let mut constants: BTreeMap = BTreeMap::new(); fold_keccak_in_statements(&mut block.statements, &mut constants); } /// Processes statements, tracking constants and folding keccak expressions. fn fold_keccak_in_statements(statements: &mut [Statement], constants: &mut BTreeMap) { for statement in statements.iter_mut() { match statement { Statement::Let { bindings, value: expression, } => { if bindings.len() == 1 { if let Expression::Literal { value, .. } = expression { constants.insert(bindings[0].0, value.clone()); } } match expression { Expression::Keccak256Single { word0 } => { if let Some(constant) = constants.get(&word0.id.0) { *expression = Expression::Literal { value: fold_keccak256_single(constant), value_type: Type::Int(BitWidth::I256), }; } } Expression::Keccak256Pair { word0, word1 } => { if let (Some(constant0), Some(constant1)) = (constants.get(&word0.id.0), constants.get(&word1.id.0)) { let constant0 = constant0.clone(); let constant1 = constant1.clone(); *expression = Expression::Literal { value: fold_keccak256_pair(&constant0, &constant1), value_type: Type::Int(BitWidth::I256), }; } } _ => {} } if bindings.len() == 1 { if let Expression::Literal { value, .. } = expression { constants.insert(bindings[0].0, value.clone()); } } } Statement::If { then_region, else_region, .. } => { fold_keccak_in_statements(&mut then_region.statements, &mut constants.clone()); if let Some(else_region_body) = else_region { fold_keccak_in_statements( &mut else_region_body.statements, &mut constants.clone(), ); } } Statement::Switch { cases, default, .. } => { for case in cases.iter_mut() { fold_keccak_in_statements(&mut case.body.statements, &mut constants.clone()); } if let Some(default_region) = default { fold_keccak_in_statements( &mut default_region.statements, &mut constants.clone(), ); } } Statement::For { condition_statements, body, post, .. } => { fold_keccak_in_statements(condition_statements, constants); fold_keccak_in_statements(&mut body.statements, &mut constants.clone()); fold_keccak_in_statements(&mut post.statements, &mut constants.clone()); } Statement::Block(region) => { fold_keccak_in_statements(&mut region.statements, &mut constants.clone()); } _ => {} } } } #[cfg(test)] mod tests { use super::*; fn make_let_literal(id: u32, value: u64) -> Statement { Statement::Let { bindings: vec![ValueId(id)], value: Expression::Literal { value: BigUint::from(value), value_type: Type::Int(BitWidth::I256), }, } } fn make_let_binop(id: u32, operation: BinaryOperation, lhs_id: u32, rhs_id: u32) -> Statement { Statement::Let { bindings: vec![ValueId(id)], value: Expression::Binary { operation, lhs: Value::int(ValueId(lhs_id)), rhs: Value::int(ValueId(rhs_id)), }, } } #[test] fn test_constant_fold_add() { let result = fold_binary( BinaryOperation::Add, &BigUint::from(100u64), &BigUint::from(200u64), ); assert_eq!(result, Some(BigUint::from(300u64))); } #[test] fn test_constant_fold_sub_wrap() { let result = fold_binary( BinaryOperation::Sub, &BigUint::from(0u64), &BigUint::from(1u64), ); assert_eq!(result, Some(max_u256())); } #[test] fn test_constant_fold_mul() { let result = fold_binary( BinaryOperation::Mul, &BigUint::from(7u64), &BigUint::from(6u64), ); assert_eq!(result, Some(BigUint::from(42u64))); } #[test] fn test_constant_fold_div_by_zero() { let result = fold_binary( BinaryOperation::Div, &BigUint::from(100u64), &BigUint::zero(), ); assert_eq!(result, Some(BigUint::zero())); } #[test] fn test_constant_fold_comparisons() { assert_eq!( fold_binary( BinaryOperation::Lt, &BigUint::from(5u64), &BigUint::from(10u64) ), Some(BigUint::one()) ); assert_eq!( fold_binary( BinaryOperation::Lt, &BigUint::from(10u64), &BigUint::from(5u64) ), Some(BigUint::zero()) ); assert_eq!( fold_binary( BinaryOperation::Eq, &BigUint::from(42u64), &BigUint::from(42u64) ), Some(BigUint::one()) ); } #[test] fn test_constant_fold_bitwise() { assert_eq!( fold_binary( BinaryOperation::And, &BigUint::from(0xFF00u64), &BigUint::from(0x0FF0u64) ), Some(BigUint::from(0x0F00u64)) ); assert_eq!( fold_binary( BinaryOperation::Or, &BigUint::from(0xFF00u64), &BigUint::from(0x00FFu64) ), Some(BigUint::from(0xFFFFu64)) ); } #[test] fn test_constant_fold_shifts() { assert_eq!( fold_binary( BinaryOperation::Shl, &BigUint::from(8u64), &BigUint::from(1u64) ), Some(BigUint::from(256u64)) ); assert_eq!( fold_binary( BinaryOperation::Shr, &BigUint::from(4u64), &BigUint::from(256u64) ), Some(BigUint::from(16u64)) ); } #[test] fn test_unary_fold() { assert_eq!( fold_unary(UnaryOperation::IsZero, &BigUint::zero()), Some(BigUint::one()) ); assert_eq!( fold_unary(UnaryOperation::IsZero, &BigUint::from(42u64)), Some(BigUint::zero()) ); assert_eq!( fold_unary(UnaryOperation::Not, &BigUint::zero()), Some(max_u256()) ); } #[test] fn test_simplifier_constant_propagation() { let mut simplifier = Simplifier::new(); let statements = vec![ make_let_literal(1, 10), make_let_literal(2, 20), make_let_binop(3, BinaryOperation::Add, 1, 2), Statement::Return { offset: Value::int(ValueId(3)), length: Value::int(ValueId(3)), }, ]; let block = &mut Block { statements }; simplifier.simplify_block(block); let v3_let = block.statements.iter().find( |statement| matches!(statement, Statement::Let { bindings, .. } if bindings.contains(&ValueId(3))), ); let v3_let = v3_let.expect("v3 should still exist"); if let Statement::Let { value, .. } = v3_let { if let Expression::Literal { value, .. } = value { assert_eq!(*value, BigUint::from(30u64)); } else { panic!("Expected literal after constant folding, got: {value:?}"); } } } #[test] fn test_simplifier_algebraic_identity_add_zero() { let mut simplifier = Simplifier::new(); let statements = vec![ make_let_literal(1, 0), Statement::Let { bindings: vec![ValueId(2)], value: Expression::Binary { operation: BinaryOperation::Add, lhs: Value::int(ValueId(99)), rhs: Value::int(ValueId(1)), }, }, Statement::Return { offset: Value::int(ValueId(2)), length: Value::int(ValueId(2)), }, ]; let block = &mut Block { statements }; simplifier.simplify_block(block); let v2_let = block.statements.iter().find( |statement| matches!(statement, Statement::Let { bindings, .. } if bindings.contains(&ValueId(2))), ); let v2_let = v2_let.expect("v2 should still exist"); if let Statement::Let { value, .. } = v2_let { match value { Expression::Var(id) => assert_eq!(id.0, 99), _ => panic!("Expected Var after algebraic simplification, got: {value:?}"), } } assert!(simplifier.statistics.identities_simplified > 0); } #[test] fn test_no_crash_on_unused_bindings() { let mut simplifier = Simplifier::new(); let statements = vec![ make_let_literal(1, 42), make_let_literal(2, 100), Statement::Return { offset: Value::int(ValueId(2)), length: Value::int(ValueId(2)), }, ]; let block = &mut Block { statements }; simplifier.simplify_block(block); assert_eq!(block.statements.len(), 3); } #[test] fn test_copy_propagation() { let mut simplifier = Simplifier::new(); let statements = vec![ make_let_literal(1, 42), Statement::Let { bindings: vec![ValueId(2)], value: Expression::Var(ValueId(1)), }, Statement::Return { offset: Value::int(ValueId(2)), length: Value::int(ValueId(2)), }, ]; let block = &mut Block { statements }; simplifier.simplify_block(block); if let Statement::Return { offset, .. } = &block.statements[block.statements.len() - 1] { assert!( offset.id.0 == 1 || matches!(block.statements.last(), Some(Statement::Return { .. })) ); } } #[test] fn test_ternary_fold() { let result = fold_ternary( BinaryOperation::AddMod, &BigUint::from(10u64), &BigUint::from(20u64), &BigUint::from(7u64), ); assert_eq!(result, Some(BigUint::from(2u64))); let result = fold_ternary( BinaryOperation::MulMod, &BigUint::from(5u64), &BigUint::from(7u64), &BigUint::from(6u64), ); assert_eq!(result, Some(BigUint::from(5u64))); let result = fold_ternary( BinaryOperation::AddMod, &BigUint::from(10u64), &BigUint::from(20u64), &BigUint::zero(), ); assert_eq!(result, Some(BigUint::zero())); } #[test] fn test_exp_fold() { let result = fold_binary( BinaryOperation::Exp, &BigUint::from(2u64), &BigUint::from(10u64), ); assert_eq!(result, Some(BigUint::from(1024u64))); } #[test] fn test_byte_fold() { let result = fold_binary( BinaryOperation::Byte, &BigUint::from(31u64), &BigUint::from(0xFFu64), ); assert_eq!(result, Some(BigUint::from(0xFFu64))); let result = fold_binary( BinaryOperation::Byte, &BigUint::from(0u64), &BigUint::from(0xFFu64), ); assert_eq!(result, Some(BigUint::zero())); } /// The panic-pattern matcher must honor last-write-wins — a later store that overwrites /// the matched selector (offset 0) or code (offset 4) means the revert payload is not the /// canonical `Panic`, so the window must not be collapsed. #[test] fn panic_pattern_respects_last_write_wins() { use crate::ir::{MemoryRegion, Value, ValueId}; let panic_word = BigUint::parse_bytes( revive_common::PANIC_UINT256_SELECTOR_WORD_HEX.as_bytes(), 16, ) .unwrap(); let mut constants: BTreeMap = BTreeMap::new(); constants.insert(0, BigUint::zero()); constants.insert(1, panic_word); constants.insert(2, BigUint::from(4u64)); constants.insert(3, BigUint::from(0x11u64)); constants.insert(4, BigUint::from(0xdeadbeefu64)); constants.insert(5, BigUint::from(0x100u64)); let mstore = |offset: u32, value: u32| Statement::MStore { offset: Value::int(ValueId(offset)), value: Value::int(ValueId(value)), region: MemoryRegion::Scratch, }; assert_eq!( find_panic_pattern_backwards(&[mstore(0, 1), mstore(2, 3)], &constants), Some((0, 0x11)), ); assert_eq!( find_panic_pattern_backwards(&[mstore(0, 1), mstore(2, 3), mstore(0, 4)], &constants), None, ); assert_eq!( find_panic_pattern_backwards(&[mstore(0, 1), mstore(2, 3), mstore(2, 5)], &constants), None, ); } }