use std::collections::{BTreeMap, BTreeSet};
use crate::{
analysis::{SsaBlock, SsaFunction, SsaInstruction, SsaOp, SsaVarId},
deobfuscation::passes::unflattening::tracer::{TraceNode, TraceTerminator, TraceTree},
utils::BitSet,
};
type PhiOperands = Vec<(usize, SsaVarId)>;
type BlockPhiData = Vec<(usize, Vec<(SsaVarId, PhiOperands)>)>;
#[derive(Debug)]
pub struct ReconstructionResult {
pub state_transitions_removed: usize,
pub user_branches_preserved: usize,
pub block_count: usize,
pub dispatcher_still_needed: bool,
}
#[derive(Debug)]
pub struct PatchPlan {
pub dispatcher_blocks: Vec<usize>,
pub state_tainted: BitSet,
pub redirects: Vec<(usize, usize)>,
state_transition_sources: BTreeSet<usize>,
pub clone_requests: BTreeMap<usize, Vec<(usize, usize)>>,
pub execution_order: Vec<usize>,
pub(crate) branch_collapses: BTreeMap<usize, usize>,
pub state_transitions_removed: usize,
pub user_branches_preserved: usize,
}
impl PatchPlan {
fn new(dispatcher_block: usize, state_tainted: BitSet) -> Self {
Self {
dispatcher_blocks: vec![dispatcher_block],
state_tainted,
redirects: Vec::new(),
state_transition_sources: BTreeSet::new(),
clone_requests: BTreeMap::new(),
execution_order: Vec::new(),
branch_collapses: BTreeMap::new(),
state_transitions_removed: 0,
user_branches_preserved: 0,
}
}
fn add_branch_collapse(&mut self, source: usize, target: usize) {
if source == target {
return;
}
self.branch_collapses.entry(source).or_insert(target);
}
pub fn is_dispatcher_block(&self, block: usize) -> bool {
self.dispatcher_blocks.contains(&block)
}
fn add_redirect(&mut self, source: usize, target: usize, predecessor: Option<usize>) {
if source == target {
return;
}
if let Some(&(_, existing_target)) = self.redirects.iter().find(|&&(s, _)| s == source) {
if existing_target != target {
let entry = self.clone_requests.entry(source).or_default();
if !entry.iter().any(|(_, t)| *t == existing_target) {
entry.push((usize::MAX, existing_target));
}
if let Some(pred) = predecessor {
if !entry.iter().any(|(p, t)| *p == pred && *t == target) {
entry.push((pred, target));
}
}
}
return; }
self.redirects.push((source, target));
if let Some(pred) = predecessor {
let entry = self.clone_requests.entry(source).or_default();
entry.push((pred, target));
}
}
pub(crate) fn safe_redirects(&self) -> Vec<(usize, usize)> {
self.redirects
.iter()
.filter(|(source, _)| {
self.clone_requests.get(source).is_none_or(|v| v.len() <= 1)
})
.copied()
.collect()
}
pub(crate) fn blocks_to_clone(&self) -> Vec<(usize, Vec<(usize, usize)>)> {
self.clone_requests
.iter()
.filter(|(_, paths)| paths.len() > 1) .map(|(&block, paths)| (block, paths.clone()))
.collect()
}
fn add_to_execution_order(&mut self, block: usize) {
if !self.execution_order.contains(&block) && !self.is_dispatcher_block(block) {
self.execution_order.push(block);
}
}
}
pub fn extract_patch_plan(tree: &TraceTree, ssa: &SsaFunction) -> Option<PatchPlan> {
let dispatcher = tree.dispatcher.as_ref()?;
let mut plan = PatchPlan::new(dispatcher.block, tree.state_tainted.clone());
extract_redirects_from_node(&tree.root, dispatcher.block, &mut plan, None, ssa);
for handler_trace in &tree.handler_traces {
extract_redirects_from_node(&handler_trace.root, dispatcher.block, &mut plan, None, ssa);
}
plan.redirects
.retain(|&(source, _)| plan.state_transition_sources.contains(&source));
plan.clone_requests
.retain(|source, _| plan.state_transition_sources.contains(source));
Some(plan)
}
pub fn merge_patch_plans(plans: Vec<PatchPlan>) -> PatchPlan {
if plans.is_empty() {
return PatchPlan {
dispatcher_blocks: Vec::new(),
state_tainted: BitSet::new(0),
redirects: Vec::new(),
state_transition_sources: BTreeSet::new(),
clone_requests: BTreeMap::new(),
execution_order: Vec::new(),
branch_collapses: BTreeMap::new(),
state_transitions_removed: 0,
user_branches_preserved: 0,
};
}
if plans.len() == 1 {
return plans.into_iter().next().unwrap();
}
let taint_size = plans
.iter()
.map(|p| p.state_tainted.len())
.max()
.unwrap_or(0);
let mut merged = PatchPlan {
dispatcher_blocks: Vec::new(),
state_tainted: BitSet::new(taint_size),
redirects: Vec::new(),
state_transition_sources: BTreeSet::new(),
clone_requests: BTreeMap::new(),
execution_order: Vec::new(),
branch_collapses: BTreeMap::new(),
state_transitions_removed: 0,
user_branches_preserved: 0,
};
for plan in plans {
merged.dispatcher_blocks.extend(&plan.dispatcher_blocks);
merged.state_tainted.union_with(&plan.state_tainted);
merged
.state_transition_sources
.extend(&plan.state_transition_sources);
for (source, target) in plan.redirects {
if let Some(&(_, existing_target)) =
merged.redirects.iter().find(|&&(s, _)| s == source)
{
if existing_target != target {
log::warn!(
"CFF merge: redirect conflict for block {} (target {} vs {}), keeping first",
source,
existing_target,
target
);
continue;
}
continue;
}
merged.redirects.push((source, target));
}
for (block, paths) in plan.clone_requests {
merged
.clone_requests
.entry(block)
.or_default()
.extend(paths);
}
for block in plan.execution_order {
if !merged.execution_order.contains(&block) {
merged.execution_order.push(block);
}
}
for (source, target) in plan.branch_collapses {
merged.branch_collapses.entry(source).or_insert(target);
}
merged.state_transitions_removed += plan.state_transitions_removed;
merged.user_branches_preserved += plan.user_branches_preserved;
}
merged
}
fn is_pure_prep_op(op: &SsaOp) -> bool {
matches!(
op,
SsaOp::Const { .. } | SsaOp::Copy { .. } | SsaOp::Jump { .. } | SsaOp::Nop
)
}
fn is_state_chain_block(ssa: &SsaFunction, block_idx: usize, state_tainted: &BitSet) -> bool {
let Some(block) = ssa.block(block_idx) else {
return false;
};
let term_is_state_check = matches!(block.terminator_op(), Some(SsaOp::BranchCmp { left, right, .. })
if state_tainted.contains(left.index()) || state_tainted.contains(right.index()));
if !term_is_state_check {
return false;
}
let instrs = block.instructions();
let non_term_count = instrs.len().saturating_sub(1);
instrs
.iter()
.take(non_term_count)
.all(|instr| is_pure_prep_op(instr.op()))
}
fn extract_redirects_from_node(
node: &TraceNode,
dispatcher_block: usize,
plan: &mut PatchPlan,
external_predecessor: Option<usize>,
ssa: &SsaFunction,
) {
let mut stack: Vec<(&TraceNode, Option<usize>)> = Vec::new();
stack.push((node, external_predecessor));
while let Some((node, external_predecessor)) = stack.pop() {
for &block in &node.blocks_visited {
plan.add_to_execution_order(block);
}
match &node.terminator {
TraceTerminator::StateTransition {
target_block,
continues,
..
} => {
let effective_target = {
let mut t = *target_block;
if is_state_chain_block(ssa, t, &plan.state_tainted) {
for &b in &continues.blocks_visited {
if !is_state_chain_block(ssa, b, &plan.state_tainted) {
t = b;
break;
}
}
}
t
};
let target_block = &effective_target;
let last_pred = node
.blocks_visited
.iter()
.rev()
.find(|&&b| b != dispatcher_block)
.copied();
let first_pred = node
.blocks_visited
.iter()
.find(|&&b| b != dispatcher_block)
.copied();
let mut collapse_first_branch: Option<(usize, usize)> = None;
let pred_block = match (first_pred, last_pred) {
(Some(first), Some(last)) if first != last => {
let mut intermediates_are_pure = true;
let start_idx = node
.blocks_visited
.iter()
.position(|&b| b == first)
.unwrap_or(0);
let end_idx = node
.blocks_visited
.iter()
.position(|&b| b == last)
.unwrap_or(node.blocks_visited.len());
if end_idx > start_idx + 1 {
let intermediate_blocks: std::collections::BTreeSet<usize> = node
.blocks_visited[start_idx + 1..end_idx]
.iter()
.copied()
.collect();
for iwv in &node.instructions {
if !intermediate_blocks.contains(&iwv.block_idx) {
continue;
}
if !is_pure_prep_op(iwv.instruction.op()) {
intermediates_are_pure = false;
break;
}
}
} else {
intermediates_are_pure = false;
}
let first_is_jump_terminated = ssa
.block(first)
.and_then(|b| b.terminator_op())
.is_some_and(|op| {
matches!(op, SsaOp::Jump { .. } | SsaOp::Leave { .. })
});
if intermediates_are_pure && first_is_jump_terminated {
Some(first)
} else if intermediates_are_pure {
let next_in_path = node.blocks_visited.get(start_idx + 1).copied();
if let Some(next) = next_in_path {
collapse_first_branch = Some((first, next));
}
Some(last)
} else {
Some(last)
}
}
_ => last_pred,
};
let predecessor_of_pred = if let Some(pred) = pred_block {
node.blocks_visited
.iter()
.position(|&b| b == pred)
.and_then(|pos| {
if pos > 0 {
Some(node.blocks_visited[pos - 1])
} else {
external_predecessor
}
})
} else {
None
};
if let Some(pred) = pred_block {
plan.add_redirect(pred, *target_block, predecessor_of_pred);
plan.state_transition_sources.insert(pred);
plan.state_transitions_removed += 1;
} else if let Some(ext_pred) = external_predecessor {
plan.add_redirect(ext_pred, *target_block, None);
plan.state_transition_sources.insert(ext_pred);
plan.state_transitions_removed += 1;
}
if let Some((src, next)) = collapse_first_branch {
plan.add_branch_collapse(src, next);
}
stack.push((continues, pred_block.or(external_predecessor)));
}
TraceTerminator::UserBranch {
block,
true_branch,
false_branch,
..
} => {
plan.user_branches_preserved += 1;
stack.push((false_branch, Some(*block)));
stack.push((true_branch, Some(*block)));
}
TraceTerminator::UserSwitch {
block,
cases,
default,
..
} => {
plan.user_branches_preserved += 1;
stack.push((default, Some(*block)));
for (_, case_node) in cases.iter().rev() {
stack.push((case_node, Some(*block)));
}
}
TraceTerminator::Exit { block } => {
plan.add_to_execution_order(*block);
}
TraceTerminator::LoopBack { target_block, .. } => {
if let Some(pred) = external_predecessor {
plan.add_redirect(pred, *target_block, external_predecessor);
plan.state_transition_sources.insert(pred);
plan.state_transitions_removed += 1;
}
plan.add_to_execution_order(*target_block);
}
TraceTerminator::Stopped { .. } | TraceTerminator::PendingStateTransition { .. } => {
}
}
}
}
pub fn apply_patch_plan(ssa: &mut SsaFunction, plan: &PatchPlan) -> ReconstructionResult {
let safe = plan.safe_redirects();
let to_clone = plan.blocks_to_clone();
for &(source_block, new_target) in &safe {
let is_jump = ssa
.block(source_block)
.and_then(|b| b.terminator_op())
.is_some_and(|op| matches!(op, SsaOp::Jump { .. } | SsaOp::Leave { .. }));
if let Some(block) = ssa.block_mut(source_block) {
if is_jump {
block.set_target(new_target);
} else {
for &db in &plan.dispatcher_blocks {
block.redirect_target(db, new_target);
}
}
}
}
for (&source_block, &new_target) in &plan.branch_collapses {
if let Some(block) = ssa.block_mut(source_block) {
let current = block
.terminator_op()
.map(|op| matches!(op, SsaOp::Branch { .. } | SsaOp::BranchCmp { .. }))
.unwrap_or(false);
if current {
if let Some(term) = block.instructions_mut().last_mut() {
*term = SsaInstruction::synthetic(SsaOp::Jump { target: new_target });
}
}
}
}
let mut clone_map: BTreeMap<usize, usize> = BTreeMap::new();
let mut cloned_blocks = Vec::new();
for (merge_block, paths) in &to_clone {
if paths.len() < 2 {
continue;
}
let merge_content = ssa.block(*merge_block).cloned();
let Some(original_block) = merge_content else {
continue;
};
let (_, first_target) = paths[0];
let is_user_branch = ssa
.block(*merge_block)
.and_then(|b| b.terminator_op())
.is_some_and(|op| {
matches!(
op,
SsaOp::Branch { .. } | SsaOp::BranchCmp { .. } | SsaOp::Switch { .. }
)
});
if is_user_branch {
continue;
}
if let Some(block) = ssa.block_mut(*merge_block) {
block.set_target(first_target);
}
for &(pred, target) in paths.iter().skip(1) {
let new_block_idx = ssa.block_count() + cloned_blocks.len();
clone_map.insert(new_block_idx, *merge_block);
let mut cloned = original_block.clone();
cloned.set_id(new_block_idx);
for phi in cloned.phi_nodes_mut() {
phi.operands_mut().retain(|op| op.predecessor() == pred);
}
cloned
.phi_nodes_mut()
.retain(|phi| !phi.operands().is_empty());
cloned.set_target(target);
cloned_blocks.push((pred, cloned, *merge_block));
}
}
for (pred, mut cloned, original_merge) in cloned_blocks {
let new_block_idx = cloned.id();
if let Some(pred_block) = ssa.block_mut(pred) {
pred_block.redirect_target(original_merge, new_block_idx);
}
filter_state_instructions(&mut cloned, &plan.state_tainted, &plan.dispatcher_blocks);
ssa.blocks_mut().push(cloned);
}
let mut patched_blocks = BitSet::new(ssa.block_count());
for &(source, _) in &safe {
patched_blocks.insert(source);
}
for (merge_block, _) in &to_clone {
patched_blocks.insert(*merge_block);
}
for block_idx in patched_blocks.iter() {
let has_switch = ssa
.block(block_idx)
.and_then(|b| b.terminator_op())
.is_some_and(|op| matches!(op, SsaOp::Switch { .. }));
if plan.is_dispatcher_block(block_idx) || has_switch {
continue;
}
if let Some(block) = ssa.block_mut(block_idx) {
filter_state_instructions(block, &plan.state_tainted, &plan.dispatcher_blocks);
}
}
materialize_dispatcher_phis(ssa, plan, &patched_blocks);
let unresolved: Vec<usize> = (0..ssa.block_count())
.filter(|&bi| {
!patched_blocks.contains(bi)
&& !plan.is_dispatcher_block(bi)
&& ssa
.block(bi)
.and_then(|b| b.terminator_op())
.is_some_and(|op| match op {
SsaOp::Jump { target } => plan.is_dispatcher_block(*target),
SsaOp::BranchCmp {
true_target,
false_target,
..
} => {
plan.is_dispatcher_block(*true_target)
|| plan.is_dispatcher_block(*false_target)
}
_ => false,
})
})
.collect();
let dispatcher_still_needed = unresolved.iter().any(|&bi| {
ssa.block_predecessors(bi)
.iter()
.any(|&pred| !plan.is_dispatcher_block(pred))
});
if !dispatcher_still_needed {
for &db in &plan.dispatcher_blocks {
if let Some(dispatcher) = ssa.block_mut(db) {
dispatcher.clear();
}
}
for &bi in &unresolved {
if ssa
.block_predecessors(bi)
.iter()
.all(|&pred| plan.is_dispatcher_block(pred))
{
if let Some(block) = ssa.block_mut(bi) {
block.clear();
}
}
}
}
ReconstructionResult {
state_transitions_removed: plan.state_transitions_removed,
user_branches_preserved: plan.user_branches_preserved,
block_count: ssa.block_count(),
dispatcher_still_needed,
}
}
fn filter_state_instructions(
block: &mut SsaBlock,
state_tainted: &BitSet,
dispatcher_blocks: &[usize],
) {
block.instructions_mut().retain(|instr| {
if instr.is_terminator() {
if let SsaOp::Jump { target } = instr.op() {
if dispatcher_blocks.contains(target) {
return false; }
}
return true;
}
if matches!(instr.op(), SsaOp::Const { .. }) {
return true;
}
let def_tainted = instr
.def()
.is_some_and(|d| state_tainted.contains(d.index()));
let uses_tainted = instr
.uses()
.iter()
.any(|u| state_tainted.contains(u.index()));
if def_tainted {
return false;
}
if uses_tainted {
return matches!(instr.op(), SsaOp::Call { .. } | SsaOp::CallVirt { .. });
}
true
});
}
fn materialize_dispatcher_phis(ssa: &mut SsaFunction, plan: &PatchPlan, patched_blocks: &BitSet) {
if plan.dispatcher_blocks.is_empty() {
return;
}
let dispatcher_set: BTreeSet<usize> = plan.dispatcher_blocks.iter().copied().collect();
let mut loop_blocks: Vec<usize> = plan.dispatcher_blocks.clone();
let mut loop_block_set: BTreeSet<usize> = dispatcher_set.clone();
for block_idx in 0..ssa.block_count() {
if loop_block_set.contains(&block_idx) {
continue;
}
if patched_blocks.contains(block_idx) {
continue;
}
let Some(block) = ssa.block(block_idx) else {
continue;
};
if block.phi_nodes().is_empty() {
continue;
}
let has_patched_predecessor = block.phi_nodes().iter().any(|phi| {
phi.operands()
.iter()
.any(|op| patched_blocks.contains(op.predecessor()))
});
if has_patched_predecessor {
loop_blocks.push(block_idx);
loop_block_set.insert(block_idx);
}
}
loop_blocks.sort_by_key(|&b| if dispatcher_set.contains(&b) { 1 } else { 0 });
let dispatcher_phis: BlockPhiData = loop_blocks
.iter()
.filter_map(|&db| {
ssa.block(db).map(|block| {
let phis = block
.phi_nodes()
.iter()
.filter(|phi| !plan.state_tainted.contains(phi.result().index()))
.map(|phi| {
let operands: Vec<(usize, SsaVarId)> = phi
.operands()
.iter()
.map(|op| (op.predecessor(), op.value()))
.collect();
(phi.result(), operands)
})
.collect();
(db, phis)
})
})
.collect();
if dispatcher_phis.is_empty() {
return;
}
let mut phi_lookup: BTreeMap<usize, BTreeMap<SsaVarId, Vec<(usize, SsaVarId)>>> =
BTreeMap::new();
for (db, phis) in &dispatcher_phis {
let map: BTreeMap<SsaVarId, Vec<(usize, SsaVarId)>> = phis
.iter()
.map(|(result, operands)| (*result, operands.clone()))
.collect();
phi_lookup.insert(*db, map);
}
let loop_block_set: BTreeSet<usize> = loop_blocks.iter().copied().collect();
let phi_result_set: BTreeSet<SsaVarId> = dispatcher_phis
.iter()
.flat_map(|(_, phis)| phis.iter().map(|(r, _)| *r))
.collect();
let mut concrete_for_phi: BTreeMap<SsaVarId, (usize, SsaVarId)> = BTreeMap::new();
for (_, phis) in &dispatcher_phis {
for (phi_result, operands) in phis {
for &(pred, val) in operands {
if !patched_blocks.contains(pred) || loop_block_set.contains(&pred) {
continue;
}
if val == *phi_result || phi_result_set.contains(&val) {
continue;
}
concrete_for_phi.insert(*phi_result, (pred, val));
}
}
}
if concrete_for_phi.is_empty() {
return;
}
let mut accumulated: BTreeMap<SsaVarId, SsaVarId> = BTreeMap::new();
for &block_idx in &plan.execution_order {
if !patched_blocks.contains(block_idx) || loop_block_set.contains(&block_idx) {
continue;
}
if !accumulated.is_empty() {
if let Some(block) = ssa.block_mut(block_idx) {
for instr in block.instructions_mut() {
for (&phi_var, &concrete_val) in &accumulated {
instr.op_mut().replace_uses(phi_var, concrete_val);
}
}
}
}
for &db in &loop_blocks {
let Some(phis) = phi_lookup.get(&db) else {
continue;
};
for (phi_result, operands) in phis {
if let Some(&(_, val)) = operands.iter().find(|&&(pred, _)| pred == block_idx) {
let concrete = accumulated.get(&val).copied().unwrap_or(val);
if *phi_result != concrete {
accumulated.insert(*phi_result, concrete);
}
}
}
}
}
}
pub fn reconstruct_from_tree(
tree: &TraceTree,
original: &SsaFunction,
) -> Option<ReconstructionResult> {
let plan = extract_patch_plan(tree, original)?;
Some(ReconstructionResult {
state_transitions_removed: plan.state_transitions_removed,
user_branches_preserved: plan.user_branches_preserved,
block_count: original.block_count(),
dispatcher_still_needed: false, })
}
#[cfg(test)]
mod tests {
use crate::{
analysis::{
ConstValue, PhiNode, PhiOperand, SsaBlock, SsaFunction, SsaInstruction, SsaOp,
SsaVarId, VariableOrigin,
},
deobfuscation::passes::unflattening::{
reconstruction::reconstruct_from_tree, tracer::trace_method_tree, UnflattenConfig,
},
};
fn create_simple_cff() -> SsaFunction {
let mut ssa = SsaFunction::new(0, 1);
let state_var = SsaVarId::from_index(0);
let const_var = SsaVarId::from_index(1);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: const_var,
value: ConstValue::I32(0),
}));
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
let mut phi = PhiNode::new(state_var, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(const_var, 0));
b1.add_phi(phi);
b1.add_instruction(SsaInstruction::synthetic(SsaOp::Switch {
value: state_var,
targets: vec![2, 3, 4],
default: 5,
}));
ssa.add_block(b1);
for i in 2..=4 {
let mut b = SsaBlock::new(i);
b.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b);
}
let mut b5 = SsaBlock::new(5);
b5.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(b5);
ssa
}
#[test]
fn test_reconstruct_simple_cff() {
let ssa = create_simple_cff();
let config = UnflattenConfig::default();
let tree = trace_method_tree(&ssa, &config, None);
assert!(tree.dispatcher.is_some(), "Should detect dispatcher");
let result = reconstruct_from_tree(&tree, &ssa);
assert!(result.is_some(), "Reconstruction should succeed");
let result = result.unwrap();
println!("=== Reconstruction Result ===");
println!("Blocks: {}", result.block_count);
println!(
"State transitions removed: {}",
result.state_transitions_removed
);
println!(
"User branches preserved: {}",
result.user_branches_preserved
);
assert!(
result.state_transitions_removed > 0,
"Should remove at least one state transition"
);
assert!(result.block_count > 0, "Should have blocks in output");
}
fn create_cff_with_user_branch() -> SsaFunction {
let mut ssa = SsaFunction::new(1, 1); let state_var = SsaVarId::from_index(0);
let init_state = SsaVarId::from_index(1);
let const_one = SsaVarId::from_index(2);
let arg0 = SsaVarId::from_index(3);
let user_zero = SsaVarId::from_index(4);
let cmp_result = SsaVarId::from_index(5);
let mut b0 = SsaBlock::new(0);
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: init_state,
value: ConstValue::I32(0),
}));
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: arg0,
value: ConstValue::I32(42), }));
b0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b0);
let mut b1 = SsaBlock::new(1);
let mut phi = PhiNode::new(state_var, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(init_state, 0)); phi.add_operand(PhiOperand::new(const_one, 3)); phi.add_operand(PhiOperand::new(const_one, 4)); b1.add_phi(phi);
b1.add_instruction(SsaInstruction::synthetic(SsaOp::Switch {
value: state_var,
targets: vec![2, 5], default: 6,
}));
ssa.add_block(b1);
let mut b2 = SsaBlock::new(2);
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: const_one,
value: ConstValue::I32(1),
}));
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: user_zero,
value: ConstValue::I32(0),
}));
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Cgt {
dest: cmp_result,
left: arg0,
right: user_zero,
unsigned: false,
}));
b2.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: cmp_result,
true_target: 3, false_target: 4, }));
ssa.add_block(b2);
let mut b3a = SsaBlock::new(3);
b3a.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b3a);
let mut b3b = SsaBlock::new(4);
b3b.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(b3b);
let mut b5 = SsaBlock::new(5);
b5.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(b5);
let mut b6 = SsaBlock::new(6);
b6.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(b6);
ssa
}
#[test]
fn test_reconstruct_with_user_branch() {
let ssa = create_cff_with_user_branch();
let config = UnflattenConfig::default();
let tree = trace_method_tree(&ssa, &config, None);
assert!(tree.dispatcher.is_some(), "Should detect dispatcher");
assert!(
tree.stats.user_branch_count > 0,
"Should have user branches"
);
let result = reconstruct_from_tree(&tree, &ssa);
assert!(result.is_some(), "Reconstruction should succeed");
let result = result.unwrap();
println!("=== Reconstruction with User Branch ===");
println!("Blocks: {}", result.block_count);
println!(
"State transitions removed: {}",
result.state_transitions_removed
);
println!(
"User branches preserved: {}",
result.user_branches_preserved
);
assert!(
result.user_branches_preserved > 0,
"Should preserve user branches"
);
println!("Block count: {}", result.block_count);
}
}