fn fold_constant_binary_ops(block: &mut ir::BasicBlock) {
let mut optimized = Vec::with_capacity(block.instructions.len());
let mut i = 0;
while i < block.instructions.len() {
if i + 1 < block.instructions.len() {
if let (
ir::Instruction::PushLiteral(lit),
ir::Instruction::JumpIf { target },
) = (&block.instructions[i], &block.instructions[i + 1]) {
let branch_taken = match lit {
ir::LiteralValue::Boolean(b) => Some(!*b),
ir::LiteralValue::Integer(n) => Some(n.is_zero()),
_ => None,
};
match branch_taken {
Some(true) => {
optimized.push(ir::Instruction::Jump { target: *target });
i += 2;
continue;
}
Some(false) => {
i += 2;
continue;
}
None => {}
}
}
}
if i + 2 < block.instructions.len() {
if let (
ir::Instruction::PushLiteral(lhs),
ir::Instruction::PushLiteral(rhs),
ir::Instruction::BinaryOp(op),
) = (
&block.instructions[i],
&block.instructions[i + 1],
&block.instructions[i + 2],
) {
if let Some(result) = evaluate_binary_literal(lhs, rhs, *op) {
optimized.push(ir::Instruction::PushLiteral(result));
i += 3;
continue;
}
}
}
if i + 1 < block.instructions.len() {
if let Some(simplified) = try_identity_elimination(
&block.instructions[i],
&block.instructions[i + 1],
) {
if let Some(instr) = simplified {
optimized.push(instr);
}
i += 2;
continue;
}
}
optimized.push(block.instructions[i].clone());
i += 1;
}
block.instructions = optimized;
}
fn try_identity_elimination(
first: &ir::Instruction,
second: &ir::Instruction,
) -> Option<Option<ir::Instruction>> {
use ir::{Instruction::{PushLiteral, BinaryOp}, LiteralValue::Integer};
match (first, second) {
(PushLiteral(Integer(n)), BinaryOp(ir::BinaryOperator::Add)) if n.is_zero() => {
Some(None)
}
(PushLiteral(Integer(n)), BinaryOp(ir::BinaryOperator::Mul)) if *n == 1.into() => {
Some(None)
}
(PushLiteral(Integer(n)), BinaryOp(ir::BinaryOperator::Div)) if *n == 1.into() => {
Some(None)
}
_ => None,
}
}
const MAX_FOLDED_LITERAL_BITS: u64 = 4096;
fn evaluate_binary_literal(
lhs: &ir::LiteralValue,
rhs: &ir::LiteralValue,
op: ir::BinaryOperator,
) -> Option<ir::LiteralValue> {
use ir::LiteralValue::{Integer, Boolean};
match (lhs, rhs) {
(Integer(a), Integer(b)) => match op {
ir::BinaryOperator::Add => Some(Integer(a + b)),
ir::BinaryOperator::Sub => Some(Integer(a - b)),
ir::BinaryOperator::Mul => {
if a.bits().saturating_add(b.bits()) > MAX_FOLDED_LITERAL_BITS {
None
} else {
Some(Integer(a * b))
}
}
ir::BinaryOperator::Div => {
if b.is_zero() {
None
} else {
Some(Integer(a / b))
}
}
ir::BinaryOperator::Mod => {
if b.is_zero() {
None
} else {
Some(Integer(a % b))
}
}
ir::BinaryOperator::BitAnd => Some(Integer(a & b)),
ir::BinaryOperator::BitOr => Some(Integer(a | b)),
ir::BinaryOperator::BitXor => Some(Integer(a ^ b)),
ir::BinaryOperator::Shl => {
let shift = b.to_u64()?;
if a.bits().saturating_add(shift) > MAX_FOLDED_LITERAL_BITS {
None
} else {
Some(Integer(a << shift))
}
}
ir::BinaryOperator::Shr => {
let shift = b.to_u64()?;
Some(Integer(a >> shift))
}
ir::BinaryOperator::Lt => Some(Boolean(a < b)),
ir::BinaryOperator::Le => Some(Boolean(a <= b)),
ir::BinaryOperator::Gt => Some(Boolean(a > b)),
ir::BinaryOperator::Ge => Some(Boolean(a >= b)),
ir::BinaryOperator::Eq => Some(Boolean(a == b)),
ir::BinaryOperator::Ne => Some(Boolean(a != b)),
},
(Boolean(a), Boolean(b)) => match op {
ir::BinaryOperator::Eq => Some(Boolean(a == b)),
ir::BinaryOperator::Ne => Some(Boolean(a != b)),
_ => None,
},
_ => None,
}
}
#[allow(clippy::items_after_test_module)]
#[cfg(test)]
mod fold_literal_bits_guard_tests {
use super::*;
use num_bigint::BigInt;
fn int(value: impl Into<BigInt>) -> ir::LiteralValue {
ir::LiteralValue::Integer(value.into())
}
#[test]
fn shl_declines_to_fold_oversized_shift() {
let result = evaluate_binary_literal(
&int(1),
&int(18_000_000_000_000_000_000u64),
ir::BinaryOperator::Shl,
);
assert_eq!(result, None, "oversized shift must be left to runtime");
let result =
evaluate_binary_literal(&int(1), &int(200_000_000), ir::BinaryOperator::Shl);
assert_eq!(result, None);
}
#[test]
fn shl_still_folds_legal_uint256_constants() {
let result = evaluate_binary_literal(&int(1), &int(255), ir::BinaryOperator::Shl)
.expect("legal uint256 shift must keep folding");
assert_eq!(result, int(BigInt::from(1) << 255u32));
}
#[test]
fn mul_declines_to_fold_when_product_exceeds_ceiling() {
let big = BigInt::from(1) << 4000u32; let result =
evaluate_binary_literal(&int(big.clone()), &int(big), ir::BinaryOperator::Mul);
assert_eq!(result, None, "huge-literal product must be left to runtime");
}
#[test]
fn mul_still_folds_uint256_operands() {
let max256: BigInt = (BigInt::from(1) << 256u32) - 1;
let result = evaluate_binary_literal(
&int(max256.clone()),
&int(max256.clone()),
ir::BinaryOperator::Mul,
)
.expect("uint256 product must keep folding");
assert_eq!(result, int(&max256 * &max256));
}
}