use std::collections::{BTreeMap, BTreeSet};
use rustc_hash::FxHashMap;
use crate::{
analysis::{
CmpKind, ConstValue, PhiNode, SsaCfg, SsaFunction, SsaInstruction, SsaOp, SsaVarId,
},
deobfuscation::passes::unflattening::dispatcher::Dispatcher,
};
const MAX_FOLD_DEPTH: usize = 24;
const MAX_OVERFLOW_CHAIN: usize = 4096;
const MAX_MERGE_DEPTH: usize = 8;
const MAX_CHAIN_LEN: usize = 16;
const MAX_PROPAGATED_STATES: usize = 4096;
const MAX_REGION: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rewire {
pub from: usize,
pub old: usize,
pub new: usize,
pub state: i64,
}
#[derive(Debug, Clone, Default)]
pub struct ResolutionStats {
pub resolved: usize,
pub unresolved: usize,
pub conflicts: usize,
pub overflow_entries: usize,
pub table_size: usize,
pub reasons: UnresolvedReasons,
}
#[derive(Debug, Clone, Default)]
pub struct UnresolvedReasons {
pub no_target: usize,
pub not_constant: usize,
pub impure_chain: usize,
}
pub struct DispatchTable {
cases: Vec<usize>,
switch_var: SsaVarId,
state_var: SsaVarId,
overflow: BTreeMap<i64, usize>,
fallthrough: Option<usize>,
executable: Vec<bool>,
}
impl DispatchTable {
pub fn build(
ssa: &SsaFunction,
dispatcher: &Dispatcher,
state_var: SsaVarId,
folder: &mut StateFolder<'_>,
) -> Self {
let mut table = Self {
cases: dispatcher.cases.clone(),
switch_var: dispatcher.switch_var,
state_var,
overflow: BTreeMap::new(),
fallthrough: None,
executable: ssa
.blocks()
.iter()
.map(|block| !block.instructions().is_empty())
.collect(),
};
table.walk_overflow_chain(ssa, dispatcher.default, folder);
table
}
fn is_executable(&self, block: usize) -> bool {
self.executable.get(block).copied().unwrap_or(false)
}
fn walk_overflow_chain(
&mut self,
ssa: &SsaFunction,
default: usize,
folder: &mut StateFolder<'_>,
) {
let mut current = default;
let mut seen: BTreeSet<usize> = BTreeSet::new();
for _ in 0..MAX_OVERFLOW_CHAIN {
if !seen.insert(current) {
return;
}
let Some(block) = ssa.block(current) else {
return;
};
let Some(SsaOp::BranchCmp {
left,
right,
cmp: CmpKind::Eq,
true_target,
false_target,
..
}) = block.control_terminator()
else {
self.fallthrough = Some(current);
return;
};
let left_const = folder.fold(*left);
let right_const = folder.fold(*right);
let value = match (left_const, right_const) {
(Some(v), None) | (None, Some(v)) => v,
_ => {
self.fallthrough = Some(current);
return;
}
};
self.overflow.entry(value).or_insert(*true_target);
current = *false_target;
}
}
pub fn lookup(&self, folder: &mut StateFolder<'_>, state: StateValue) -> Option<usize> {
let target = if let Some(&target) = self.overflow.get(&state.value) {
target
} else {
let index = folder.fold_with(self.switch_var, self.state_var, state)?;
let index = usize::try_from(index.value).ok()?;
self.cases.get(index).copied().or(self.fallthrough)?
};
self.is_executable(target).then_some(target)
}
#[must_use]
pub fn len(&self) -> usize {
self.cases.len().saturating_add(self.overflow.len())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.cases.is_empty() && self.overflow.is_empty()
}
#[must_use]
pub fn overflow_len(&self) -> usize {
self.overflow.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct StateValue {
pub value: i64,
pub wide: bool,
}
impl StateValue {
fn narrow(value: i64) -> Self {
Self {
value: truncate32(value),
wide: false,
}
}
fn wide(value: i64) -> Self {
Self { value, wide: true }
}
fn rewrap(self, value: i64) -> Self {
if self.wide {
Self::wide(value)
} else {
Self::narrow(value)
}
}
}
#[allow(clippy::cast_possible_truncation)]
fn truncate32(value: i64) -> i64 {
i64::from(value as i32)
}
fn shift_amount(amount: i64, wide: bool) -> Option<u32> {
let mask: u32 = if wide { 63 } else { 31 };
u32::try_from(amount).ok().map(|a| a & mask)
}
#[allow(clippy::cast_possible_truncation)]
fn unsigned_bits(value: i64, wide: bool) -> u64 {
if wide {
value.cast_unsigned()
} else {
u64::from(value as u32)
}
}
enum Folded {
Value(StateValue),
Forward(SsaVarId),
Binary(SsaVarId, SsaVarId, BinKind),
Unary(SsaVarId, UnKind),
Opaque,
}
#[derive(Clone, Copy)]
enum BinKind {
Add,
Sub,
Mul,
Div { unsigned: bool },
Rem { unsigned: bool },
And,
Or,
Xor,
Shl,
Shr { unsigned: bool },
}
#[derive(Clone, Copy)]
enum UnKind {
Neg,
Not,
}
pub struct StateFolder<'a> {
ssa: &'a SsaFunction,
memo: FxHashMap<(Bindings, SsaVarId), Option<StateValue>>,
bindings: Bindings,
}
type Bindings = [Option<(SsaVarId, StateValue)>; 2];
impl<'a> StateFolder<'a> {
#[must_use]
pub fn new(ssa: &'a SsaFunction) -> Self {
Self {
ssa,
memo: FxHashMap::default(),
bindings: [None, None],
}
}
pub fn fold(&mut self, var: SsaVarId) -> Option<i64> {
self.fold_value(var).map(|v| v.value)
}
pub fn fold_value(&mut self, var: SsaVarId) -> Option<StateValue> {
self.bindings = [None, None];
self.fold_at(var, 0)
}
pub fn fold_bound(&mut self, var: SsaVarId, bindings: Bindings) -> Option<StateValue> {
self.bindings = bindings;
self.fold_at(var, 0)
}
pub fn fold_with(
&mut self,
var: SsaVarId,
state_var: SsaVarId,
state: StateValue,
) -> Option<StateValue> {
self.bindings = [Some((state_var, state)), None];
self.fold_at(var, 0)
}
fn fold_at(&mut self, var: SsaVarId, depth: usize) -> Option<StateValue> {
if depth > MAX_FOLD_DEPTH {
return None;
}
let key = (self.bindings, var);
if let Some(&cached) = self.memo.get(&key) {
return cached;
}
self.memo.insert(key, None);
let result = match self.classify(var) {
Folded::Value(v) => Some(v),
Folded::Forward(src) => self.fold_at(src, depth.saturating_add(1)),
Folded::Unary(src, kind) => {
let v = self.fold_at(src, depth.saturating_add(1))?;
Some(v.rewrap(match kind {
UnKind::Neg => v.value.wrapping_neg(),
UnKind::Not => !v.value,
}))
}
Folded::Binary(left, right, kind) => {
let l = self.fold_at(left, depth.saturating_add(1))?;
let r = self.fold_at(right, depth.saturating_add(1))?;
apply_binary(l, r, kind)
}
Folded::Opaque => None,
};
self.memo.insert(key, result);
result
}
fn copy_root(&self, var: SsaVarId) -> SsaVarId {
let mut current = var;
for _ in 0..MAX_FOLD_DEPTH {
match self.classify(current) {
Folded::Forward(src) => current = src,
_ => break,
}
}
current
}
fn classify(&self, var: SsaVarId) -> Folded {
for pinned in self.bindings.iter().flatten() {
if pinned.0 == var {
return Folded::Value(pinned.1);
}
}
let Some(variable) = self.ssa.variable(var) else {
return Folded::Opaque;
};
let site = variable.def_site();
let Some(index) = site.instruction else {
return Folded::Opaque;
};
let Some(op) = self
.ssa
.block(site.block)
.and_then(|b| b.instructions().get(index))
.map(|i| i.op())
else {
return Folded::Opaque;
};
match op {
SsaOp::Const { value, .. } => value.as_i64().map_or(Folded::Opaque, |v| {
let wide = matches!(
value,
ConstValue::I64(_)
| ConstValue::U64(_)
| ConstValue::NativeInt(_)
| ConstValue::NativeUInt(_)
);
Folded::Value(if wide {
StateValue::wide(v)
} else {
StateValue::narrow(v)
})
}),
SsaOp::Copy { src, .. } => Folded::Forward(*src),
SsaOp::IntConv { operand, .. } => Folded::Forward(*operand),
SsaOp::Add { left, right, .. } => Folded::Binary(*left, *right, BinKind::Add),
SsaOp::Sub { left, right, .. } => Folded::Binary(*left, *right, BinKind::Sub),
SsaOp::Mul { left, right, .. } => Folded::Binary(*left, *right, BinKind::Mul),
SsaOp::And { left, right, .. } => Folded::Binary(*left, *right, BinKind::And),
SsaOp::Or { left, right, .. } => Folded::Binary(*left, *right, BinKind::Or),
SsaOp::Xor { left, right, .. } => Folded::Binary(*left, *right, BinKind::Xor),
SsaOp::Shl { value, amount, .. } => Folded::Binary(*value, *amount, BinKind::Shl),
SsaOp::Shr {
value,
amount,
unsigned,
..
} => Folded::Binary(
*value,
*amount,
BinKind::Shr {
unsigned: *unsigned,
},
),
SsaOp::Div {
left,
right,
unsigned,
..
} => Folded::Binary(
*left,
*right,
BinKind::Div {
unsigned: *unsigned,
},
),
SsaOp::Rem {
left,
right,
unsigned,
..
} => Folded::Binary(
*left,
*right,
BinKind::Rem {
unsigned: *unsigned,
},
),
SsaOp::Neg { operand, .. } => Folded::Unary(*operand, UnKind::Neg),
SsaOp::Not { operand, .. } => Folded::Unary(*operand, UnKind::Not),
_ => Folded::Opaque,
}
}
}
fn apply_binary(left: StateValue, right: StateValue, kind: BinKind) -> Option<StateValue> {
let out = StateValue {
value: 0,
wide: left.wide || right.wide,
};
let (l, r) = (left.value, right.value);
let value = match kind {
BinKind::Add => l.wrapping_add(r),
BinKind::Sub => l.wrapping_sub(r),
BinKind::Mul => l.wrapping_mul(r),
BinKind::And => l & r,
BinKind::Or => l | r,
BinKind::Xor => l ^ r,
BinKind::Shl => {
let amount = shift_amount(r, out.wide)?;
l.wrapping_shl(amount)
}
BinKind::Shr { unsigned } => {
let amount = shift_amount(r, out.wide)?;
if unsigned {
let bits = if out.wide {
l.cast_unsigned()
} else {
u64::from(l.cast_unsigned() as u32)
};
bits.wrapping_shr(amount).cast_signed()
} else {
l.wrapping_shr(amount)
}
}
BinKind::Div { unsigned } => {
if r == 0 {
return None;
}
if unsigned {
unsigned_bits(l, out.wide)
.checked_div(unsigned_bits(r, out.wide))?
.cast_signed()
} else {
l.checked_div(r)?
}
}
BinKind::Rem { unsigned } => {
if r == 0 {
return None;
}
if unsigned {
unsigned_bits(l, out.wide)
.checked_rem(unsigned_bits(r, out.wide))?
.cast_signed()
} else {
l.checked_rem(r)?
}
}
};
Some(out.rewrap(value))
}
fn dependency_phi<'s>(
ssa: &'s SsaFunction,
folder: &StateFolder<'_>,
value: SsaVarId,
) -> Option<(usize, &'s PhiNode)> {
let mut frontier = vec![value];
let mut seen: BTreeSet<SsaVarId> = BTreeSet::new();
for _ in 0..MAX_FOLD_DEPTH {
let mut next = Vec::new();
for var in frontier.drain(..) {
if !seen.insert(var) {
continue;
}
if let Some(found) = ssa.find_phi_defining(var) {
return Some(found);
}
match folder.classify(var) {
Folded::Forward(src) | Folded::Unary(src, _) => next.push(src),
Folded::Binary(left, right, _) => {
next.push(left);
next.push(right);
}
Folded::Value(_) | Folded::Opaque => {}
}
}
if next.is_empty() {
break;
}
frontier = next;
}
None
}
fn pure_chain_between(
ssa: &SsaFunction,
start: usize,
end: usize,
state_only: &BTreeSet<SsaVarId>,
) -> bool {
let mut chain: Vec<usize> = Vec::new();
let mut current = start;
for _ in 0..MAX_CHAIN_LEN {
if current == end {
return chain
.iter()
.all(|&block| defs_stay_within(ssa, block, &chain, end));
}
let Some(block) = ssa.block(current) else {
return false;
};
let Some((terminator, body)) = block.instructions().split_last() else {
return false;
};
if !body.iter().all(|instr| is_skippable(instr, state_only)) {
return false;
}
match terminator.op() {
SsaOp::Jump { target } => {
chain.push(current);
current = *target;
}
_ => return false,
}
}
false
}
fn is_skippable(instr: &SsaInstruction, state_only: &BTreeSet<SsaVarId>) -> bool {
match instr.def() {
Some(def) => instr.is_pure() && state_only.contains(&def),
None => matches!(instr.op(), SsaOp::Nop | SsaOp::Pop { .. }),
}
}
fn defs_stay_within(ssa: &SsaFunction, block: usize, chain: &[usize], end: usize) -> bool {
let Some(ssa_block) = ssa.block(block) else {
return false;
};
ssa_block
.instructions()
.iter()
.filter_map(|instr| instr.def())
.all(|def| {
ssa.variable(def).is_none_or(|variable| {
variable
.uses()
.iter()
.all(|site| site.block == end || chain.contains(&site.block))
})
})
}
fn state_phi_at<'s>(ssa: &'s SsaFunction, dispatcher: &Dispatcher) -> Option<&'s PhiNode> {
let block = ssa.block(dispatcher.block)?;
if let Some(state) = dispatcher.state_phi {
if let Some(phi) = block.phi_nodes().iter().find(|p| p.result() == state) {
return Some(phi);
}
}
if let Some(phi) = block
.phi_nodes()
.iter()
.find(|p| p.result() == dispatcher.switch_var)
{
return Some(phi);
}
let folder = StateFolder::new(ssa);
let mut frontier = vec![dispatcher.switch_var];
let mut seen: BTreeSet<SsaVarId> = BTreeSet::new();
for _ in 0..MAX_FOLD_DEPTH {
let mut next = Vec::new();
for var in frontier.drain(..) {
if !seen.insert(var) {
continue;
}
if let Some(phi) = block.phi_nodes().iter().find(|p| p.result() == var) {
return Some(phi);
}
match folder.classify(var) {
Folded::Forward(src) | Folded::Unary(src, _) => next.push(src),
Folded::Binary(left, right, _) => {
next.push(left);
next.push(right);
}
Folded::Value(_) | Folded::Opaque => {}
}
}
if next.is_empty() {
break;
}
frontier = next;
}
None
}
fn state_only_values(ssa: &SsaFunction, dispatcher_block: usize) -> BTreeSet<SsaVarId> {
let mut state_only: BTreeSet<SsaVarId> = ssa
.variables()
.iter()
.map(|variable| variable.id())
.collect();
loop {
let mut struck = false;
for (index, block) in ssa.iter_blocks() {
if index == dispatcher_block {
continue;
}
for instr in block.instructions() {
if instr.def().is_some_and(|def| state_only.contains(&def)) {
continue;
}
for used in instr.uses() {
if state_only.remove(&used) {
struck = true;
}
}
}
}
if !struck {
break;
}
}
state_only
}
fn dispatcher_is_transparent(
ssa: &SsaFunction,
dispatcher_block: usize,
state_only: &BTreeSet<SsaVarId>,
) -> bool {
let Some(block) = ssa.block(dispatcher_block) else {
return false;
};
let Some((_terminator, body)) = block.instructions().split_last() else {
return false;
};
body.iter()
.filter_map(SsaInstruction::def)
.all(|def| state_only.contains(&def))
}
fn region_from(ssa: &SsaFunction, start: usize, stop: usize, budget: usize) -> BTreeSet<usize> {
let mut seen = BTreeSet::new();
if start == stop {
return seen;
}
let mut frontier = vec![start];
while let Some(current) = frontier.pop() {
if current == stop || !seen.insert(current) {
continue;
}
if seen.len() > budget {
break;
}
if let Some(op) = ssa.block(current).and_then(|b| b.control_terminator()) {
frontier.extend(op.successors());
}
}
seen
}
pub fn resolve_dispatch_edges(
ssa: &SsaFunction,
dispatcher: &Dispatcher,
) -> (Vec<Rewire>, ResolutionStats) {
let mut folder = StateFolder::new(ssa);
let mut stats = ResolutionStats::default();
let Some(phi) = state_phi_at(ssa, dispatcher) else {
stats.unresolved = SsaCfg::from_ssa(ssa)
.block_predecessors(dispatcher.block)
.len();
return (Vec::new(), stats);
};
let state_only = state_only_values(ssa, dispatcher.block);
let table = DispatchTable::build(ssa, dispatcher, phi.result(), &mut folder);
stats.overflow_entries = table.overflow_len();
stats.table_size = table.len();
let mut rewires: Vec<Rewire> = Vec::new();
let mut states: Vec<StateValue> = Vec::new();
let mut visited: BTreeSet<usize> = BTreeSet::new();
let mut unresolved: BTreeSet<usize> = BTreeSet::new();
resolve_merge(
ssa,
&table,
&mut folder,
dispatcher.block,
phi,
dispatcher.block,
0,
&mut visited,
&mut rewires,
&mut states,
&mut unresolved,
&mut stats.reasons,
None,
None,
&state_only,
);
if !unresolved.is_empty() {
let (recovered, covered) = propagate_states(
ssa,
&table,
dispatcher,
phi,
&mut folder,
&states,
&state_only,
);
for pred in covered {
unresolved.remove(&pred);
}
rewires.extend(recovered);
}
let rewires = drop_conflicts(rewires, &mut stats);
stats.unresolved = unresolved.len();
if stats.unresolved > 0 && !dispatcher_is_transparent(ssa, dispatcher.block, &state_only) {
log::debug!(
"CFF resolve b{}: {} edge(s) unresolved and the dispatcher decodes state \
in its body, so none are rewired",
dispatcher.block,
stats.unresolved
);
stats.resolved = 0;
return (Vec::new(), stats);
}
stats.resolved = rewires.len();
(rewires, stats)
}
fn propagate_states(
ssa: &SsaFunction,
table: &DispatchTable,
dispatcher: &Dispatcher,
phi: &PhiNode,
folder: &mut StateFolder<'_>,
seeds: &[StateValue],
state_only: &BTreeSet<SsaVarId>,
) -> (Vec<Rewire>, BTreeSet<usize>) {
let state_var = phi.result();
let operands: Vec<(usize, SsaVarId)> = phi
.operands()
.iter()
.map(|op| (op.predecessor(), op.value()))
.collect();
let mut worklist: Vec<StateValue> = seeds.to_vec();
if let Some(initial) = dispatcher.initial_state {
worklist.push(StateValue::narrow(initial));
}
for &(_, value) in &operands {
if let Some(state) = folder.fold_value(value) {
worklist.push(state);
}
}
let mut merged: Vec<Rewire> = Vec::new();
let mut covered: BTreeSet<usize> = BTreeSet::new();
let mut reaching: BTreeMap<usize, BTreeSet<i64>> = BTreeMap::new();
let mut outcome: BTreeMap<(usize, i64), StateValue> = BTreeMap::new();
let mut seen: BTreeSet<i64> = BTreeSet::new();
while let Some(state) = worklist.pop() {
if seen.len() >= MAX_PROPAGATED_STATES || !seen.insert(state.value) {
continue;
}
let Some(target) = table.lookup(folder, state) else {
continue;
};
let region = region_from(ssa, target, dispatcher.block, MAX_REGION);
for &(pred, value) in &operands {
if !region.contains(&pred) {
continue;
}
reaching.entry(pred).or_default().insert(state.value);
if let Some(next) = folder.fold_with(value, state_var, state) {
outcome.insert((pred, state.value), next);
worklist.push(next);
continue;
}
let Some((inner_block, inner_phi)) = dependency_phi(ssa, folder, value) else {
continue;
};
if inner_block == dispatcher.block
|| !pure_chain_between(ssa, inner_block, dispatcher.block, state_only)
{
continue;
}
let inner: Vec<(usize, SsaVarId)> = inner_phi
.operands()
.iter()
.map(|op| (op.predecessor(), op.value()))
.collect();
let phi_result = inner_phi.result();
let pinned = Some((state_var, state));
for (source, operand) in inner {
let Some(branch) = folder.fold_bound(operand, [pinned, None]) else {
continue;
};
let Some(next) = folder.fold_bound(value, [pinned, Some((phi_result, branch))])
else {
continue;
};
let Some(target) = table.lookup(folder, next) else {
continue;
};
merged.push(Rewire {
from: source,
old: inner_block,
new: target,
state: next.value,
});
covered.insert(pred);
worklist.push(next);
}
}
}
let mut rewires: Vec<Rewire> = merged;
for (pred, states) in reaching {
for state in states {
let Some(&next) = outcome.get(&(pred, state)) else {
continue;
};
let Some(next_target) = table.lookup(folder, next) else {
continue;
};
covered.insert(pred);
rewires.push(Rewire {
from: pred,
old: dispatcher.block,
new: next_target,
state: next.value,
});
}
}
(rewires, covered)
}
#[must_use]
pub fn merge_rewires(per_dispatcher: Vec<Vec<Rewire>>) -> (Vec<Rewire>, usize) {
let mut stats = ResolutionStats::default();
let combined: Vec<Rewire> = per_dispatcher.into_iter().flatten().collect();
let merged = drop_conflicts(combined, &mut stats);
(merged, stats.conflicts)
}
#[allow(clippy::too_many_arguments)]
fn resolve_merge(
ssa: &SsaFunction,
table: &DispatchTable,
folder: &mut StateFolder<'_>,
merge_block: usize,
phi: &PhiNode,
dispatcher_block: usize,
depth: usize,
visited: &mut BTreeSet<usize>,
rewires: &mut Vec<Rewire>,
states: &mut Vec<StateValue>,
unresolved: &mut BTreeSet<usize>,
reasons: &mut UnresolvedReasons,
root: Option<usize>,
outer: Option<(SsaVarId, StateValue)>,
state_only: &BTreeSet<SsaVarId>,
) {
if !visited.insert(merge_block) {
return;
}
let operands: Vec<(usize, SsaVarId)> = phi
.operands()
.iter()
.map(|op| (op.predecessor(), op.value()))
.collect();
for (pred, value) in operands {
let blame = root.unwrap_or(pred);
if let Some(state) = folder.fold_bound(value, [outer, None]) {
if let Some(target) = table.lookup(folder, state) {
states.push(state);
rewires.push(Rewire {
from: pred,
old: merge_block,
new: target,
state: state.value,
});
} else {
unresolved.insert(blame);
reasons.no_target = reasons.no_target.saturating_add(1);
}
continue;
}
let Some((inner_block, inner_phi)) = dependency_phi(ssa, folder, value) else {
unresolved.insert(blame);
reasons.not_constant = reasons.not_constant.saturating_add(1);
continue;
};
if depth >= MAX_MERGE_DEPTH
|| inner_block == merge_block
|| !pure_chain_between(ssa, inner_block, dispatcher_block, state_only)
{
unresolved.insert(blame);
reasons.impure_chain = reasons.impure_chain.saturating_add(1);
continue;
}
let operands: Vec<(usize, SsaVarId)> = inner_phi
.operands()
.iter()
.map(|op| (op.predecessor(), op.value()))
.collect();
let phi_result = inner_phi.result();
let mut split_any = false;
for (source, operand) in operands {
let Some(branch) = folder.fold_bound(operand, [outer, None]) else {
continue;
};
let Some(state) = folder.fold_bound(value, [outer, Some((phi_result, branch))]) else {
continue;
};
let Some(target) = table.lookup(folder, state) else {
continue;
};
states.push(state);
rewires.push(Rewire {
from: source,
old: inner_block,
new: target,
state: state.value,
});
split_any = true;
}
if !split_any {
unresolved.insert(blame);
reasons.not_constant = reasons.not_constant.saturating_add(1);
}
}
}
fn drop_conflicts(rewires: Vec<Rewire>, stats: &mut ResolutionStats) -> Vec<Rewire> {
let mut chosen: BTreeMap<(usize, usize), Rewire> = BTreeMap::new();
let mut conflicted: BTreeSet<(usize, usize)> = BTreeSet::new();
for rewire in rewires {
let key = (rewire.from, rewire.old);
match chosen.get(&key) {
Some(existing) if existing.new != rewire.new => {
conflicted.insert(key);
}
Some(_) => {}
None => {
chosen.insert(key, rewire);
}
}
}
for key in &conflicted {
chosen.remove(key);
stats.conflicts = stats.conflicts.saturating_add(1);
stats.unresolved = stats.unresolved.saturating_add(1);
}
chosen.into_values().collect()
}
fn is_state_machinery(instr: &SsaInstruction) -> bool {
if let SsaOp::Const { value, .. } = instr.op() {
return value.as_i64().is_some();
}
instr.is_pure()
|| matches!(
instr.op(),
SsaOp::Jump { .. }
| SsaOp::Leave { .. }
| SsaOp::Switch { .. }
| SsaOp::Branch { .. }
| SsaOp::BranchCmp { .. }
)
}
pub fn clear_unreachable(ssa: &mut SsaFunction) -> usize {
let block_count = ssa.blocks().len();
if block_count == 0 {
return 0;
}
let mut roots: Vec<usize> = vec![0];
for handler in ssa.exception_handlers() {
roots.extend(handler.entry_blocks());
roots.extend(handler.protected_range.map(|range| range.start()));
}
let mut reachable = vec![false; block_count];
let mut frontier = roots;
while let Some(current) = frontier.pop() {
let Some(slot) = reachable.get_mut(current) else {
continue;
};
if *slot {
continue;
}
*slot = true;
if let Some(op) = ssa.block(current).and_then(|b| b.control_terminator()) {
frontier.extend(op.successors());
}
}
let dead: Vec<usize> = (0..block_count)
.filter(|&index| !reachable.get(index).copied().unwrap_or(true))
.filter(|&index| {
ssa.block(index).is_some_and(|block| {
(!block.instructions().is_empty() || !block.phi_nodes().is_empty())
&& block.instructions().iter().all(is_state_machinery)
})
})
.collect();
for index in &dead {
if let Some(block) = ssa.block_mut(*index) {
block.clear();
}
}
dead.len()
}
pub fn apply_rewires(ssa: &mut SsaFunction, rewires: &[Rewire]) -> usize {
let mut applied: usize = 0;
for rewire in rewires {
let changed = ssa
.block_mut(rewire.from)
.and_then(|block| block.instructions_mut().last_mut())
.is_some_and(|term| term.op_mut().redirect_target(rewire.old, rewire.new));
if changed {
applied = applied.saturating_add(1);
}
}
applied
}
#[cfg(test)]
mod tests {
use super::*;
use crate::analysis::{DefSite, PhiOperand, SsaBlock, SsaInstruction, SsaType, VariableOrigin};
fn define(
ssa: &mut SsaFunction,
block: usize,
make: impl FnOnce(SsaVarId) -> SsaOp,
) -> SsaVarId {
let index = ssa.block(block).map_or(0, |b| b.instructions().len());
let var = ssa.create_variable(
VariableOrigin::Phi,
0,
DefSite::instruction(block, index),
SsaType::I32,
);
let op = make(var);
if let Some(b) = ssa.block_mut(block) {
b.add_instruction(SsaInstruction::synthetic(op));
}
var
}
fn constant(ssa: &mut SsaFunction, block: usize, value: i32) -> SsaVarId {
define(ssa, block, |dest| SsaOp::Const {
dest,
value: ConstValue::I32(value),
})
}
fn terminate(ssa: &mut SsaFunction, block: usize, op: SsaOp) {
if let Some(b) = ssa.block_mut(block) {
b.add_instruction(SsaInstruction::synthetic(op));
}
}
fn constant_state_cff() -> (SsaFunction, Dispatcher) {
let mut ssa = SsaFunction::new(0, 0);
for index in 0..5 {
ssa.add_block(SsaBlock::new(index));
}
let entry_state = constant(&mut ssa, 0, 1);
terminate(&mut ssa, 0, SsaOp::Jump { target: 2 });
let case3_state = constant(&mut ssa, 3, 0);
terminate(&mut ssa, 3, SsaOp::Jump { target: 2 });
let state = ssa.create_variable(VariableOrigin::Phi, 1, DefSite::phi(2), SsaType::I32);
let mut phi = PhiNode::new(state, VariableOrigin::Phi);
phi.add_operand(PhiOperand::new(entry_state, 0));
phi.add_operand(PhiOperand::new(case3_state, 3));
if let Some(block) = ssa.block_mut(2) {
block.add_phi(phi);
block.add_instruction(SsaInstruction::synthetic(SsaOp::Switch {
value: state,
targets: vec![4, 3],
default: 1,
}));
}
terminate(&mut ssa, 4, SsaOp::Return { value: None });
terminate(&mut ssa, 1, SsaOp::Return { value: None });
let dispatcher = Dispatcher::new(2, state, vec![4, 3], 1).with_state_phi(state);
(ssa, dispatcher)
}
#[test]
fn folds_arithmetic_at_32_bit_width() {
let mut ssa = SsaFunction::new(0, 0);
ssa.add_block(SsaBlock::new(0));
let left = constant(&mut ssa, 0, 1_975_223_132);
let right = constant(&mut ssa, 0, 3);
let product = define(&mut ssa, 0, |dest| SsaOp::Mul {
dest,
left,
right,
flags: None,
});
let mut folder = StateFolder::new(&ssa);
assert_eq!(folder.fold(product), Some(1_630_702_100));
}
#[test]
fn folds_through_copy_chains() {
let mut ssa = SsaFunction::new(0, 0);
ssa.add_block(SsaBlock::new(0));
let base = constant(&mut ssa, 0, 42);
let first = define(&mut ssa, 0, |dest| SsaOp::Copy { dest, src: base });
let second = define(&mut ssa, 0, |dest| SsaOp::Copy { dest, src: first });
let mut folder = StateFolder::new(&ssa);
assert_eq!(folder.fold(second), Some(42));
assert_eq!(folder.copy_root(second), base);
}
#[test]
fn dispatch_table_reads_the_overflow_chain() {
let mut ssa = SsaFunction::new(0, 0);
for index in 0..10 {
ssa.add_block(SsaBlock::new(index));
}
for index in [2, 9] {
terminate(&mut ssa, index, SsaOp::Return { value: None });
}
let state =
ssa.create_variable(VariableOrigin::Local(0), 0, DefSite::entry(), SsaType::I32);
let probe = constant(&mut ssa, 0, 700);
terminate(
&mut ssa,
0,
SsaOp::BranchCmp {
left: state,
right: probe,
cmp: CmpKind::Eq,
unsigned: false,
true_target: 2,
false_target: 3,
},
);
terminate(&mut ssa, 3, SsaOp::Return { value: None });
let dispatcher = Dispatcher::new(1, state, vec![9], 0);
let mut folder = StateFolder::new(&ssa);
let table = DispatchTable::build(&ssa, &dispatcher, state, &mut folder);
assert_eq!(table.overflow_len(), 1);
assert_eq!(table.lookup(&mut folder, StateValue::narrow(700)), Some(2));
assert_eq!(table.lookup(&mut folder, StateValue::narrow(0)), Some(9));
assert_eq!(table.lookup(&mut folder, StateValue::narrow(123)), Some(3));
}
#[test]
fn resolves_constant_state_edges() {
let (ssa, dispatcher) = constant_state_cff();
let (rewires, stats) = resolve_dispatch_edges(&ssa, &dispatcher);
assert_eq!(stats.unresolved, 0, "both edges carry a constant state");
assert_eq!(stats.resolved, 2);
let mut targets: Vec<(usize, usize)> = rewires.iter().map(|r| (r.from, r.new)).collect();
targets.sort_unstable();
assert_eq!(targets, vec![(0, 3), (3, 4)]);
}
#[test]
fn applying_rewires_bypasses_the_dispatcher() {
let (mut ssa, dispatcher) = constant_state_cff();
let (rewires, _) = resolve_dispatch_edges(&ssa, &dispatcher);
assert_eq!(apply_rewires(&mut ssa, &rewires), 2);
assert!(
SsaCfg::from_ssa(&ssa).block_predecessors(2).is_empty(),
"no edge should still reach the dispatcher"
);
assert_eq!(clear_unreachable(&mut ssa), 1);
assert!(ssa.block(2).is_some_and(|b| b.instructions().is_empty()));
assert!(ssa.block(1).is_some_and(|b| !b.instructions().is_empty()));
}
#[test]
fn emptied_dispatch_targets_are_not_rewired_into() {
let (mut ssa, dispatcher) = constant_state_cff();
if let Some(block) = ssa.block_mut(3) {
block.clear();
}
let (rewires, stats) = resolve_dispatch_edges(&ssa, &dispatcher);
assert!(
rewires.iter().all(|r| r.new != 3),
"no edge may be rewired into a block that cannot execute"
);
assert_eq!(stats.unresolved, 2, "those edges keep using the dispatcher");
apply_rewires(&mut ssa, &rewires);
for rewire in &rewires {
let successors = ssa
.block(rewire.from)
.and_then(|b| b.control_terminator())
.map(SsaOp::successors)
.unwrap_or_default();
assert!(
!successors.contains(&3),
"rewired block b{} must not send control into an empty block",
rewire.from
);
}
}
#[test]
fn conflicting_edges_are_dropped() {
let mut stats = ResolutionStats::default();
let kept = drop_conflicts(
vec![
Rewire {
from: 5,
old: 2,
new: 7,
state: 1,
},
Rewire {
from: 5,
old: 2,
new: 9,
state: 2,
},
Rewire {
from: 6,
old: 2,
new: 7,
state: 1,
},
],
&mut stats,
);
assert_eq!(kept.len(), 1);
assert_eq!(kept[0].from, 6);
assert_eq!(stats.conflicts, 1);
}
#[test]
fn unresolved_edges_leave_the_dispatcher_in_place() {
let (mut ssa, dispatcher) = constant_state_cff();
if let Some(instr) = ssa
.block_mut(0)
.and_then(|block| block.instructions_mut().first_mut())
{
instr.set_op(SsaOp::Nop);
}
let (rewires, stats) = resolve_dispatch_edges(&ssa, &dispatcher);
assert_eq!(stats.unresolved, 1, "the entry edge no longer folds");
apply_rewires(&mut ssa, &rewires);
assert_eq!(
SsaCfg::from_ssa(&ssa).block_predecessors(2),
[0],
"the unresolved edge keeps using the dispatcher"
);
assert_eq!(
clear_unreachable(&mut ssa),
0,
"a reachable dispatcher is never emptied"
);
}
}