use std::collections::BTreeMap;
use analyssa::BitSet;
use crate::{
analysis::{
conv_op_for_target, simplify_op, CilTarget, CmpKind, ConstValue, ConstValueCilExt,
ConstantPropagation, EhCfg, MethodRef, SccpResult, SimplifyResult, SsaEvaluator,
SsaFunction, SsaOp, SsaType, SsaVarId,
},
compiler::{
pass::{ModificationScope, SsaPass},
CompilerContext, EventKind, EventLog,
},
metadata::{tables::TableId, token::Token, typesystem::PointerSize},
CilObject,
};
fn is_method_on_type(assembly: &CilObject, token: Token, type_name: &str) -> bool {
match token.table() {
0x06 => assembly
.method(&token)
.ok()
.and_then(|m| m.declaring_type_rc())
.is_some_and(|ty| ty.name.contains(type_name)),
0x0A => assembly
.refs_members()
.get(&token)
.and_then(|entry| entry.value().declaredby.fullname())
.is_some_and(|name| name.contains(type_name)),
_ => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IntOperand {
W32(u32),
W64(u64),
}
impl IntOperand {
fn from_const(value: &ConstValue) -> Option<Self> {
#[allow(clippy::cast_sign_loss)]
Some(match value {
ConstValue::I8(v) => Self::W32(i32::from(*v) as u32),
ConstValue::I16(v) => Self::W32(i32::from(*v) as u32),
ConstValue::I32(v) => Self::W32(*v as u32),
ConstValue::U8(v) => Self::W32(u32::from(*v)),
ConstValue::U16(v) => Self::W32(u32::from(*v)),
ConstValue::U32(v) => Self::W32(*v),
ConstValue::I64(v) => Self::W64(*v as u64),
ConstValue::U64(v) => Self::W64(*v),
ConstValue::True => Self::W32(1),
ConstValue::False => Self::W32(0),
_ => return None,
})
}
}
fn fold_rem(left: IntOperand, right: IntOperand, unsigned: bool) -> Option<ConstValue> {
#[allow(clippy::cast_possible_wrap)]
match (left, right) {
(IntOperand::W32(l), IntOperand::W32(r)) => {
if unsigned {
Some(ConstValue::I32(l.checked_rem(r)? as i32))
} else {
Some(ConstValue::I32((l as i32).checked_rem(r as i32)?))
}
}
(IntOperand::W64(l), IntOperand::W64(r)) => {
if unsigned {
Some(ConstValue::I64(l.checked_rem(r)? as i64))
} else {
Some(ConstValue::I64((l as i64).checked_rem(r as i64)?))
}
}
_ => None,
}
}
#[derive(Debug, Clone)]
enum AlgebraicResult {
Constant { dest: SsaVarId, value: ConstValue },
Copy { dest: SsaVarId, src: SsaVarId },
}
#[derive(Debug, Clone)]
struct ConvInfo {
operand: SsaVarId,
target: SsaType,
overflow_check: bool,
unsigned: bool,
block_idx: usize,
instr_idx: usize,
}
#[derive(Debug)]
enum ConvTransform {
ReplaceOperand {
block_idx: usize,
instr_idx: usize,
dest: SsaVarId,
new_operand: SsaVarId,
target: SsaType,
unsigned: bool,
reason: &'static str,
},
ReplaceWithCopy {
block_idx: usize,
instr_idx: usize,
dest: SsaVarId,
src: SsaVarId,
reason: &'static str,
},
}
#[derive(Debug, Clone, Copy)]
enum StringFoldOp {
Concat2,
Concat3,
Concat4,
SubstringFrom,
SubstringRange,
Replace,
ToLower,
ToUpper,
}
pub struct ConstantPropagationPass {
max_iterations: usize,
}
impl ConstantPropagationPass {
#[must_use]
pub fn new(max_iterations: usize) -> Self {
Self { max_iterations }
}
fn run_constant_propagation(
ssa: &mut SsaFunction,
method_token: Token,
changes: &mut EventLog,
ptr_size: PointerSize,
max_iterations: usize,
assembly: &CilObject,
) -> BTreeMap<SsaVarId, ConstValue> {
let block_count = ssa.block_count();
if block_count == 0 {
return BTreeMap::new();
}
ssa.recompute_uses();
let cfg = EhCfg::from_ssa(ssa);
let mut sccp = ConstantPropagation::new(ptr_size);
let mut sccp_result = sccp.analyze(ssa, &cfg);
let mut constants: BTreeMap<SsaVarId, ConstValue> = sccp_result
.constants()
.map(|(var, c)| (var, c.clone()))
.collect();
let pre_fold_count = constants.len();
Self::fold_pure_calls(
ssa,
&mut constants,
method_token,
changes,
assembly,
ptr_size,
);
if constants.len() > pre_fold_count {
ssa.recompute_uses();
let cfg = EhCfg::from_ssa(ssa);
let mut sccp2 = ConstantPropagation::new(ptr_size);
let sccp_result2 = sccp2.analyze(ssa, &cfg);
for (var, c) in sccp_result2.constants() {
constants.entry(var).or_insert_with(|| c.clone());
}
sccp_result = sccp_result2;
}
for _ in 0..max_iterations {
let prev_count = constants.len();
Self::optimize_algebraic_identities(ssa, &mut constants, method_token, changes);
Self::simplify_involutory_ops(ssa, method_token, changes);
Self::fold_conversions(ssa, &mut constants, method_token, changes, ptr_size);
Self::eliminate_redundant_conversions(ssa, method_token, changes);
Self::fold_overflow_checked_ops(ssa, &mut constants, method_token, changes, ptr_size);
Self::fold_string_operations(ssa, &mut constants, method_token, changes, assembly);
if constants.len() == prev_count {
break;
}
}
Self::apply_constant_folding(ssa, &constants, &sccp_result, method_token, changes);
Self::simplify_control_flow(ssa, &constants, &sccp_result, method_token, changes);
constants
}
fn optimize_algebraic_identities(
ssa: &mut SsaFunction,
constants: &mut BTreeMap<SsaVarId, ConstValue>,
method_token: Token,
changes: &mut EventLog,
) {
let mut transformations: Vec<(usize, usize, AlgebraicResult)> = Vec::new();
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
let op = instr.op();
let operand_type = op
.uses()
.first()
.and_then(|operand| ssa.variable(*operand))
.map(|var| var.var_type().clone());
if let Some(result) =
Self::check_algebraic_identity(op, constants, operand_type.as_ref())
{
transformations.push((block_idx, instr_idx, result));
}
}
}
for (block_idx, instr_idx, result) in transformations {
if let Some(block) = ssa.block_mut(block_idx) {
let Some(instr) = block.instructions_mut().get_mut(instr_idx) else {
continue;
};
let old_op_str = format!("{}", instr.op());
match result {
AlgebraicResult::Constant { dest, value } => {
constants.insert(dest, value.clone());
instr.set_op(SsaOp::Const {
dest,
value: value.clone(),
});
changes
.record(EventKind::ConstantFolded)
.at(method_token, instr_idx)
.message(format!("{old_op_str} → {value} (algebraic)"));
}
AlgebraicResult::Copy { dest, src } => {
if let Some(value) = constants.get(&src).cloned() {
constants.insert(dest, value.clone());
instr.set_op(SsaOp::Const {
dest,
value: value.clone(),
});
changes
.record(EventKind::ConstantFolded)
.at(method_token, instr_idx)
.message(format!("{old_op_str} → {value} (identity)"));
} else {
instr.set_op(SsaOp::Copy { dest, src });
changes
.record(EventKind::ConstantFolded)
.at(method_token, instr_idx)
.message(format!("{old_op_str} → copy {src} (identity)"));
}
}
}
}
}
}
fn check_algebraic_identity(
op: &SsaOp,
constants: &BTreeMap<SsaVarId, ConstValue>,
operand_type: Option<&SsaType>,
) -> Option<AlgebraicResult> {
let dest = op.dest()?;
match simplify_op(op, constants, operand_type) {
SimplifyResult::Constant(value) => Some(AlgebraicResult::Constant { dest, value }),
SimplifyResult::Copy(src) => Some(AlgebraicResult::Copy { dest, src }),
SimplifyResult::None => None,
}
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::cast_lossless)]
#[allow(clippy::cast_possible_wrap)]
fn fold_conversions(
ssa: &mut SsaFunction,
constants: &mut BTreeMap<SsaVarId, ConstValue>,
method_token: Token,
changes: &mut EventLog,
ptr_size: PointerSize,
) {
let mut new_constants: Vec<(SsaVarId, ConstValue, usize, usize)> = Vec::new();
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let SsaOp::IntConv {
dest,
operand,
target,
overflow_check,
unsigned,
..
} = instr.op()
{
if let Some(operand_val) = constants.get(operand) {
let ptr_bytes = ptr_size.bytes() as u32;
let result = if *overflow_check {
operand_val.convert_to_checked(target, *unsigned, ptr_bytes)
} else {
operand_val.convert_to(target, *unsigned, ptr_bytes)
};
if let Some(result) = result {
new_constants.push((*dest, result, block_idx, instr_idx));
}
}
}
}
}
for (dest, value, block_idx, instr_idx) in new_constants {
constants.insert(dest, value.clone());
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(instr_idx) {
let old_op_str = format!("{}", instr.op());
instr.set_op(SsaOp::Const {
dest,
value: value.clone(),
});
changes
.record(EventKind::ConstantFolded)
.at(method_token, instr_idx)
.message(format!("{old_op_str} → {value} (conv)"));
}
}
}
}
fn eliminate_redundant_conversions(
ssa: &mut SsaFunction,
method_token: Token,
changes: &mut EventLog,
) {
let mut definitions: BTreeMap<SsaVarId, ConvInfo> = BTreeMap::new();
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let SsaOp::IntConv {
dest,
operand,
target,
overflow_check,
unsigned,
..
} = instr.op()
{
definitions.insert(
*dest,
ConvInfo {
operand: *operand,
target: target.clone(),
overflow_check: *overflow_check,
unsigned: *unsigned,
block_idx,
instr_idx,
},
);
}
}
}
let mut transformations: Vec<ConvTransform> = Vec::new();
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let SsaOp::IntConv {
dest,
operand,
target,
overflow_check,
unsigned,
..
} = instr.op()
{
if *overflow_check {
continue;
}
if let Some(inner_conv) = definitions.get(operand) {
if inner_conv.overflow_check {
continue;
}
if inner_conv.target == *target && inner_conv.unsigned == *unsigned {
transformations.push(ConvTransform::ReplaceOperand {
block_idx,
instr_idx,
dest: *dest,
new_operand: inner_conv.operand,
target: target.clone(),
unsigned: *unsigned,
reason: "duplicate conversion",
});
continue;
}
if let Some(source_var) = ssa.variable(inner_conv.operand) {
let source_type = source_var.var_type();
if Self::is_safe_widening_chain(
source_type,
&inner_conv.target,
target,
inner_conv.unsigned,
*unsigned,
) {
transformations.push(ConvTransform::ReplaceOperand {
block_idx,
instr_idx,
dest: *dest,
new_operand: inner_conv.operand,
target: target.clone(),
unsigned: *unsigned,
reason: "widening chain",
});
continue;
}
}
}
if let Some(var) = ssa.variable(*operand) {
if Self::types_match(var.var_type(), target) {
transformations.push(ConvTransform::ReplaceWithCopy {
block_idx,
instr_idx,
dest: *dest,
src: *operand,
reason: "unnecessary conversion",
});
}
}
}
}
}
for transform in transformations {
match transform {
ConvTransform::ReplaceOperand {
block_idx,
instr_idx,
dest,
new_operand,
target,
unsigned,
reason,
} => {
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(instr_idx) {
let old_op_str = format!("{}", instr.op());
instr.set_op(conv_op_for_target(
dest,
new_operand,
target.clone(),
unsigned,
false,
));
changes
.record(EventKind::ConstantFolded)
.at(method_token, instr_idx)
.message(format!(
"{old_op_str} → conv.{target} {new_operand} ({reason})"
));
}
}
}
ConvTransform::ReplaceWithCopy {
block_idx,
instr_idx,
dest,
src,
reason,
} => {
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(instr_idx) {
let old_op_str = format!("{}", instr.op());
instr.set_op(SsaOp::Copy { dest, src });
changes
.record(EventKind::ConstantFolded)
.at(method_token, instr_idx)
.message(format!("{old_op_str} → copy {src} ({reason})"));
}
}
}
}
}
}
fn types_match(var_type: &SsaType, target: &SsaType) -> bool {
if var_type == target {
return true;
}
matches!(
(var_type, target),
(
SsaType::I8
| SsaType::U8
| SsaType::I16
| SsaType::U16
| SsaType::Bool
| SsaType::Char
| SsaType::U32,
SsaType::I32
) | (SsaType::I32, SsaType::U32)
| (SsaType::U64, SsaType::I64)
| (SsaType::I64, SsaType::U64)
| (SsaType::NativeInt, SsaType::NativeUInt)
| (SsaType::NativeUInt, SsaType::NativeInt)
)
}
fn is_safe_widening_chain(
source_type: &SsaType,
inner_target: &SsaType,
outer_target: &SsaType,
inner_unsigned: bool,
outer_unsigned: bool,
) -> bool {
if source_type.is_float() || inner_target.is_float() || outer_target.is_float() {
return false;
}
let source_size = source_type.size_bytes();
let inner_size = inner_target.size_bytes();
let outer_size = outer_target.size_bytes();
let (Some(source_size), Some(inner_size), Some(outer_size)) =
(source_size, inner_size, outer_size)
else {
return false; };
if source_size > inner_size {
return false;
}
if inner_size >= outer_size {
return false;
}
if inner_unsigned == outer_unsigned {
return true;
}
if inner_unsigned && !outer_unsigned {
return true;
}
false
}
#[allow(clippy::cast_possible_truncation)]
fn fold_overflow_checked_ops(
ssa: &mut SsaFunction,
constants: &mut BTreeMap<SsaVarId, ConstValue>,
method_token: Token,
changes: &mut EventLog,
ptr_size: PointerSize,
) {
let mut new_constants: Vec<(SsaVarId, ConstValue, usize, usize)> = Vec::new();
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let Some((dest, value)) =
Self::check_overflow_op(instr.op(), constants, ptr_size)
{
new_constants.push((dest, value, block_idx, instr_idx));
}
}
}
for (dest, value, block_idx, instr_idx) in new_constants {
constants.insert(dest, value.clone());
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(instr_idx) {
let old_op_str = format!("{}", instr.op());
instr.set_op(SsaOp::Const {
dest,
value: value.clone(),
});
changes
.record(EventKind::ConstantFolded)
.at(method_token, instr_idx)
.message(format!("{old_op_str} → {value} (ovf)"));
}
}
}
}
fn check_overflow_op(
op: &SsaOp,
constants: &BTreeMap<SsaVarId, ConstValue>,
ptr_size: PointerSize,
) -> Option<(SsaVarId, ConstValue)> {
let (dest, folded) = match op {
SsaOp::AddOvf {
dest,
left,
right,
unsigned,
..
} => {
let (l, r) = (constants.get(left)?, constants.get(right)?);
(dest, l.add_checked(r, *unsigned, ptr_size))
}
SsaOp::SubOvf {
dest,
left,
right,
unsigned,
..
} => {
let (l, r) = (constants.get(left)?, constants.get(right)?);
(dest, l.sub_checked(r, *unsigned, ptr_size))
}
SsaOp::MulOvf {
dest,
left,
right,
unsigned,
..
} => {
let (l, r) = (constants.get(left)?, constants.get(right)?);
(dest, l.mul_checked(r, *unsigned, ptr_size))
}
_ => return None,
};
folded.map(|value| (*dest, value))
}
fn fold_pure_calls(
ssa: &mut SsaFunction,
constants: &mut BTreeMap<SsaVarId, ConstValue>,
method_token: Token,
changes: &mut EventLog,
assembly: &CilObject,
ptr_size: PointerSize,
) {
let mut replacements: Vec<(usize, usize, SsaVarId, ConstValue)> = Vec::new();
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
let (dest, callee_token, args) = match instr.op() {
SsaOp::Call {
dest: Some(dest),
method,
args,
} => (*dest, method.token(), args),
_ => continue,
};
if !callee_token.is_table(TableId::MethodDef) {
continue;
}
let concrete_args: Option<Vec<ConstValue>> = args
.iter()
.map(|&a| {
constants
.get(&a)
.cloned()
.or_else(|| match ssa.get_definition(a) {
Some(SsaOp::Const { value, .. }) => Some(value.clone()),
_ => None,
})
})
.collect();
let Some(concrete_args) = concrete_args else {
continue;
};
let Some(result) =
Self::evaluate_pure_call(assembly, callee_token, &concrete_args, ptr_size)
else {
continue;
};
replacements.push((block_idx, instr_idx, dest, result));
}
}
for (block_idx, instr_idx, dest, value) in replacements {
constants.insert(dest, value.clone());
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(instr_idx) {
instr.set_op(SsaOp::Const { dest, value });
changes
.record(EventKind::ConstantFolded)
.at(
method_token,
block_idx.saturating_mul(1000).saturating_add(instr_idx),
)
.message("folded pure call with constant arguments");
}
}
}
if !constants.is_empty() {
Self::propagate_folded_arithmetic(ssa, constants, method_token, changes);
}
}
fn propagate_folded_arithmetic(
ssa: &mut SsaFunction,
constants: &mut BTreeMap<SsaVarId, ConstValue>,
method_token: Token,
changes: &mut EventLog,
) {
let mut new_constants: Vec<(usize, usize, SsaVarId, ConstValue)> = Vec::new();
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
if let SsaOp::Rem {
dest,
left,
right,
unsigned,
..
} = instr.op()
{
let lval = constants
.get(left)
.or_else(|| match ssa.get_definition(*left) {
Some(SsaOp::Const { value, .. }) => Some(value),
_ => None,
})
.and_then(IntOperand::from_const);
let rval = constants
.get(right)
.or_else(|| match ssa.get_definition(*right) {
Some(SsaOp::Const { value, .. }) => Some(value),
_ => None,
})
.and_then(IntOperand::from_const);
if let (Some(l), Some(r)) = (lval, rval) {
if let Some(value) = fold_rem(l, r, *unsigned) {
new_constants.push((block_idx, instr_idx, *dest, value));
}
}
}
}
}
for (block_idx, instr_idx, dest, value) in new_constants {
constants.insert(dest, value.clone());
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(instr_idx) {
instr.set_op(SsaOp::Const { dest, value });
changes
.record(EventKind::ConstantFolded)
.at(
method_token,
block_idx.saturating_mul(1000).saturating_add(instr_idx),
)
.message("folded arithmetic with constant operands");
}
}
}
}
fn evaluate_pure_call(
assembly: &CilObject,
callee_token: Token,
args: &[ConstValue],
ptr_size: PointerSize,
) -> Option<ConstValue> {
let method = assembly.method(&callee_token).ok()?;
let callee_ssa = method.ssa(assembly).ok()?;
let mut eval = SsaEvaluator::new(&callee_ssa, ptr_size);
for (var, value) in callee_ssa.argument_variables().zip(args) {
eval.set_concrete(var.id(), value.clone());
}
let trace = eval.execute(0, None, 50);
if !trace.is_complete() {
return None;
}
let last_block_idx = trace.last_block()?;
let last_block = callee_ssa.block(last_block_idx)?;
for instr in last_block.instructions() {
if let SsaOp::Return {
value: Some(ret_var),
} = instr.op()
{
return eval.get_concrete(*ret_var).cloned();
}
}
None
}
fn fold_string_operations(
ssa: &mut SsaFunction,
constants: &mut BTreeMap<SsaVarId, ConstValue>,
method_token: Token,
changes: &mut EventLog,
assembly: &CilObject,
) {
let mut new_constants: Vec<(SsaVarId, ConstValue, usize, usize)> = Vec::new();
for (block_idx, block) in ssa.iter_blocks() {
for (instr_idx, instr) in block.instructions().iter().enumerate() {
let folded = match instr.op() {
SsaOp::Call {
dest: Some(dest),
method,
args,
}
| SsaOp::CallVirt {
dest: Some(dest),
method,
args,
} => Self::try_fold_string_call(*dest, method, args, constants, assembly),
_ => None,
};
if let Some((dest, value)) = folded {
new_constants.push((dest, value, block_idx, instr_idx));
}
}
}
for (dest, value, block_idx, instr_idx) in new_constants {
constants.insert(dest, value.clone());
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(instr) = block.instructions_mut().get_mut(instr_idx) {
let old_op_str = format!("{}", instr.op());
instr.set_op(SsaOp::Const {
dest,
value: value.clone(),
});
changes
.record(EventKind::ConstantFolded)
.at(method_token, instr_idx)
.message(format!("{old_op_str} → {value} (string fold)"));
}
}
}
}
fn identify_string_op(
method: &MethodRef,
args_len: usize,
assembly: &CilObject,
) -> Option<StringFoldOp> {
let token = method.token();
if !is_method_on_type(assembly, token, "String") {
return None;
}
let name = assembly.resolve_method_name(token)?;
match (name.as_str(), args_len) {
("Concat", 2) => Some(StringFoldOp::Concat2),
("Concat", 3) => Some(StringFoldOp::Concat3),
("Concat", 4) => Some(StringFoldOp::Concat4),
("Substring", 2) => Some(StringFoldOp::SubstringFrom),
("Substring", 3) => Some(StringFoldOp::SubstringRange),
("Replace", 3) => Some(StringFoldOp::Replace),
("ToLower", 1) => Some(StringFoldOp::ToLower),
("ToUpper", 1) => Some(StringFoldOp::ToUpper),
("ToLowerInvariant", 1) => Some(StringFoldOp::ToLower),
("ToUpperInvariant", 1) => Some(StringFoldOp::ToUpper),
_ => None,
}
}
fn try_fold_string_call(
dest: SsaVarId,
method: &MethodRef,
args: &[SsaVarId],
constants: &BTreeMap<SsaVarId, ConstValue>,
assembly: &CilObject,
) -> Option<(SsaVarId, ConstValue)> {
let string_op = Self::identify_string_op(method, args.len(), assembly)?;
match string_op {
StringFoldOp::Concat2 | StringFoldOp::Concat3 | StringFoldOp::Concat4 => {
let strings: Option<Vec<String>> = args
.iter()
.map(|arg| {
constants
.get(arg)
.and_then(|v| v.as_string_content(assembly))
})
.collect();
let result = strings?.concat();
Some((dest, ConstValue::DecryptedString(result.into())))
}
StringFoldOp::SubstringFrom => {
let this_str = constants.get(args.first()?)?.as_string_content(assembly)?;
if !this_str.is_ascii() {
return None;
}
let start = constants.get(args.get(1)?)?.as_i32()? as usize;
let tail = this_str.get(start..)?;
Some((dest, ConstValue::DecryptedString(tail.into())))
}
StringFoldOp::SubstringRange => {
let this_str = constants.get(args.first()?)?.as_string_content(assembly)?;
if !this_str.is_ascii() {
return None;
}
let start = constants.get(args.get(1)?)?.as_i32()? as usize;
let len = constants.get(args.get(2)?)?.as_i32()? as usize;
let end = start.checked_add(len)?;
let slice = this_str.get(start..end)?;
Some((dest, ConstValue::DecryptedString(slice.into())))
}
StringFoldOp::Replace => {
let this_str = constants.get(args.first()?)?.as_string_content(assembly)?;
let old = constants.get(args.get(1)?)?.as_string_content(assembly)?;
let new = constants.get(args.get(2)?)?.as_string_content(assembly)?;
Some((
dest,
ConstValue::DecryptedString(this_str.replace(&old, &new).into()),
))
}
StringFoldOp::ToLower => {
let this_str = constants.get(args.first()?)?.as_string_content(assembly)?;
Some((
dest,
ConstValue::DecryptedString(this_str.to_lowercase().into()),
))
}
StringFoldOp::ToUpper => {
let this_str = constants.get(args.first()?)?.as_string_content(assembly)?;
Some((
dest,
ConstValue::DecryptedString(this_str.to_uppercase().into()),
))
}
}
}
#[allow(clippy::cast_possible_truncation)]
fn apply_constant_folding(
ssa: &mut SsaFunction,
constants: &BTreeMap<SsaVarId, ConstValue>,
sccp_result: &SccpResult,
method_token: Token,
changes: &mut EventLog,
) {
for block_idx in 0..ssa.block_count() {
if !sccp_result.is_block_executable(block_idx) {
continue;
}
if let Some(block) = ssa.block_mut(block_idx) {
for (instr_idx, instr) in block.instructions_mut().iter_mut().enumerate() {
let op = instr.op();
if matches!(op, SsaOp::Const { .. }) {
continue;
}
if let Some(dest) = op.dest() {
if let Some(value) = constants.get(&dest) {
if matches!(value, ConstValue::DecryptedArray { .. }) {
continue;
}
let old_op_str = format!("{op}");
instr.set_op(SsaOp::Const {
dest,
value: value.clone(),
});
changes
.record(EventKind::ConstantFolded)
.at(method_token, instr_idx)
.message(format!("{old_op_str} → {value}"));
}
}
}
}
}
}
fn simplify_involutory_ops(ssa: &mut SsaFunction, method_token: Token, changes: &mut EventLog) {
let mut definitions: BTreeMap<SsaVarId, (usize, usize)> = BTreeMap::new();
let mut use_counts: BTreeMap<SsaVarId, usize> = BTreeMap::new();
for (block_idx, instr_idx, instr) in ssa.iter_instructions() {
if let Some(dest) = instr.op().dest() {
definitions.insert(dest, (block_idx, instr_idx));
}
for use_var in instr.op().uses() {
let slot = use_counts.entry(use_var).or_default();
*slot = slot.saturating_add(1);
}
}
for phi in ssa.all_phi_nodes() {
for operand in phi.operands() {
let slot = use_counts.entry(operand.value()).or_default();
*slot = slot.saturating_add(1);
}
}
let mut neg_operands = BitSet::new(ssa.var_id_capacity());
let mut not_operands = BitSet::new(ssa.var_id_capacity());
for (_, _, instr) in ssa.iter_instructions() {
match instr.op() {
SsaOp::Neg { operand, .. } => {
neg_operands.insert(operand.index());
}
SsaOp::Not { operand, .. } => {
not_operands.insert(operand.index());
}
_ => {}
}
}
struct ChainTransform {
outermost_dest: SsaVarId,
innermost_operand: SsaVarId,
chain_length: usize,
instructions_to_nop: Vec<(usize, usize)>,
outermost_location: (usize, usize),
is_neg: bool,
}
let mut processed = BitSet::new(ssa.var_id_capacity());
let mut transforms: Vec<ChainTransform> = Vec::new();
for (block_idx, instr_idx, instr) in ssa.iter_instructions() {
let (dest, operand, is_neg) = match instr.op() {
SsaOp::Neg { dest, operand, .. } => (*dest, *operand, true),
SsaOp::Not { dest, operand, .. } => (*dest, *operand, false),
_ => continue,
};
if processed.contains(dest.index()) {
continue;
}
let is_outermost = if is_neg {
!neg_operands.contains(dest.index())
} else {
!not_operands.contains(dest.index())
};
if !is_outermost {
continue;
}
let mut chain_locations: Vec<(usize, usize)> = vec![(block_idx, instr_idx)];
let mut chain_dests: Vec<SsaVarId> = vec![dest];
let mut current_operand = operand;
let mut all_intermediates_single_use = true;
loop {
let uses = use_counts.get(¤t_operand).copied().unwrap_or(0);
if uses != 1 {
all_intermediates_single_use = false;
break;
}
let Some(&(def_block, def_instr)) = definitions.get(¤t_operand) else {
break;
};
let Some(def_block_ref) = ssa.block(def_block) else {
break;
};
let Some(def_instr_ref) = def_block_ref.instructions().get(def_instr) else {
break;
};
let inner = match def_instr_ref.op() {
SsaOp::Neg {
dest: d,
operand: inner,
..
} if is_neg => (*d, *inner),
SsaOp::Not {
dest: d,
operand: inner,
..
} if !is_neg => (*d, *inner),
_ => break,
};
chain_locations.push((def_block, def_instr));
chain_dests.push(inner.0);
current_operand = inner.1;
}
for d in &chain_dests {
processed.insert(d.index());
}
let chain_len = chain_locations.len();
if chain_len < 2 || !all_intermediates_single_use {
continue;
}
transforms.push(ChainTransform {
outermost_dest: dest,
innermost_operand: current_operand,
chain_length: chain_len,
instructions_to_nop: chain_locations,
outermost_location: (block_idx, instr_idx),
is_neg,
});
}
for t in transforms {
let op_name = if t.is_neg { "neg" } else { "not" };
if t.chain_length % 2 == 0 {
ssa.replace_uses_including_phis(t.outermost_dest, t.innermost_operand);
for &(b, i) in &t.instructions_to_nop {
ssa.replace_instruction_op(b, i, SsaOp::Nop);
}
changes
.record(EventKind::ConstantFolded)
.at(method_token, t.outermost_location.1)
.message(format!(
"{} → {} ({op_name}^{}(x))",
t.outermost_dest, t.innermost_operand, t.chain_length
));
} else {
let (b, i) = t.outermost_location;
if let Some(block) = ssa.block_mut(b) {
if let Some(instr) = block.instructions_mut().get_mut(i) {
if t.is_neg {
instr.set_op(SsaOp::Neg {
dest: t.outermost_dest,
operand: t.innermost_operand,
flags: None,
});
} else {
instr.set_op(SsaOp::Not {
dest: t.outermost_dest,
operand: t.innermost_operand,
flags: None,
});
}
}
}
if let Some(rest) = t.instructions_to_nop.get(1..) {
for &(b, i) in rest {
ssa.replace_instruction_op(b, i, SsaOp::Nop);
}
}
changes
.record(EventKind::ConstantFolded)
.at(method_token, t.outermost_location.1)
.message(format!(
"{} = {op_name}({}) ({op_name}^{}(x))",
t.outermost_dest, t.innermost_operand, t.chain_length
));
}
}
}
fn is_loop_header(ssa: &SsaFunction, block_idx: usize) -> bool {
if let Some(block) = ssa.block(block_idx) {
if let Some(op) = block.control_terminator() {
let self_targets = match op {
SsaOp::Switch {
targets, default, ..
} => targets.contains(&block_idx) || *default == block_idx,
_ => false,
};
if self_targets {
return true;
}
}
}
for bi in block_idx.saturating_add(1)..ssa.block_count() {
if let Some(block) = ssa.block(bi) {
if let Some(op) = block.control_terminator() {
let targets_block = match op {
SsaOp::Jump { target } => *target == block_idx,
SsaOp::Leave { target } => *target == block_idx,
SsaOp::Branch {
true_target,
false_target,
..
} => *true_target == block_idx || *false_target == block_idx,
SsaOp::BranchCmp {
true_target,
false_target,
..
} => *true_target == block_idx || *false_target == block_idx,
SsaOp::Switch {
targets, default, ..
} => targets.contains(&block_idx) || *default == block_idx,
_ => false,
};
if targets_block {
return true;
}
}
}
}
false
}
fn simplify_control_flow(
ssa: &mut SsaFunction,
constants: &BTreeMap<SsaVarId, ConstValue>,
sccp_result: &SccpResult,
method_token: Token,
changes: &mut EventLog,
) {
for block_idx in 0..ssa.block_count() {
if !sccp_result.is_block_executable(block_idx) {
continue;
}
let simplification = if let Some(block) = ssa.block(block_idx) {
if let Some(op) = block.control_terminator() {
match op {
SsaOp::Branch {
condition,
true_target,
false_target,
} => {
if let Some(c) = constants.get(condition) {
if let Some(is_true) = c.as_bool() {
let target = if is_true { *true_target } else { *false_target };
Some((SsaOp::Jump { target }, target))
} else {
None
}
} else {
None
}
}
SsaOp::Switch {
value,
targets,
default,
} => {
if ssa.is_preserved_dispatch_var(*value) {
None
} else if Self::is_loop_header(ssa, block_idx) {
None
} else if let Some(c) = constants.get(value) {
if let Some(idx) = c.as_i32() {
let target = usize::try_from(idx)
.ok()
.and_then(|i| targets.get(i).copied())
.unwrap_or(*default);
Some((SsaOp::Jump { target }, target))
} else {
None
}
} else {
None
}
}
SsaOp::BranchCmp {
left,
right,
cmp,
unsigned,
true_target,
false_target,
} => {
if let (Some(left_val), Some(right_val)) =
(constants.get(left), constants.get(right))
{
let result = if *unsigned {
Self::eval_cmp_unsigned(*cmp, left_val, right_val)
} else {
Self::eval_cmp_signed(*cmp, left_val, right_val)
};
if let Some(is_true) = result {
let target = if is_true { *true_target } else { *false_target };
Some((SsaOp::Jump { target }, target))
} else {
None
}
} else {
None
}
}
_ => None,
}
} else {
None
}
} else {
None
};
if let Some((new_op, target)) = simplification {
if let Some(block) = ssa.block_mut(block_idx) {
if let Some(last_instr) = block.instructions_mut().last_mut() {
last_instr.set_op(new_op);
changes
.record(EventKind::BranchSimplified)
.at(method_token, block_idx)
.message(format!("simplified to unconditional branch to {target}"));
}
}
}
}
}
fn eval_cmp_signed(cmp: CmpKind, left: &ConstValue, right: &ConstValue) -> Option<bool> {
let l = left.as_i64()?;
let r = right.as_i64()?;
Some(match cmp {
CmpKind::Eq => l == r,
CmpKind::Ne => l != r,
CmpKind::Lt => l < r,
CmpKind::Le => l <= r,
CmpKind::Gt => l > r,
CmpKind::Ge => l >= r,
})
}
fn eval_cmp_unsigned(cmp: CmpKind, left: &ConstValue, right: &ConstValue) -> Option<bool> {
let l = left.as_u64()?;
let r = right.as_u64()?;
Some(match cmp {
CmpKind::Eq => l == r,
CmpKind::Ne => l != r,
CmpKind::Lt => l < r,
CmpKind::Le => l <= r,
CmpKind::Gt => l > r,
CmpKind::Ge => l >= r,
})
}
}
impl SsaPass<CilTarget, CompilerContext> for ConstantPropagationPass {
fn name(&self) -> &'static str {
"constant-propagation"
}
fn description(&self) -> &'static str {
"Propagates constant values and folds constant expressions using SCCP"
}
fn modification_scope(&self) -> ModificationScope {
ModificationScope::InstructionsOnly
}
fn run_on_method(
&self,
ssa: &mut SsaFunction,
method: &MethodRef,
host: &CompilerContext,
) -> analyssa::Result<bool> {
let assembly = host
.assembly()
.ok_or_else(|| analyssa::Error::new("ConstantPropagationPass requires an assembly"))?;
let method_token = method.0;
let mut changes = EventLog::new();
let ptr_size = PointerSize::from_is_64bit(assembly.file().pe().is_64bit);
let constants = Self::run_constant_propagation(
ssa,
method_token,
&mut changes,
ptr_size,
self.max_iterations,
&assembly,
);
for (var, value) in &constants {
host.add_known_value(method_token, *var, value.clone());
}
let changed = !changes.is_empty();
if changed {
host.events.merge(&changes);
}
Ok(changed)
}
}
#[cfg(test)]
mod tests;