use crate::{
analysis::{cfg::SsaCfg, evaluator::SsaEvaluator},
events::{EventKind, EventListener},
ir::{function::SsaFunction, ops::SsaOp, value::ConstValue, variable::SsaVarId},
pointer::PointerSize,
target::Target,
};
pub fn run<T, L>(
ssa: &mut SsaFunction<T>,
method: &T::MethodRef,
events: &L,
ptr_size: PointerSize,
) -> bool
where
T: Target,
L: EventListener<T> + ?Sized,
{
if ssa.is_empty() {
return false;
}
let cfg = SsaCfg::from_ssa(ssa);
let mut threadings: Vec<(usize, usize, usize)> = Vec::new();
for (block_idx, block) in ssa.iter_blocks() {
let Some(SsaOp::Branch {
condition,
true_target,
false_target,
}) = block.terminator_op()
else {
continue;
};
for pred_idx in cfg.block_predecessors(block_idx) {
if let Some(target) = try_thread(
ssa,
*pred_idx,
block_idx,
*condition,
*true_target,
*false_target,
ptr_size,
) {
let pred_target = ssa.block(*pred_idx).and_then(|b| {
b.terminator_op().and_then(|op| match op {
SsaOp::Jump { target } | SsaOp::Leave { target } => Some(*target),
_ => None,
})
});
if pred_target != Some(target) {
threadings.push((*pred_idx, block_idx, target));
}
}
}
}
let mut changed = false;
for (pred_block, branch_block, new_target) in threadings {
if apply_threading(ssa, pred_block, branch_block, new_target, method, events) {
changed = true;
}
}
changed
}
fn try_thread<T: Target>(
ssa: &SsaFunction<T>,
pred_block: usize,
branch_block: usize,
condition: SsaVarId,
true_target: usize,
false_target: usize,
ptr_size: PointerSize,
) -> Option<usize> {
let mut eval = SsaEvaluator::new(ssa, ptr_size);
eval.evaluate_block(pred_block);
eval.set_predecessor(Some(pred_block));
eval.evaluate_phis(branch_block);
let cond_value = eval
.get_concrete(condition)
.and_then(ConstValue::as_i64)
.or_else(|| {
eval.resolve_with_trace(condition, 10)
.and_then(|e| e.as_i64())
})?;
let _ = branch_block;
if cond_value != 0 {
Some(true_target)
} else {
Some(false_target)
}
}
fn apply_threading<T, L>(
ssa: &mut SsaFunction<T>,
pred_block: usize,
_branch_block: usize,
new_target: usize,
method: &T::MethodRef,
events: &L,
) -> bool
where
T: Target,
L: EventListener<T> + ?Sized,
{
let Some(block) = ssa.block_mut(pred_block) else {
return false;
};
let Some(last) = block.instructions_mut().last_mut() else {
return false;
};
match last.op().clone() {
SsaOp::Jump { target } if target != new_target => {
last.set_op(SsaOp::Jump { target: new_target });
push(
events,
EventKind::ControlFlowRestructured,
method,
pred_block,
format!("jump threaded: B{pred_block} now jumps to B{new_target} (was B{target})"),
);
true
}
SsaOp::Branch {
condition,
true_target,
false_target,
} => {
let old_target = if new_target == true_target {
false_target
} else {
true_target
};
last.set_op(SsaOp::Jump { target: new_target });
push(events, EventKind::BranchSimplified, method, pred_block, format!(
"branch threaded: B{pred_block} condition on {condition:?} resolved to B{new_target} (eliminated B{old_target})"
));
true
}
SsaOp::Leave { target } if target != new_target => {
last.set_op(SsaOp::Leave { target: new_target });
push(
events,
EventKind::ControlFlowRestructured,
method,
pred_block,
format!(
"leave threaded: B{pred_block} now leaves to B{new_target} (was B{target})"
),
);
true
}
_ => false,
}
}
fn push<T, L>(events: &L, kind: EventKind, method: &T::MethodRef, location: usize, message: String)
where
T: Target,
L: EventListener<T> + ?Sized,
{
let event = crate::events::Event {
kind,
method: Some(method.clone()),
location: Some(location),
message,
pass: None,
};
events.push(event);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
events::EventLog,
ir::{
block::SsaBlock,
instruction::SsaInstruction,
phi::{PhiNode, PhiOperand},
value::ConstValue,
variable::{DefSite, SsaVarId, VariableOrigin},
},
testing::{MockTarget, MockType},
PointerSize,
};
fn instr(op: SsaOp<MockTarget>) -> SsaInstruction<MockTarget> {
SsaInstruction::synthetic(op)
}
fn local_at(
ssa: &mut SsaFunction<MockTarget>,
idx: u16,
block: usize,
instr: usize,
) -> SsaVarId {
ssa.create_variable(
VariableOrigin::Local(idx),
0,
DefSite::instruction(block, instr),
MockType::I32,
)
}
#[test]
fn thread_constant_condition_to_true_branch() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 2);
let true_val = local_at(&mut ssa, 0, 0, 0);
let cond = local_at(&mut ssa, 1, 0, 1);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: true_val,
value: ConstValue::I32(1),
}));
b0.add_instruction(instr(SsaOp::Const {
dest: cond,
value: ConstValue::I32(1),
}));
b0.add_instruction(instr(SsaOp::Jump { target: 1 }));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::Branch {
condition: cond,
true_target: 2,
false_target: 3,
}));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
b2.add_instruction(instr(SsaOp::Return {
value: Some(true_val),
}));
ssa.add_block(b2);
let mut b3 = SsaBlock::new(3);
b3.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b3);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run(&mut ssa, &method, &log, PointerSize::Bit64);
assert!(changed, "constant true condition should be threaded");
assert!(
log.has(EventKind::BranchSimplified) || log.has(EventKind::ControlFlowRestructured)
);
}
#[test]
fn thread_with_phi_value_resolution() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 3);
let true_cond = local_at(&mut ssa, 0, 0, 0);
let false_cond = local_at(&mut ssa, 1, 1, 0);
let merged_cond =
ssa.create_variable(VariableOrigin::Local(2), 0, DefSite::phi(2), MockType::I32);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: true_cond,
value: ConstValue::I32(1),
}));
b0.add_instruction(instr(SsaOp::Jump { target: 2 }));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::Const {
dest: false_cond,
value: ConstValue::I32(0),
}));
b1.add_instruction(instr(SsaOp::Jump { target: 2 }));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
let mut phi = PhiNode::new(merged_cond, VariableOrigin::Local(2));
phi.add_operand(PhiOperand::new(true_cond, 0));
phi.add_operand(PhiOperand::new(false_cond, 1));
b2.add_phi(phi);
b2.add_instruction(instr(SsaOp::Branch {
condition: merged_cond,
true_target: 3,
false_target: 4,
}));
ssa.add_block(b2);
let mut b3 = SsaBlock::new(3);
b3.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b3);
let mut b4 = SsaBlock::new(4);
b4.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b4);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run(&mut ssa, &method, &log, PointerSize::Bit64);
assert!(changed, "phi-based threading should work");
}
#[test]
fn no_threading_when_all_unknown() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 1);
let cond = local_at(&mut ssa, 0, 0, 0);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::LoadArg {
dest: cond,
arg_index: 0,
}));
b0.add_instruction(instr(SsaOp::Branch {
condition: cond,
true_target: 1,
false_target: 2,
}));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
b2.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b2);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run(&mut ssa, &method, &log, PointerSize::Bit64);
assert!(
!changed,
"no threading should occur when condition cannot be resolved"
);
}
#[test]
fn empty_function_no_changes() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 0);
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run(&mut ssa, &method, &log, PointerSize::Bit64);
assert!(!changed);
}
#[test]
fn threading_with_copy_chain() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 3);
let src = local_at(&mut ssa, 0, 0, 0);
let mid = local_at(&mut ssa, 1, 0, 1);
let cond = local_at(&mut ssa, 2, 0, 2);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Const {
dest: src,
value: ConstValue::I32(1),
}));
b0.add_instruction(instr(SsaOp::Copy { dest: mid, src }));
b0.add_instruction(instr(SsaOp::Copy {
dest: cond,
src: mid,
}));
b0.add_instruction(instr(SsaOp::Branch {
condition: cond,
true_target: 1,
false_target: 2,
}));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
b2.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b2);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run(&mut ssa, &method, &log, PointerSize::Bit64);
let _ = changed;
}
#[test]
fn no_branch_terminator_no_threading() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 0);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b0);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run(&mut ssa, &method, &log, PointerSize::Bit64);
assert!(!changed, "no branch means no threading");
}
#[test]
fn jump_not_branch_not_threaded() {
let mut ssa: SsaFunction<MockTarget> = SsaFunction::new(0, 0);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(instr(SsaOp::Jump { target: 1 }));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
b1.add_instruction(instr(SsaOp::Return { value: None }));
ssa.add_block(b1);
ssa.recompute_uses();
let log: EventLog<MockTarget> = EventLog::new();
let method = 0u32;
let changed = run(&mut ssa, &method, &log, PointerSize::Bit64);
assert!(!changed, "Jump should not be threaded by this pass");
}
}