use crate::{
analysis::{
ConstValue, DefSite, FieldRef, MethodRef, PhiNode, PhiOperand, SsaBlock, SsaFunction,
SsaFunctionBuilder, SsaInstruction, SsaOp, SsaType, SsaVarId, SsaVariable, TypeRef,
VariableOrigin,
},
assembly::decode_stream,
deobfuscation::{DeobfuscationEngine, EngineConfig},
metadata::{
method::MethodBody, tables::MethodDefRaw, token::Token, validation::ValidationConfig,
},
CilObject, Parser,
};
use super::{SsaCodeGenerator, VarStorage};
fn generate_mnemonics(ssa: &SsaFunction) -> Vec<&'static str> {
let mut gen = SsaCodeGenerator::new();
let (bytecode, _, _) = gen.generate(ssa).expect("codegen failed");
let instructions = decode_stream(&mut Parser::new(&bytecode), 0x1000).expect("decode failed");
instructions.iter().map(|i| i.mnemonic).collect()
}
fn assert_generates(ssa: &SsaFunction, expected: &[&str]) {
let mnemonics = generate_mnemonics(ssa);
assert_eq!(
mnemonics, expected,
"Mnemonic mismatch.\nExpected: {:?}\nGot: {:?}",
expected, mnemonics
);
}
fn assert_generates_ok(ssa: &SsaFunction) {
let mut gen = SsaCodeGenerator::new();
let result = gen.generate(ssa);
assert!(result.is_ok(), "Code generation failed: {:?}", result.err());
let (bytecode, _, _) = result.unwrap();
assert!(!bytecode.is_empty(), "Generated bytecode is empty");
}
#[test]
fn test_codegen_creation() {
let gen = SsaCodeGenerator::new();
assert!(gen.var_storage.is_empty());
assert_eq!(gen.next_local, 0);
}
#[test]
fn test_codegen_default() {
let gen = SsaCodeGenerator::default();
assert!(gen.var_storage.is_empty());
}
#[test]
fn test_var_storage_equality() {
assert_eq!(VarStorage::Arg(0), VarStorage::Arg(0));
assert_eq!(VarStorage::Local(5), VarStorage::Local(5));
assert_eq!(VarStorage::Stack, VarStorage::Stack);
assert_ne!(VarStorage::Arg(0), VarStorage::Local(0));
}
#[test]
fn test_var_storage_debug() {
let storage = VarStorage::Arg(1);
let debug = format!("{:?}", storage);
assert!(debug.contains("Arg"));
}
#[test]
fn test_empty_ssa_function() {
let mut gen = SsaCodeGenerator::new();
let ssa = SsaFunction::new(0, 0);
let result = gen.generate(&ssa);
assert!(result.is_ok());
let (bytecode, _, _) = result.unwrap();
assert!(bytecode.is_empty());
}
#[test]
fn test_void_return() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| b.ret());
});
assert_generates(&ssa, &["ret"]);
}
#[test]
fn test_simple_codegen() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_i32(42);
b.ret_val(v);
});
});
assert_generates(&ssa, &["ldc.i4.s", "ret"]);
}
#[test]
fn test_optimized_return() {
let mut ssa = SsaFunction::new(2, 0);
let var0 = SsaVariable::new(VariableOrigin::Argument(0), 0, DefSite::phi(0));
let v0 = var0.id();
ssa.add_variable(var0);
let var1 = SsaVariable::new(VariableOrigin::Argument(1), 0, DefSite::phi(0));
let v1 = var1.id();
ssa.add_variable(var1);
let var2 = SsaVariable::new(VariableOrigin::Stack(0), 0, DefSite::instruction(0, 0));
let v2 = var2.id();
ssa.add_variable(var2);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: v2,
left: v0,
right: v1,
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v2) }));
ssa.add_block(block);
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "add", "ret"]);
}
#[test]
fn test_const_i32_special_values() {
let expected_mnemonics = [
("ldc.i4.m1", -1i32),
("ldc.i4.0", 0),
("ldc.i4.1", 1),
("ldc.i4.2", 2),
("ldc.i4.3", 3),
("ldc.i4.4", 4),
("ldc.i4.5", 5),
("ldc.i4.6", 6),
("ldc.i4.7", 7),
("ldc.i4.8", 8),
];
for (expected_mnemonic, value) in expected_mnemonics {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_i32(value);
b.ret_val(v);
});
});
assert_generates(&ssa, &[expected_mnemonic, "ret"]);
}
}
#[test]
fn test_const_i32_short_form() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_i32(100);
b.ret_val(v);
});
});
assert_generates(&ssa, &["ldc.i4.s", "ret"]);
}
#[test]
fn test_const_i32_full() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_i32(1_000_000);
b.ret_val(v);
});
});
assert_generates(&ssa, &["ldc.i4", "ret"]);
}
#[test]
fn test_const_i64() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_i64(9_000_000_000);
b.ret_val(v);
});
});
assert_generates(&ssa, &["ldc.i8", "ret"]);
}
#[test]
fn test_const_null() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_null();
b.ret_val(v);
});
});
assert_generates(&ssa, &["ldnull", "ret"]);
}
#[test]
fn test_const_true_false() {
let ssa_true = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_true();
b.ret_val(v);
});
});
assert_generates(&ssa_true, &["ldc.i4.1", "ret"]);
let ssa_false = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_false();
b.ret_val(v);
});
});
assert_generates(&ssa_false, &["ldc.i4.0", "ret"]);
}
#[test]
fn test_const_floats() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_f32(std::f32::consts::PI);
b.ret_val(v);
});
});
assert_generates(&ssa, &["ldc.r4", "ret"]);
}
#[test]
fn test_const_f64() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
let v = b.const_f64(std::f64::consts::E);
b.ret_val(v);
});
});
assert_generates(&ssa, &["ldc.r8", "ret"]);
}
#[test]
fn test_add() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.add(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "add", "ret"]);
}
#[test]
fn test_sub() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.sub(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "sub", "ret"]);
}
#[test]
fn test_mul() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.mul(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "mul", "ret"]);
}
#[test]
fn test_div_signed() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.div(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "div", "ret"]);
}
#[test]
fn test_div_unsigned() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.div_un(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "div.un", "ret"]);
}
#[test]
fn test_rem_signed() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.rem(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "rem", "ret"]);
}
#[test]
fn test_rem_unsigned() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.rem_un(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "rem.un", "ret"]);
}
#[test]
fn test_add_ovf_signed() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
let dest = f.var();
f.block(0, |blk| {
blk.op(SsaOp::AddOvf {
dest,
left: a,
right: b,
unsigned: false,
});
blk.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "add.ovf", "ret"]);
}
#[test]
fn test_add_ovf_unsigned() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
let dest = f.var();
f.block(0, |blk| {
blk.op(SsaOp::AddOvf {
dest,
left: a,
right: b,
unsigned: true,
});
blk.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "add.ovf.un", "ret"]);
}
#[test]
fn test_sub_ovf_signed() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
let dest = f.var();
f.block(0, |blk| {
blk.op(SsaOp::SubOvf {
dest,
left: a,
right: b,
unsigned: false,
});
blk.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "sub.ovf", "ret"]);
}
#[test]
fn test_sub_ovf_unsigned() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
let dest = f.var();
f.block(0, |blk| {
blk.op(SsaOp::SubOvf {
dest,
left: a,
right: b,
unsigned: true,
});
blk.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "sub.ovf.un", "ret"]);
}
#[test]
fn test_mul_ovf_signed() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
let dest = f.var();
f.block(0, |blk| {
blk.op(SsaOp::MulOvf {
dest,
left: a,
right: b,
unsigned: false,
});
blk.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "mul.ovf", "ret"]);
}
#[test]
fn test_mul_ovf_unsigned() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
let dest = f.var();
f.block(0, |blk| {
blk.op(SsaOp::MulOvf {
dest,
left: a,
right: b,
unsigned: true,
});
blk.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "mul.ovf.un", "ret"]);
}
#[test]
fn test_neg() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.neg(a);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "neg", "ret"]);
}
#[test]
fn test_not() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.not(a);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "not", "ret"]);
}
#[test]
fn test_and() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.and(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "and", "ret"]);
}
#[test]
fn test_or() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.or(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "or", "ret"]);
}
#[test]
fn test_xor() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.xor(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "xor", "ret"]);
}
#[test]
fn test_shl() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.shl(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "shl", "ret"]);
}
#[test]
fn test_shr_signed() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.shr(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "shr", "ret"]);
}
#[test]
fn test_shr_unsigned() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.shr_un(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "shr.un", "ret"]);
}
#[test]
fn test_ceq() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.ceq(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "ceq", "ret"]);
}
#[test]
fn test_clt_signed() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.clt(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "clt", "ret"]);
}
#[test]
fn test_clt_unsigned() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.clt_un(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "clt.un", "ret"]);
}
#[test]
fn test_cgt_signed() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.cgt(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "cgt", "ret"]);
}
#[test]
fn test_cgt_unsigned() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (a, b) = (f.arg(0), f.arg(1));
f.block(0, |blk| {
let r = blk.cgt_un(a, b);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "cgt.un", "ret"]);
}
#[test]
fn test_jump_fallthrough_optimization() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| b.jump(1));
f.block(1, |b| b.ret());
});
assert_generates(&ssa, &["ret"]);
}
#[test]
fn test_branch_instruction() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let cond = f.arg(0);
f.block(0, |b| b.branch(cond, 1, 2));
f.block(1, |b| {
let v = b.const_i32(1);
b.ret_val(v);
});
f.block(2, |b| {
let v = b.const_i32(0);
b.ret_val(v);
});
});
let mnemonics = generate_mnemonics(&ssa);
assert!(
mnemonics.iter().any(|m| m.starts_with("br")),
"Expected branch instruction, got {:?}",
mnemonics
);
}
#[test]
fn test_switch_instruction() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let v0 = f.arg(0);
f.block(0, |blk| {
blk.op(SsaOp::Switch {
value: v0,
targets: vec![1, 2, 3],
default: 4,
});
});
for i in 1..=4 {
f.block(i, |blk| {
blk.ret();
});
}
});
let mnemonics = generate_mnemonics(&ssa);
assert!(
mnemonics.contains(&"switch"),
"Expected switch instruction, got {:?}",
mnemonics
);
}
#[test]
fn test_conversion_i8() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.conv(a, SsaType::I8);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "conv.i1", "ret"]);
}
#[test]
fn test_conversion_i16() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.conv(a, SsaType::I16);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "conv.i2", "ret"]);
}
#[test]
fn test_conversion_i32() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.conv(a, SsaType::I32);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "conv.i4", "ret"]);
}
#[test]
fn test_conversion_i64() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.conv(a, SsaType::I64);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "conv.i8", "ret"]);
}
#[test]
fn test_conversion_u8() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.conv(a, SsaType::U8);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "conv.u1", "ret"]);
}
#[test]
fn test_conversion_u16() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.conv(a, SsaType::U16);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "conv.u2", "ret"]);
}
#[test]
fn test_conversion_u32() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.conv(a, SsaType::U32);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "conv.u4", "ret"]);
}
#[test]
fn test_conversion_f32() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.conv(a, SsaType::F32);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "conv.r4", "ret"]);
}
#[test]
fn test_conversion_f64() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let a = f.arg(0);
f.block(0, |blk| {
let r = blk.conv(a, SsaType::F64);
blk.ret_val(r);
});
});
assert_generates(&ssa, &["ldarg.0", "conv.r8", "ret"]);
}
#[test]
fn test_dup_instruction() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let v0 = f.arg(0);
f.block(0, |blk| {
let v1 = blk.copy(v0);
blk.ret_val(v1);
});
});
assert_generates_ok(&ssa);
}
#[test]
fn test_pop_instruction() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let value = f.arg(0);
f.block(0, |b| {
b.op(SsaOp::Pop { value });
b.ret();
});
});
assert_generates(&ssa, &["ldarg.0", "pop", "ret"]);
}
#[test]
fn test_nop_instruction() {
let ssa = SsaFunctionBuilder::new(0, 0).build_with(|f| {
f.block(0, |b| {
b.nop();
b.ret();
});
});
assert_generates(&ssa, &["ret"]);
}
#[test]
fn test_break_instruction() {
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::Break));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(block);
assert_generates(&ssa, &["break", "ret"]);
}
#[test]
fn test_throw_instruction() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let exc = f.arg(0);
f.block(0, |b| b.throw(exc));
});
assert_generates(&ssa, &["ldarg.0", "throw"]);
}
#[test]
fn test_rethrow_instruction() {
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::Rethrow));
ssa.add_block(block);
assert_generates(&ssa, &["rethrow"]);
}
#[test]
fn test_ldfld() {
let field_token = Token::new(0x04000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let object = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::LoadField {
dest,
object,
field: FieldRef::new(field_token),
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldfld", "ret"]);
}
#[test]
fn test_stfld() {
let field_token = Token::new(0x04000001);
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (object, value) = (f.arg(0), f.arg(1));
f.block(0, |b| {
b.op(SsaOp::StoreField {
object,
field: FieldRef::new(field_token),
value,
});
b.ret();
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "stfld", "ret"]);
}
#[test]
fn test_ldsfld() {
let field_token = Token::new(0x04000001);
let v0 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::LoadStaticField {
dest: v0,
field: FieldRef::new(field_token),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v0) }));
ssa.add_block(block);
assert_generates(&ssa, &["ldsfld", "ret"]);
}
#[test]
fn test_stsfld() {
let field_token = Token::new(0x04000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let value = f.arg(0);
f.block(0, |b| {
b.op(SsaOp::StoreStaticField {
field: FieldRef::new(field_token),
value,
});
b.ret();
});
});
assert_generates(&ssa, &["ldarg.0", "stsfld", "ret"]);
}
#[test]
fn test_ldflda() {
let field_token = Token::new(0x04000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let object = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::LoadFieldAddr {
dest,
object,
field: FieldRef::new(field_token),
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldflda", "ret"]);
}
#[test]
fn test_ldsflda() {
let field_token = Token::new(0x04000001);
let v0 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::LoadStaticFieldAddr {
dest: v0,
field: FieldRef::new(field_token),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v0) }));
ssa.add_block(block);
assert_generates(&ssa, &["ldsflda", "ret"]);
}
#[test]
fn test_ldloca() {
let ssa = SsaFunctionBuilder::new(0, 1).build_with(|f| {
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::LoadLocalAddr {
dest,
local_index: 0,
});
b.ret_val(dest);
});
});
let mnemonics = generate_mnemonics(&ssa);
assert!(
mnemonics.iter().any(|m| m.starts_with("ldloca")),
"Expected ldloca, got {:?}",
mnemonics
);
}
#[test]
fn test_ldarga() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::LoadArgAddr { dest, arg_index: 0 });
b.ret_val(dest);
});
});
let mnemonics = generate_mnemonics(&ssa);
assert!(
mnemonics.iter().any(|m| m.starts_with("ldarga")),
"Expected ldarga, got {:?}",
mnemonics
);
}
#[test]
fn test_call() {
let method_token = Token::new(0x06000001);
let v0 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::Call {
dest: Some(v0),
method: MethodRef::new(method_token),
args: vec![],
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v0) }));
ssa.add_block(block);
assert_generates(&ssa, &["call", "ret"]);
}
#[test]
fn test_call_void() {
let method_token = Token::new(0x06000001);
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::Call {
dest: None,
method: MethodRef::new(method_token),
args: vec![],
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: None }));
ssa.add_block(block);
assert_generates(&ssa, &["call", "ret"]);
}
#[test]
fn test_call_with_args() {
let method_token = Token::new(0x06000001);
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (arg0, arg1) = (f.arg(0), f.arg(1));
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::Call {
dest: Some(dest),
method: MethodRef::new(method_token),
args: vec![arg0, arg1],
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "call", "ret"]);
}
#[test]
fn test_callvirt() {
let method_token = Token::new(0x06000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let object = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::CallVirt {
dest: Some(dest),
method: MethodRef::new(method_token),
args: vec![object],
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "callvirt", "ret"]);
}
#[test]
fn test_newobj() {
let ctor_token = Token::new(0x06000001);
let v0 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::NewObj {
dest: v0,
ctor: MethodRef::new(ctor_token),
args: vec![],
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v0) }));
ssa.add_block(block);
assert_generates(&ssa, &["newobj", "ret"]);
}
#[test]
fn test_newobj_with_args() {
let ctor_token = Token::new(0x06000001);
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (arg0, arg1) = (f.arg(0), f.arg(1));
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::NewObj {
dest,
ctor: MethodRef::new(ctor_token),
args: vec![arg0, arg1],
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "newobj", "ret"]);
}
#[test]
fn test_newarr() {
let type_token = Token::new(0x02000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let len = f.arg(0);
let dest = f.var();
f.block(0, |blk| {
blk.op(SsaOp::NewArr {
dest,
elem_type: TypeRef(type_token),
length: len,
});
blk.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "newarr", "ret"]);
}
#[test]
fn test_ldlen() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let arr = f.arg(0);
f.block(0, |b| {
let len = b.array_length(arr);
b.ret_val(len);
});
});
assert_generates(&ssa, &["ldarg.0", "ldlen", "ret"]);
}
#[test]
fn test_ldelem_i4() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (arr, idx) = (f.arg(0), f.arg(1));
let dest = f.var();
f.block(0, |blk| {
blk.op(SsaOp::LoadElement {
dest,
array: arr,
index: idx,
elem_type: SsaType::I32,
});
blk.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "ldelem.i4", "ret"]);
}
#[test]
fn test_stelem_i4() {
let ssa = SsaFunctionBuilder::new(3, 0).build_with(|f| {
let (arr, idx, val) = (f.arg(0), f.arg(1), f.arg(2));
f.block(0, |blk| {
blk.op(SsaOp::StoreElement {
array: arr,
index: idx,
value: val,
elem_type: SsaType::I32,
});
blk.ret();
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "ldarg.2", "stelem.i4", "ret"]);
}
#[test]
fn test_box() {
let type_token = Token::new(0x02000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let value = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::Box {
dest,
value,
value_type: TypeRef(type_token),
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "box", "ret"]);
}
#[test]
fn test_unbox() {
let type_token = Token::new(0x02000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let object = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::Unbox {
dest,
object,
value_type: TypeRef(type_token),
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "unbox", "ret"]);
}
#[test]
fn test_unbox_any() {
let type_token = Token::new(0x02000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let object = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::UnboxAny {
dest,
object,
value_type: TypeRef(type_token),
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "unbox.any", "ret"]);
}
#[test]
fn test_isinst() {
let type_token = Token::new(0x02000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let object = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::IsInst {
dest,
object,
target_type: TypeRef(type_token),
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "isinst", "ret"]);
}
#[test]
fn test_castclass() {
let type_token = Token::new(0x02000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let object = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::CastClass {
dest,
object,
target_type: TypeRef(type_token),
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "castclass", "ret"]);
}
#[test]
fn test_sizeof() {
let type_token = Token::new(0x02000001);
let v0 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::SizeOf {
dest: v0,
value_type: TypeRef(type_token),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v0) }));
ssa.add_block(block);
assert_generates(&ssa, &["sizeof", "ret"]);
}
#[test]
fn test_initobj() {
let type_token = Token::new(0x02000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let dest_addr = f.arg(0);
f.block(0, |b| {
b.op(SsaOp::InitObj {
dest_addr,
value_type: TypeRef(type_token),
});
b.ret();
});
});
assert_generates(&ssa, &["ldarg.0", "initobj", "ret"]);
}
#[test]
fn test_ldobj() {
let type_token = Token::new(0x02000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let src_addr = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::LoadObj {
dest,
src_addr,
value_type: TypeRef(type_token),
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldobj", "ret"]);
}
#[test]
fn test_stobj() {
let type_token = Token::new(0x02000001);
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let (dest_addr, value) = (f.arg(0), f.arg(1));
f.block(0, |b| {
b.op(SsaOp::StoreObj {
dest_addr,
value,
value_type: TypeRef(type_token),
});
b.ret();
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "stobj", "ret"]);
}
#[test]
fn test_ldtoken() {
let token = Token::new(0x02000001);
let v0 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::LoadToken {
dest: v0,
token: TypeRef(token),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v0) }));
ssa.add_block(block);
assert_generates(&ssa, &["ldtoken", "ret"]);
}
#[test]
fn test_ldftn() {
let method_token = Token::new(0x06000001);
let v0 = SsaVarId::new();
let mut ssa = SsaFunction::new(0, 0);
let mut block = SsaBlock::new(0);
block.add_instruction(SsaInstruction::synthetic(SsaOp::LoadFunctionPtr {
dest: v0,
method: MethodRef::new(method_token),
}));
block.add_instruction(SsaInstruction::synthetic(SsaOp::Return { value: Some(v0) }));
ssa.add_block(block);
assert_generates(&ssa, &["ldftn", "ret"]);
}
#[test]
fn test_ldvirtftn() {
let method_token = Token::new(0x06000001);
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let object = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::LoadVirtFunctionPtr {
dest,
object,
method: MethodRef::new(method_token),
});
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ldvirtftn", "ret"]);
}
#[test]
fn test_localloc() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let size = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::LocalAlloc { dest, size });
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "localloc", "ret"]);
}
#[test]
fn test_ckfinite() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let operand = f.arg(0);
let dest = f.var();
f.block(0, |b| {
b.op(SsaOp::Ckfinite { dest, operand });
b.ret_val(dest);
});
});
assert_generates(&ssa, &["ldarg.0", "ckfinite", "ret"]);
}
#[test]
fn test_is_immediately_consumed_return() {
let gen = SsaCodeGenerator::new();
let var = SsaVarId::new();
let next = SsaOp::Return { value: Some(var) };
assert!(gen.is_immediately_consumed(var, Some(&next)));
}
#[test]
fn test_is_immediately_consumed_branch() {
let gen = SsaCodeGenerator::new();
let var = SsaVarId::new();
let next = SsaOp::Branch {
condition: var,
true_target: 1,
false_target: 2,
};
assert!(gen.is_immediately_consumed(var, Some(&next)));
}
#[test]
fn test_is_immediately_consumed_add_left() {
let gen = SsaCodeGenerator::new();
let var = SsaVarId::new();
let v1 = SsaVarId::new();
let v2 = SsaVarId::new();
let next = SsaOp::Add {
dest: v2,
left: var,
right: v1,
};
assert!(gen.is_immediately_consumed(var, Some(&next)));
}
#[test]
fn test_is_immediately_consumed_right_operand_simple_left() {
let mut gen = SsaCodeGenerator::new();
let var = SsaVarId::new();
let other_var = SsaVarId::new();
let v2 = SsaVarId::new();
gen.var_storage.insert(other_var, VarStorage::Arg(0));
let next = SsaOp::Add {
dest: v2,
left: other_var,
right: var,
};
assert!(gen.is_immediately_consumed(var, Some(&next)));
}
#[test]
fn test_is_immediately_consumed_right_operand_complex_left() {
let gen = SsaCodeGenerator::new();
let var = SsaVarId::new();
let other_var = SsaVarId::new();
let v2 = SsaVarId::new();
let next = SsaOp::Add {
dest: v2,
left: other_var,
right: var,
};
assert!(!gen.is_immediately_consumed(var, Some(&next)));
}
#[test]
fn test_dup_optimization_same_arg_twice() {
let ssa = SsaFunctionBuilder::new(1, 0).build_with(|f| {
let x = f.arg(0);
let result = f.var();
f.block(0, |b| {
b.op(SsaOp::Mul {
dest: result,
left: x,
right: x,
});
b.ret_val(result);
});
});
assert_generates(&ssa, &["ldarg.0", "dup", "mul", "ret"]);
}
#[test]
fn test_dup_optimization_same_local_twice() {
let ssa = SsaFunctionBuilder::new(0, 1).build_with(|f| {
let x = f.local(0);
let result = f.var();
f.block(0, |b| {
b.op(SsaOp::Add {
dest: result,
left: x,
right: x,
});
b.ret_val(result);
});
});
assert_generates(&ssa, &["ldloc.0", "dup", "add", "ret"]);
}
#[test]
fn test_no_dup_for_different_vars() {
let ssa = SsaFunctionBuilder::new(2, 0).build_with(|f| {
let x = f.arg(0);
let y = f.arg(1);
let result = f.var();
f.block(0, |b| {
b.op(SsaOp::Add {
dest: result,
left: x,
right: y,
});
b.ret_val(result);
});
});
assert_generates(&ssa, &["ldarg.0", "ldarg.1", "add", "ret"]);
}
#[test]
fn test_loop_variable_phi_nodes() {
let mut ssa = SsaFunction::new(1, 1);
let n = SsaVariable::new(VariableOrigin::Argument(0), 0, DefSite::phi(0));
let n_id = n.id();
ssa.add_variable(n);
let i_init = SsaVariable::new(VariableOrigin::Stack(0), 0, DefSite::instruction(0, 0));
let i_init_id = i_init.id();
ssa.add_variable(i_init);
let i_phi = SsaVariable::new(VariableOrigin::Local(0), 0, DefSite::phi(1));
let i_phi_id = i_phi.id();
ssa.add_variable(i_phi);
let cond = SsaVariable::new(VariableOrigin::Stack(1), 0, DefSite::instruction(1, 0));
let cond_id = cond.id();
ssa.add_variable(cond);
let one = SsaVariable::new(VariableOrigin::Stack(2), 0, DefSite::instruction(2, 0));
let one_id = one.id();
ssa.add_variable(one);
let i_next = SsaVariable::new(VariableOrigin::Stack(3), 0, DefSite::instruction(2, 1));
let i_next_id = i_next.id();
ssa.add_variable(i_next);
let mut block0 = SsaBlock::new(0);
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: i_init_id,
value: ConstValue::I32(0),
}));
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block0);
let mut block1 = SsaBlock::new(1);
let mut phi = PhiNode::new(i_phi_id, VariableOrigin::Local(0));
phi.add_operand(PhiOperand::new(i_init_id, 0));
phi.add_operand(PhiOperand::new(i_next_id, 2));
block1.add_phi(phi);
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Clt {
dest: cond_id,
left: i_phi_id,
right: n_id,
unsigned: false,
}));
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: cond_id,
true_target: 2,
false_target: 3,
}));
ssa.add_block(block1);
let mut block2 = SsaBlock::new(2);
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: one_id,
value: ConstValue::I32(1),
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: i_next_id,
left: i_phi_id,
right: one_id,
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block2);
let mut block3 = SsaBlock::new(3);
block3.add_instruction(SsaInstruction::synthetic(SsaOp::Return {
value: Some(i_phi_id),
}));
ssa.add_block(block3);
let mut gen = SsaCodeGenerator::new();
let result = gen.generate(&ssa);
assert!(result.is_ok(), "Codegen failed: {:?}", result.err());
let (bytecode, _, num_locals) = result.unwrap();
assert!(
num_locals >= 1,
"Should have at least 1 local for loop variable"
);
let instructions = decode_stream(&mut Parser::new(&bytecode), 0x1000).expect("decode failed");
let mnemonics: Vec<&str> = instructions.iter().map(|i| i.mnemonic).collect();
let ldloc_count = mnemonics.iter().filter(|m| m.starts_with("ldloc")).count();
assert!(
ldloc_count >= 2,
"Expected at least 2 ldloc for loop variable, got {}. Mnemonics: {:?}",
ldloc_count,
mnemonics
);
let stloc_count = mnemonics.iter().filter(|m| m.starts_with("stloc")).count();
assert!(
stloc_count >= 1,
"Expected at least 1 stloc for loop variable, got {}. Mnemonics: {:?}",
stloc_count,
mnemonics
);
assert!(
mnemonics.contains(&"ret"),
"Should have ret instruction. Mnemonics: {:?}",
mnemonics
);
let ret_idx = mnemonics.iter().position(|m| *m == "ret").unwrap();
assert!(
ret_idx > 0,
"ret should not be the first instruction. Mnemonics: {:?}",
mnemonics
);
let before_ret = mnemonics[ret_idx - 1];
assert!(
before_ret.starts_with("ldloc"),
"Should load loop variable before ret, got '{}'. Mnemonics: {:?}",
before_ret,
mnemonics
);
println!("Loop codegen test passed. Generated: {:?}", mnemonics);
}
#[test]
fn test_fibonacci_style_loop() {
let mut ssa = SsaFunction::new(1, 4);
let n = SsaVariable::new(VariableOrigin::Argument(0), 0, DefSite::phi(0));
let n_id = n.id();
ssa.add_variable(n);
let a_init = SsaVariable::new(VariableOrigin::Stack(0), 0, DefSite::instruction(0, 0));
let a_init_id = a_init.id();
ssa.add_variable(a_init);
let b_init = SsaVariable::new(VariableOrigin::Stack(1), 0, DefSite::instruction(0, 1));
let b_init_id = b_init.id();
ssa.add_variable(b_init);
let i_init = SsaVariable::new(VariableOrigin::Stack(2), 0, DefSite::instruction(0, 2));
let i_init_id = i_init.id();
ssa.add_variable(i_init);
let a_phi = SsaVariable::new(VariableOrigin::Local(0), 0, DefSite::phi(1));
let a_phi_id = a_phi.id();
ssa.add_variable(a_phi);
let b_phi = SsaVariable::new(VariableOrigin::Local(1), 0, DefSite::phi(1));
let b_phi_id = b_phi.id();
ssa.add_variable(b_phi);
let i_phi = SsaVariable::new(VariableOrigin::Local(2), 0, DefSite::phi(1));
let i_phi_id = i_phi.id();
ssa.add_variable(i_phi);
let cond = SsaVariable::new(VariableOrigin::Stack(3), 0, DefSite::instruction(1, 0));
let cond_id = cond.id();
ssa.add_variable(cond);
let temp = SsaVariable::new(VariableOrigin::Local(3), 0, DefSite::instruction(2, 0));
let temp_id = temp.id();
ssa.add_variable(temp);
let one = SsaVariable::new(VariableOrigin::Stack(4), 0, DefSite::instruction(2, 1));
let one_id = one.id();
ssa.add_variable(one);
let i_next = SsaVariable::new(VariableOrigin::Stack(5), 0, DefSite::instruction(2, 2));
let i_next_id = i_next.id();
ssa.add_variable(i_next);
let mut block0 = SsaBlock::new(0);
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: a_init_id,
value: ConstValue::I32(0),
}));
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: b_init_id,
value: ConstValue::I32(1),
}));
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: i_init_id,
value: ConstValue::I32(2),
}));
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block0);
let mut block1 = SsaBlock::new(1);
let mut phi_a = PhiNode::new(a_phi_id, VariableOrigin::Local(0));
phi_a.add_operand(PhiOperand::new(a_init_id, 0));
phi_a.add_operand(PhiOperand::new(b_phi_id, 2)); block1.add_phi(phi_a);
let mut phi_b = PhiNode::new(b_phi_id, VariableOrigin::Local(1));
phi_b.add_operand(PhiOperand::new(b_init_id, 0));
phi_b.add_operand(PhiOperand::new(temp_id, 2)); block1.add_phi(phi_b);
let mut phi_i = PhiNode::new(i_phi_id, VariableOrigin::Local(2));
phi_i.add_operand(PhiOperand::new(i_init_id, 0));
phi_i.add_operand(PhiOperand::new(i_next_id, 2));
block1.add_phi(phi_i);
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Cgt {
dest: cond_id,
left: i_phi_id,
right: n_id,
unsigned: false,
}));
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: cond_id,
true_target: 3, false_target: 2, }));
ssa.add_block(block1);
let mut block2 = SsaBlock::new(2);
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: temp_id,
left: a_phi_id,
right: b_phi_id,
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: one_id,
value: ConstValue::I32(1),
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: i_next_id,
left: i_phi_id,
right: one_id,
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block2);
let mut block3 = SsaBlock::new(3);
block3.add_instruction(SsaInstruction::synthetic(SsaOp::Return {
value: Some(b_phi_id),
}));
ssa.add_block(block3);
let mut gen = SsaCodeGenerator::new();
let result = gen.generate(&ssa);
assert!(result.is_ok(), "Codegen failed: {:?}", result.err());
let (bytecode, _, num_locals) = result.unwrap();
println!(
"Fibonacci-style loop: {} bytes, {} locals",
bytecode.len(),
num_locals
);
let instructions = decode_stream(&mut Parser::new(&bytecode), 0x1000).expect("decode failed");
let mnemonics: Vec<&str> = instructions.iter().map(|i| i.mnemonic).collect();
println!("Generated: {:?}", mnemonics);
assert!(
num_locals >= 3,
"Should have at least 3 locals for loop variables, got {}",
num_locals
);
assert!(mnemonics.contains(&"ret"), "Should have ret");
let ret_idx = mnemonics.iter().position(|m| *m == "ret").unwrap();
let before_ret = mnemonics[ret_idx - 1];
assert!(
before_ret.starts_with("ldloc"),
"Should load b before ret, got '{}'. Mnemonics: {:?}",
before_ret,
mnemonics
);
let add_count = mnemonics.iter().filter(|m| **m == "add").count();
assert!(
add_count >= 2,
"Should have at least 2 add instructions, got {}. Mnemonics: {:?}",
add_count,
mnemonics
);
let ldloc_count = mnemonics.iter().filter(|m| m.starts_with("ldloc")).count();
assert!(
ldloc_count >= 4,
"Should have at least 4 ldloc (a, b for add, i for cmp, b for ret), got {}. Mnemonics: {:?}",
ldloc_count,
mnemonics
);
}
#[test]
fn test_fibonacci_deobfuscation_preserves_semantics() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/samples/packers/source/TestApp/TestApp.exe");
let assembly = match CilObject::from_path_with_validation(&path, ValidationConfig::analysis()) {
Ok(asm) => asm,
Err(_) => return, };
let config = EngineConfig::default();
let mut engine = DeobfuscationEngine::new(config);
let (output, _) = engine.process_assembly(assembly).expect("deobfuscation");
let tables = output.tables().expect("tables");
let method_table = tables.table::<MethodDefRaw>().expect("method table");
let strings = output.strings().expect("strings");
let mut found = false;
for row in method_table.iter() {
let name = strings.get(row.name as usize).unwrap_or("");
if name != "Fibonacci" {
continue;
}
found = true;
let rva = row.rva as usize;
if rva == 0 {
continue;
}
let file = output.file();
let offset = file.rva_to_offset(rva).expect("rva to offset");
let data = &file.data()[offset..];
let body = MethodBody::from(data).expect("method body");
let bytecode = &data[body.size_header..body.size_header + body.size_code];
let instructions = decode_stream(&mut Parser::new(bytecode), 0).expect("decode");
let mnemonics: Vec<&str> = instructions.iter().map(|i| i.mnemonic).collect();
let ret_positions: Vec<usize> = mnemonics
.iter()
.enumerate()
.filter(|(_, m)| **m == "ret")
.map(|(i, _)| i)
.collect();
if let Some(&last_ret) = ret_positions.last() {
if last_ret > 0 {
let before_ret = mnemonics[last_ret - 1];
assert!(
!before_ret.starts_with("ldc.i4"),
"BUG: Fibonacci returns a constant instead of computed value. \
The final return is preceded by '{}' instead of ldloc",
before_ret
);
}
}
for (i, mnemonic) in mnemonics.iter().enumerate() {
if *mnemonic == "add" && i >= 2 {
let prev1 = mnemonics[i - 1];
let prev2 = mnemonics[i - 2];
if prev1.starts_with("ldloc") && prev2.starts_with("ldloc") {
assert_ne!(
prev1, prev2,
"BUG: add uses same local twice ({}, {}). Should be a + b, not a + a",
prev2, prev1
);
}
}
}
}
assert!(found, "Fibonacci method not found");
}
#[test]
fn test_fibonacci_pass_combinations() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/samples/packers/source/TestApp/TestApp.exe");
let configs: Vec<(&str, EngineConfig)> = vec![
(
"copy_prop_only",
EngineConfig {
enable_dead_code_elimination: false,
enable_copy_propagation: true,
enable_constant_propagation: false,
enable_strength_reduction: false,
enable_control_flow_simplification: false,
enable_opaque_predicate_removal: false,
..EngineConfig::default()
},
),
(
"dce_copy_prop",
EngineConfig {
enable_dead_code_elimination: true,
enable_copy_propagation: true,
enable_constant_propagation: false,
enable_strength_reduction: false,
enable_control_flow_simplification: false,
enable_opaque_predicate_removal: false,
..EngineConfig::default()
},
),
("full_pipeline", EngineConfig::default()),
];
for (name, config) in configs {
let assembly =
match CilObject::from_path_with_validation(&path, ValidationConfig::analysis()) {
Ok(asm) => asm,
Err(_) => return, };
let mut engine = DeobfuscationEngine::new(config);
let (output, _) = engine.process_assembly(assembly).expect("deobfuscate");
let tables = output.tables().expect("tables");
let method_table = tables.table::<MethodDefRaw>().expect("method table");
let strings = output.strings().expect("strings");
for row in method_table.iter() {
let method_name = strings.get(row.name as usize).unwrap_or("");
if method_name != "Fibonacci" {
continue;
}
let rva = row.rva as usize;
if rva == 0 {
continue;
}
let file = output.file();
let offset = file.rva_to_offset(rva).expect("rva to offset");
let data = &file.data()[offset..];
let body = MethodBody::from(data).expect("method body");
let bytecode = &data[body.size_header..body.size_header + body.size_code];
let instructions = decode_stream(&mut Parser::new(bytecode), 0).expect("decode");
let mnemonics: Vec<&str> = instructions.iter().map(|i| i.mnemonic).collect();
let ret_positions: Vec<usize> = mnemonics
.iter()
.enumerate()
.filter(|(_, m)| **m == "ret")
.map(|(i, _)| i)
.collect();
if let Some(&last_ret) = ret_positions.last() {
if last_ret > 0 {
let before_ret = mnemonics[last_ret - 1];
assert!(
!before_ret.starts_with("ldc.i4"),
"Config '{}': Fibonacci returns constant instead of computed value",
name
);
}
}
}
}
}
#[test]
fn test_factorial_style_loop() {
let mut ssa = SsaFunction::new(1, 2);
let n = SsaVariable::new(VariableOrigin::Argument(0), 0, DefSite::phi(0));
let n_id = n.id();
ssa.add_variable(n);
let result_init = SsaVariable::new(VariableOrigin::Stack(0), 0, DefSite::instruction(0, 0));
let result_init_id = result_init.id();
ssa.add_variable(result_init);
let result_phi = SsaVariable::new(VariableOrigin::Local(0), 0, DefSite::phi(1));
let result_phi_id = result_phi.id();
ssa.add_variable(result_phi);
let i_phi = SsaVariable::new(VariableOrigin::Local(1), 0, DefSite::phi(1));
let i_phi_id = i_phi.id();
ssa.add_variable(i_phi);
let one = SsaVariable::new(VariableOrigin::Stack(1), 0, DefSite::instruction(1, 0));
let one_id = one.id();
ssa.add_variable(one);
let cond = SsaVariable::new(VariableOrigin::Stack(2), 0, DefSite::instruction(1, 1));
let cond_id = cond.id();
ssa.add_variable(cond);
let result_next = SsaVariable::new(VariableOrigin::Stack(3), 0, DefSite::instruction(2, 0));
let result_next_id = result_next.id();
ssa.add_variable(result_next);
let i_next = SsaVariable::new(VariableOrigin::Stack(4), 0, DefSite::instruction(2, 1));
let i_next_id = i_next.id();
ssa.add_variable(i_next);
let mut block0 = SsaBlock::new(0);
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: result_init_id,
value: ConstValue::I32(1),
}));
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block0);
let mut block1 = SsaBlock::new(1);
let mut phi_result = PhiNode::new(result_phi_id, VariableOrigin::Local(0));
phi_result.add_operand(PhiOperand::new(result_init_id, 0));
phi_result.add_operand(PhiOperand::new(result_next_id, 2));
block1.add_phi(phi_result);
let mut phi_i = PhiNode::new(i_phi_id, VariableOrigin::Local(1));
phi_i.add_operand(PhiOperand::new(n_id, 0));
phi_i.add_operand(PhiOperand::new(i_next_id, 2));
block1.add_phi(phi_i);
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: one_id,
value: ConstValue::I32(1),
}));
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Cgt {
dest: cond_id,
left: i_phi_id,
right: one_id,
unsigned: false,
}));
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: cond_id,
true_target: 2,
false_target: 3,
}));
ssa.add_block(block1);
let mut block2 = SsaBlock::new(2);
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Mul {
dest: result_next_id,
left: result_phi_id,
right: i_phi_id,
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Sub {
dest: i_next_id,
left: i_phi_id,
right: one_id,
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block2);
let mut block3 = SsaBlock::new(3);
block3.add_instruction(SsaInstruction::synthetic(SsaOp::Return {
value: Some(result_phi_id),
}));
ssa.add_block(block3);
let mut gen = SsaCodeGenerator::new();
let result = gen.generate(&ssa);
assert!(result.is_ok(), "Codegen failed: {:?}", result.err());
let (bytecode, _, num_locals) = result.unwrap();
let instructions = decode_stream(&mut Parser::new(&bytecode), 0x1000).expect("decode");
let mnemonics: Vec<&str> = instructions.iter().map(|i| i.mnemonic).collect();
assert!(mnemonics.contains(&"mul"), "Should have mul instruction");
assert!(mnemonics.contains(&"sub"), "Should have sub instruction");
let ret_idx = mnemonics.iter().position(|m| *m == "ret").unwrap();
assert!(
mnemonics[ret_idx - 1].starts_with("ldloc"),
"Should load result before ret"
);
assert!(
num_locals >= 2,
"Should have at least 2 locals, got {}",
num_locals
);
}
#[test]
fn test_nested_loop_pattern() {
let mut ssa = SsaFunction::new(2, 3);
let n = SsaVariable::new(VariableOrigin::Argument(0), 0, DefSite::phi(0));
let n_id = n.id();
ssa.add_variable(n);
let m = SsaVariable::new(VariableOrigin::Argument(1), 0, DefSite::phi(0));
let m_id = m.id();
ssa.add_variable(m);
let sum_init = SsaVariable::new(VariableOrigin::Stack(0), 0, DefSite::instruction(0, 0));
let sum_init_id = sum_init.id();
ssa.add_variable(sum_init);
let i_init = SsaVariable::new(VariableOrigin::Stack(1), 0, DefSite::instruction(0, 1));
let i_init_id = i_init.id();
ssa.add_variable(i_init);
let sum_outer = SsaVariable::new(VariableOrigin::Local(0), 0, DefSite::phi(1));
let sum_outer_id = sum_outer.id();
ssa.add_variable(sum_outer);
let i_phi = SsaVariable::new(VariableOrigin::Local(1), 0, DefSite::phi(1));
let i_phi_id = i_phi.id();
ssa.add_variable(i_phi);
let outer_cond = SsaVariable::new(VariableOrigin::Stack(2), 0, DefSite::instruction(1, 0));
let outer_cond_id = outer_cond.id();
ssa.add_variable(outer_cond);
let j_init = SsaVariable::new(VariableOrigin::Stack(3), 0, DefSite::instruction(2, 0));
let j_init_id = j_init.id();
ssa.add_variable(j_init);
let sum_inner = SsaVariable::new(VariableOrigin::Local(0), 0, DefSite::phi(3));
let sum_inner_id = sum_inner.id();
ssa.add_variable(sum_inner);
let j_phi = SsaVariable::new(VariableOrigin::Local(2), 0, DefSite::phi(3));
let j_phi_id = j_phi.id();
ssa.add_variable(j_phi);
let inner_cond = SsaVariable::new(VariableOrigin::Stack(4), 0, DefSite::instruction(3, 0));
let inner_cond_id = inner_cond.id();
ssa.add_variable(inner_cond);
let one = SsaVariable::new(VariableOrigin::Stack(5), 0, DefSite::instruction(4, 0));
let one_id = one.id();
ssa.add_variable(one);
let sum_next = SsaVariable::new(VariableOrigin::Stack(6), 0, DefSite::instruction(4, 1));
let sum_next_id = sum_next.id();
ssa.add_variable(sum_next);
let j_next = SsaVariable::new(VariableOrigin::Stack(7), 0, DefSite::instruction(4, 2));
let j_next_id = j_next.id();
ssa.add_variable(j_next);
let i_next = SsaVariable::new(VariableOrigin::Stack(8), 0, DefSite::instruction(5, 0));
let i_next_id = i_next.id();
ssa.add_variable(i_next);
let mut block0 = SsaBlock::new(0);
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: sum_init_id,
value: ConstValue::I32(0),
}));
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: i_init_id,
value: ConstValue::I32(0),
}));
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block0);
let mut block1 = SsaBlock::new(1);
let mut phi_sum_outer = PhiNode::new(sum_outer_id, VariableOrigin::Local(0));
phi_sum_outer.add_operand(PhiOperand::new(sum_init_id, 0));
phi_sum_outer.add_operand(PhiOperand::new(sum_inner_id, 5));
block1.add_phi(phi_sum_outer);
let mut phi_i = PhiNode::new(i_phi_id, VariableOrigin::Local(1));
phi_i.add_operand(PhiOperand::new(i_init_id, 0));
phi_i.add_operand(PhiOperand::new(i_next_id, 5));
block1.add_phi(phi_i);
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Clt {
dest: outer_cond_id,
left: i_phi_id,
right: n_id,
unsigned: false,
}));
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: outer_cond_id,
true_target: 2,
false_target: 6,
}));
ssa.add_block(block1);
let mut block2 = SsaBlock::new(2);
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: j_init_id,
value: ConstValue::I32(0),
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 3 }));
ssa.add_block(block2);
let mut block3 = SsaBlock::new(3);
let mut phi_sum_inner = PhiNode::new(sum_inner_id, VariableOrigin::Local(0));
phi_sum_inner.add_operand(PhiOperand::new(sum_outer_id, 2));
phi_sum_inner.add_operand(PhiOperand::new(sum_next_id, 4));
block3.add_phi(phi_sum_inner);
let mut phi_j = PhiNode::new(j_phi_id, VariableOrigin::Local(2));
phi_j.add_operand(PhiOperand::new(j_init_id, 2));
phi_j.add_operand(PhiOperand::new(j_next_id, 4));
block3.add_phi(phi_j);
block3.add_instruction(SsaInstruction::synthetic(SsaOp::Clt {
dest: inner_cond_id,
left: j_phi_id,
right: m_id,
unsigned: false,
}));
block3.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: inner_cond_id,
true_target: 4,
false_target: 5,
}));
ssa.add_block(block3);
let mut block4 = SsaBlock::new(4);
block4.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: one_id,
value: ConstValue::I32(1),
}));
block4.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: sum_next_id,
left: sum_inner_id,
right: one_id,
}));
block4.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: j_next_id,
left: j_phi_id,
right: one_id,
}));
block4.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 3 }));
ssa.add_block(block4);
let mut block5 = SsaBlock::new(5);
block5.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: one_id,
value: ConstValue::I32(1),
}));
block5.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: i_next_id,
left: i_phi_id,
right: one_id,
}));
block5.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block5);
let mut block6 = SsaBlock::new(6);
block6.add_instruction(SsaInstruction::synthetic(SsaOp::Return {
value: Some(sum_outer_id),
}));
ssa.add_block(block6);
let mut gen = SsaCodeGenerator::new();
let result = gen.generate(&ssa);
assert!(result.is_ok(), "Codegen failed: {:?}", result.err());
let (bytecode, _, num_locals) = result.unwrap();
let instructions = decode_stream(&mut Parser::new(&bytecode), 0x1000).expect("decode");
let mnemonics: Vec<&str> = instructions.iter().map(|i| i.mnemonic).collect();
let clt_count = mnemonics.iter().filter(|m| **m == "clt").count();
assert!(
clt_count >= 2,
"Should have at least 2 comparisons for nested loops"
);
let add_count = mnemonics.iter().filter(|m| **m == "add").count();
assert!(add_count >= 3, "Should have adds for sum++, j++, i++");
assert!(
num_locals >= 3,
"Should have at least 3 locals, got {}",
num_locals
);
}
#[test]
fn test_accumulator_with_early_exit() {
let mut ssa = SsaFunction::new(1, 2);
let n = SsaVariable::new(VariableOrigin::Argument(0), 0, DefSite::phi(0));
let n_id = n.id();
ssa.add_variable(n);
let sum_init = SsaVariable::new(VariableOrigin::Stack(0), 0, DefSite::instruction(0, 0));
let sum_init_id = sum_init.id();
ssa.add_variable(sum_init);
let i_init = SsaVariable::new(VariableOrigin::Stack(1), 0, DefSite::instruction(0, 1));
let i_init_id = i_init.id();
ssa.add_variable(i_init);
let sum_phi = SsaVariable::new(VariableOrigin::Local(0), 0, DefSite::phi(1));
let sum_phi_id = sum_phi.id();
ssa.add_variable(sum_phi);
let i_phi = SsaVariable::new(VariableOrigin::Local(1), 0, DefSite::phi(1));
let i_phi_id = i_phi.id();
ssa.add_variable(i_phi);
let loop_cond = SsaVariable::new(VariableOrigin::Stack(2), 0, DefSite::instruction(1, 0));
let loop_cond_id = loop_cond.id();
ssa.add_variable(loop_cond);
let sum_next = SsaVariable::new(VariableOrigin::Stack(3), 0, DefSite::instruction(2, 0));
let sum_next_id = sum_next.id();
ssa.add_variable(sum_next);
let hundred = SsaVariable::new(VariableOrigin::Stack(4), 0, DefSite::instruction(2, 1));
let hundred_id = hundred.id();
ssa.add_variable(hundred);
let break_cond = SsaVariable::new(VariableOrigin::Stack(5), 0, DefSite::instruction(2, 2));
let break_cond_id = break_cond.id();
ssa.add_variable(break_cond);
let one = SsaVariable::new(VariableOrigin::Stack(6), 0, DefSite::instruction(3, 0));
let one_id = one.id();
ssa.add_variable(one);
let i_next = SsaVariable::new(VariableOrigin::Stack(7), 0, DefSite::instruction(3, 1));
let i_next_id = i_next.id();
ssa.add_variable(i_next);
let mut block0 = SsaBlock::new(0);
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: sum_init_id,
value: ConstValue::I32(0),
}));
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: i_init_id,
value: ConstValue::I32(0),
}));
block0.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block0);
let mut block1 = SsaBlock::new(1);
let mut phi_sum = PhiNode::new(sum_phi_id, VariableOrigin::Local(0));
phi_sum.add_operand(PhiOperand::new(sum_init_id, 0));
phi_sum.add_operand(PhiOperand::new(sum_next_id, 3));
block1.add_phi(phi_sum);
let mut phi_i = PhiNode::new(i_phi_id, VariableOrigin::Local(1));
phi_i.add_operand(PhiOperand::new(i_init_id, 0));
phi_i.add_operand(PhiOperand::new(i_next_id, 3));
block1.add_phi(phi_i);
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Clt {
dest: loop_cond_id,
left: i_phi_id,
right: n_id,
unsigned: false,
}));
block1.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: loop_cond_id,
true_target: 2,
false_target: 4,
}));
ssa.add_block(block1);
let mut block2 = SsaBlock::new(2);
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: sum_next_id,
left: sum_phi_id,
right: i_phi_id,
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: hundred_id,
value: ConstValue::I32(100),
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Cgt {
dest: break_cond_id,
left: sum_next_id,
right: hundred_id,
unsigned: false,
}));
block2.add_instruction(SsaInstruction::synthetic(SsaOp::Branch {
condition: break_cond_id,
true_target: 4, false_target: 3,
}));
ssa.add_block(block2);
let mut block3 = SsaBlock::new(3);
block3.add_instruction(SsaInstruction::synthetic(SsaOp::Const {
dest: one_id,
value: ConstValue::I32(1),
}));
block3.add_instruction(SsaInstruction::synthetic(SsaOp::Add {
dest: i_next_id,
left: i_phi_id,
right: one_id,
}));
block3.add_instruction(SsaInstruction::synthetic(SsaOp::Jump { target: 1 }));
ssa.add_block(block3);
let mut block4 = SsaBlock::new(4);
block4.add_instruction(SsaInstruction::synthetic(SsaOp::Return {
value: Some(sum_phi_id),
}));
ssa.add_block(block4);
let mut gen = SsaCodeGenerator::new();
let result = gen.generate(&ssa);
assert!(result.is_ok(), "Codegen failed: {:?}", result.err());
let (bytecode, _, _) = result.unwrap();
let instructions = decode_stream(&mut Parser::new(&bytecode), 0x1000).expect("decode");
let mnemonics: Vec<&str> = instructions.iter().map(|i| i.mnemonic).collect();
assert!(
mnemonics.contains(&"clt"),
"Should have clt for loop condition"
);
assert!(
mnemonics.contains(&"cgt"),
"Should have cgt for break condition"
);
let branch_count = mnemonics.iter().filter(|m| m.starts_with("br")).count();
assert!(
branch_count >= 2,
"Should have multiple branches for loop and break"
);
}