use super::*;
use crate::decompiler::cfg::ssa::form::{SsaExpr, SsaStmt};
use crate::decompiler::cfg::{BasicBlock, BlockId, CfgBuilder, EdgeKind, Terminator};
use crate::decompiler::ir::BinOp;
use crate::instruction::{Instruction, OpCode, Operand};
fn linear(instrs: Vec<Instruction>) -> (Vec<Instruction>, Cfg) {
let cfg = CfgBuilder::new(&instrs).build();
(instrs, cfg)
}
fn instr(off: usize, op: OpCode) -> Instruction {
Instruction::new(off, op, None)
}
#[test]
fn linear_compute_produces_real_binary_expr() {
let ins = vec![
instr(0, OpCode::Push1),
instr(1, OpCode::Push2),
instr(2, OpCode::Add),
Instruction::new(3, OpCode::Ret, None),
];
let (ins, cfg) = linear(ins);
let ssa = SsaBuilder::new(&cfg, &ins).build();
let (_id, block) = ssa
.blocks_iter()
.find(|(_, b)| b.stmt_count() >= 3)
.expect("a block with >= 3 assignments should exist");
assert_eq!(block.stmt_count(), 3, "expected 3 push/compute assignments");
let add = &block.stmts[2];
let SsaStmt::Assign { value, .. } = add else {
panic!("third stmt should be the ADD assignment: {add:?}");
};
let SsaExpr::Binary { op, left, right } = value else {
panic!("ADD should lower to a binary expr, got {value:?}");
};
assert_eq!(*op, BinOp::Add);
assert!(
matches!(left.as_ref(), SsaExpr::Variable(_)),
"left operand"
);
assert!(
matches!(right.as_ref(), SsaExpr::Variable(_)),
"right operand"
);
}
#[test]
fn dup_creates_a_copy_definition() {
let ins = vec![
instr(0, OpCode::Push1),
instr(1, OpCode::Dup),
Instruction::new(2, OpCode::Ret, None),
];
let (ins, cfg) = linear(ins);
let ssa = SsaBuilder::new(&cfg, &ins).build();
let block = ssa.blocks_iter().next().expect("a block exists").1;
assert_eq!(block.stmt_count(), 2);
let copy = &block.stmts[1];
let SsaStmt::Assign { value, .. } = copy else {
panic!("DUP should produce an assignment: {copy:?}");
};
assert!(
matches!(value, SsaExpr::Variable(_)),
"DUP copy should reference its source var, got {value:?}"
);
}
#[test]
fn store_local_emits_a_slot_assignment() {
let ins = vec![
Instruction::new(0, OpCode::Push10, None),
Instruction::new(1, OpCode::Stloc0, None),
Instruction::new(2, OpCode::Ret, None),
];
let (ins, cfg) = linear(ins);
let ssa = SsaBuilder::new(&cfg, &ins).build();
let block = ssa.blocks_iter().next().expect("a block exists").1;
let has_loc0_assign = block.stmts.iter().any(|s| match s {
SsaStmt::Assign { target, .. } => target.base == "loc0",
_ => false,
});
assert!(
has_loc0_assign,
"STLOC0 should define a loc0 SSA variable; got {:?}",
block.stmts
);
}
#[test]
fn convert_preserves_lower_stack_values() {
let ins = vec![
Instruction::new(0, OpCode::Push2, None),
Instruction::new(1, OpCode::Push1, None),
Instruction::new(2, OpCode::Convert, Some(Operand::U8(0x28))),
Instruction::new(4, OpCode::Drop, None),
Instruction::new(5, OpCode::Stloc0, None),
Instruction::new(6, OpCode::Ret, None),
];
let (ins, cfg) = linear(ins);
let ssa = SsaBuilder::new(&cfg, &ins).build();
let block = ssa.blocks_iter().next().expect("a block exists").1;
let loc0_store = block
.stmts
.iter()
.find(|stmt| matches!(stmt, SsaStmt::Assign { target, .. } if target.base == "loc0"))
.expect("STLOC0 should define loc0");
let SsaStmt::Assign { value, .. } = loc0_store else {
panic!("loc0 store should be an assignment: {loc0_store:?}");
};
let SsaExpr::Variable(stored) = value else {
panic!("loc0 should store the lower PUSH2 variable, got {value:?}");
};
assert_ne!(
stored.base, "?",
"CONVERT must not consume the lower PUSH2 stack value: {:?}",
block.stmts
);
}
#[test]
fn store_then_load_connects_within_a_block() {
let ins = vec![
Instruction::new(0, OpCode::Push10, None),
Instruction::new(1, OpCode::Stloc0, None),
Instruction::new(2, OpCode::Ldloc0, None),
Instruction::new(3, OpCode::Ret, None),
];
let (ins, cfg) = linear(ins);
let ssa = SsaBuilder::new(&cfg, &ins).build();
let block = ssa.blocks_iter().next().expect("a block exists").1;
let loc0_defs: Vec<&SsaStmt> = block
.stmts
.iter()
.filter(|s| matches!(s, SsaStmt::Assign { target, .. } if target.base == "loc0"))
.collect();
assert!(
loc0_defs.len() >= 2,
"expected a store def and a load def for loc0; got {:?}",
block.stmts
);
let load_def = loc0_defs.last().copied().unwrap();
let SsaStmt::Assign { value, .. } = load_def else {
panic!("load def should be an Assign: {load_def:?}");
};
assert!(
matches!(value, SsaExpr::Variable(_)),
"LDLOC0 after STLOC0 should read the stored var; got {value:?}"
);
assert!(
!matches!(value, SsaExpr::Call { .. }),
"LDLOC0 should not stay an opaque ldloc0() call once a store exists; got {value:?}"
);
}
#[test]
fn diamond_places_a_phi_at_the_merge() {
let ins = vec![
Instruction::new(0, OpCode::Push1, None), Instruction::new(0, OpCode::Pushint8, Some(Operand::I8(10))), Instruction::new(0, OpCode::Pushint8, Some(Operand::I8(20))), Instruction::new(0, OpCode::Ret, None), ];
let mut cfg = Cfg::new();
cfg.add_block(BasicBlock::new(
BlockId(0),
0,
1,
0..1,
Terminator::Branch {
then_target: BlockId(1),
else_target: BlockId(2),
},
));
cfg.add_block(BasicBlock::new(
BlockId(1),
1,
2,
1..2,
Terminator::Jump { target: BlockId(3) },
));
cfg.add_block(BasicBlock::new(
BlockId(2),
2,
3,
2..3,
Terminator::Jump { target: BlockId(3) },
));
cfg.add_block(BasicBlock::new(BlockId(3), 3, 4, 3..4, Terminator::Return));
cfg.add_edge(BlockId(0), BlockId(1), EdgeKind::ConditionalTrue);
cfg.add_edge(BlockId(0), BlockId(2), EdgeKind::ConditionalFalse);
cfg.add_edge(BlockId(1), BlockId(3), EdgeKind::Unconditional);
cfg.add_edge(BlockId(2), BlockId(3), EdgeKind::Unconditional);
let ssa = SsaBuilder::new(&cfg, &ins).build();
let merge = ssa.block(BlockId(3)).expect("merge block exists");
assert!(
merge.phi_count() >= 1,
"merge block should have a phi node for the incoming value slot"
);
let phi = &merge.phi_nodes[0];
assert_eq!(
phi.operands.len(),
2,
"phi should have one operand per predecessor"
);
}
#[test]
fn diamond_places_a_phi_for_a_slot() {
let ins = vec![
Instruction::new(0, OpCode::Push1, None),
Instruction::new(1, OpCode::Stloc0, None),
Instruction::new(0, OpCode::Push11, None),
Instruction::new(1, OpCode::Stloc0, None),
Instruction::new(0, OpCode::Push12, None),
Instruction::new(1, OpCode::Stloc0, None),
Instruction::new(0, OpCode::Ldloc0, None),
Instruction::new(0, OpCode::Ret, None),
];
let mut cfg = Cfg::new();
cfg.add_block(BasicBlock::new(
BlockId(0),
0,
1,
0..2,
Terminator::Branch {
then_target: BlockId(1),
else_target: BlockId(2),
},
));
cfg.add_block(BasicBlock::new(
BlockId(1),
1,
2,
2..4,
Terminator::Jump { target: BlockId(3) },
));
cfg.add_block(BasicBlock::new(
BlockId(2),
2,
3,
4..6,
Terminator::Jump { target: BlockId(3) },
));
cfg.add_block(BasicBlock::new(BlockId(3), 3, 4, 6..8, Terminator::Return));
cfg.add_edge(BlockId(0), BlockId(1), EdgeKind::ConditionalTrue);
cfg.add_edge(BlockId(0), BlockId(2), EdgeKind::ConditionalFalse);
cfg.add_edge(BlockId(1), BlockId(3), EdgeKind::Unconditional);
cfg.add_edge(BlockId(2), BlockId(3), EdgeKind::Unconditional);
let ssa = SsaBuilder::new(&cfg, &ins).build();
let merge = ssa.block(BlockId(3)).expect("merge block exists");
let has_slot_phi = merge.phi_nodes.iter().any(|phi| phi.target.base == "loc0");
assert!(
has_slot_phi,
"merge of two STLOC0 arms should place a loc0 φ; got {:?}",
merge.phi_nodes
);
}