use synth_backend::ArmEncoder;
use synth_synthesis::{ArmOp, InstructionSelector, RuleDatabase, WasmOp};
fn encoded_size(ops: &[ArmOp]) -> usize {
let enc = ArmEncoder::new_thumb2();
ops.iter()
.map(|op| enc.encode(op).expect("encoding failed").len())
.sum()
}
fn compile_body(wasm_ops: &[WasmOp], num_params: u32) -> Vec<ArmOp> {
let db = RuleDatabase::with_standard_rules();
let mut selector = InstructionSelector::new(db.rules().to_vec());
let instrs = selector
.select_with_stack(wasm_ops, num_params)
.expect("selection failed");
instrs
.into_iter()
.map(|i| i.op)
.filter(|op| {
!matches!(
op,
ArmOp::Push { .. }
| ArmOp::Pop { .. }
| ArmOp::Sub {
rd: synth_synthesis::Reg::SP,
..
}
| ArmOp::Add {
rd: synth_synthesis::Reg::SP,
..
}
| ArmOp::Bx { .. }
)
})
.collect()
}
#[test]
fn canonical_const_addr_load_drops_from_10_to_4_bytes() {
let wasm = vec![
WasmOp::I32Const(0x100),
WasmOp::I32Load {
offset: 8,
align: 2,
},
WasmOp::End,
];
let body = compile_body(&wasm, 0);
let movw_movt_count = body
.iter()
.filter(|op| matches!(op, ArmOp::Movw { .. } | ArmOp::Movt { .. }))
.count();
assert_eq!(
movw_movt_count, 0,
"issue #95 fold: const-addr load must NOT emit Movw/Movt; got body: {body:#?}"
);
let ldr_count = body
.iter()
.filter(|op| matches!(op, ArmOp::Ldr { .. }))
.count();
assert_eq!(ldr_count, 1, "expected exactly one Ldr; got {ldr_count}");
let only_ldr: Vec<_> = body
.iter()
.filter(|op| matches!(op, ArmOp::Ldr { .. }))
.cloned()
.collect();
assert_eq!(encoded_size(&only_ldr), 4, "folded LDR.W must be 4 bytes");
}
#[test]
fn const_addr_load_falls_back_when_offset_too_large() {
let wasm = vec![
WasmOp::I32Const(0x10000),
WasmOp::I32Load {
offset: 8,
align: 2,
},
WasmOp::End,
];
let body = compile_body(&wasm, 0);
let has_movw = body.iter().any(|op| matches!(op, ArmOp::Movw { .. }));
let has_movt = body.iter().any(|op| matches!(op, ArmOp::Movt { .. }));
assert!(has_movw, "fallback path must emit Movw for 0x10000");
assert!(
has_movt,
"fallback path must emit Movt for 0x10000 (high16=1)"
);
let ldr = body
.iter()
.find_map(|op| match op {
ArmOp::Ldr { addr, .. } => Some(addr.clone()),
_ => None,
})
.expect("fallback must still emit a Ldr");
assert!(
ldr.offset_reg.is_some(),
"fallback Ldr should use register-offset addressing"
);
}
#[test]
fn canonical_const_addr_store_folds() {
let wasm = vec![
WasmOp::I32Const(0x100),
WasmOp::I32Const(42),
WasmOp::I32Store {
offset: 4,
align: 2,
},
WasmOp::End,
];
let body = compile_body(&wasm, 0);
let movw_count = body
.iter()
.filter(|op| matches!(op, ArmOp::Movw { .. }))
.count();
assert_eq!(
movw_count, 1,
"value Movw must remain, address Movw must be folded out; got: {body:#?}"
);
let strs: Vec<_> = body
.iter()
.filter_map(|op| match op {
ArmOp::Str { rd, addr } => Some((rd, addr.clone())),
_ => None,
})
.collect();
assert_eq!(strs.len(), 1);
let (_rd, addr) = &strs[0];
assert_eq!(addr.base, synth_synthesis::Reg::R11);
assert_eq!(addr.offset, 0x104);
assert!(addr.offset_reg.is_none());
}
#[test]
fn canonical_load_before_vs_after_byte_count() {
let enc = ArmEncoder::new_thumb2();
let folded_seq = [ArmOp::Ldr {
rd: synth_synthesis::Reg::R3,
addr: synth_synthesis::MemAddr::imm(synth_synthesis::Reg::R11, 0x108),
}];
let folded_bytes: usize = folded_seq
.iter()
.map(|op| enc.encode(op).expect("encode").len())
.sum();
let unfolded_seq = [
ArmOp::Movw {
rd: synth_synthesis::Reg::R12,
imm16: 0x0100,
},
ArmOp::Movt {
rd: synth_synthesis::Reg::R12,
imm16: 0x0000,
},
ArmOp::Ldr {
rd: synth_synthesis::Reg::R3,
addr: synth_synthesis::MemAddr::reg_imm(
synth_synthesis::Reg::R11,
synth_synthesis::Reg::R12,
8,
),
},
];
let unfolded_bytes: usize = unfolded_seq
.iter()
.map(|op| enc.encode(op).expect("encode").len())
.sum();
assert_eq!(folded_bytes, 4, "folded LDR.W must be 4 bytes");
assert!(
unfolded_bytes >= 10,
"unfolded path should be at least 10 bytes; got {unfolded_bytes}"
);
assert!(
folded_bytes < unfolded_bytes,
"fold must reduce byte count: {folded_bytes} < {unfolded_bytes}"
);
let body = compile_body(
&[
WasmOp::I32Const(0x100),
WasmOp::I32Load {
offset: 8,
align: 2,
},
WasmOp::End,
],
0,
);
let selector_bytes: usize = body
.iter()
.map(|op| enc.encode(op).expect("encode").len())
.sum();
assert_eq!(
selector_bytes, 4,
"selector must emit 4 bytes for the folded canonical sequence"
);
}
#[test]
fn const_addr_subword_loads_fold() {
let wasm = vec![
WasmOp::I32Const(0x10),
WasmOp::I32Load8U {
offset: 0x20,
align: 1,
},
WasmOp::End,
];
let body = compile_body(&wasm, 0);
let movw = body
.iter()
.filter(|o| matches!(o, ArmOp::Movw { .. }))
.count();
assert_eq!(movw, 0, "i32.load8_u with const addr should fold");
let ldrb = body
.iter()
.find_map(|op| match op {
ArmOp::Ldrb { addr, .. } => Some(addr.clone()),
_ => None,
})
.expect("must emit Ldrb");
assert_eq!(ldrb.base, synth_synthesis::Reg::R11);
assert_eq!(ldrb.offset, 0x30);
let wasm = vec![
WasmOp::I32Const(0x40),
WasmOp::I32Load16S {
offset: 2,
align: 2,
},
WasmOp::End,
];
let body = compile_body(&wasm, 0);
assert_eq!(
body.iter()
.filter(|o| matches!(o, ArmOp::Movw { .. }))
.count(),
0
);
let ldrsh = body
.iter()
.find_map(|op| match op {
ArmOp::Ldrsh { addr, .. } => Some(addr.clone()),
_ => None,
})
.expect("must emit Ldrsh");
assert_eq!(ldrsh.base, synth_synthesis::Reg::R11);
assert_eq!(ldrsh.offset, 0x42);
}