synth_backend/arm_encoder.rs
1//! ARM Code Encoder - Converts ARM instructions to binary machine code
2//!
3//! Generates ARM32/Thumb-2 machine code from ARM instruction structures
4
5use synth_core::Result;
6use synth_core::target::FPUPrecision;
7use synth_synthesis::contracts::encoding as encoding_contracts;
8use synth_synthesis::{ArmOp, MemAddr, MveSize, Operand2, QReg, Reg, VfpReg};
9
10/// ARM instruction encoding
11pub struct ArmEncoder {
12 /// Use Thumb mode (vs ARM mode)
13 thumb_mode: bool,
14 /// FPU capability for VFP instruction encoding
15 #[allow(dead_code)]
16 fpu: Option<FPUPrecision>,
17}
18
19impl ArmEncoder {
20 /// Create a new ARM encoder in ARM32 mode
21 pub fn new_arm32() -> Self {
22 Self {
23 thumb_mode: false,
24 fpu: None,
25 }
26 }
27
28 /// Create a new ARM encoder in Thumb-2 mode
29 pub fn new_thumb2() -> Self {
30 Self {
31 thumb_mode: true,
32 fpu: None,
33 }
34 }
35
36 /// Create a new Thumb-2 encoder with FPU capability
37 pub fn new_thumb2_with_fpu(fpu: Option<FPUPrecision>) -> Self {
38 Self {
39 thumb_mode: true,
40 fpu,
41 }
42 }
43
44 /// Encode a single ARM instruction to bytes
45 pub fn encode(&self, op: &ArmOp) -> Result<Vec<u8>> {
46 if self.thumb_mode {
47 self.encode_thumb(op)
48 } else {
49 self.encode_arm(op)
50 }
51 }
52
53 /// Encode an ARM instruction in ARM32 mode (32-bit instructions)
54 /// #206 / RQ-63-ARMI64OFF (#1165): encode an ARM32 (A32) word/sub-word
55 /// load/store whose effective base must be computed into IP — because the
56 /// address carries a register offset (`[rn, rm{, #off}]`, #206) and/or
57 /// because its static offset exceeds the form's immediate field (imm12 for
58 /// LDR/STR/LDRB/STRB, imm8 for LDRH/STRH/LDRSB/LDRSH; #1165 — the
59 /// immediate arms used to MASK such an offset to a wrong address). Returns
60 /// `None` for a plain in-range immediate access (the caller falls through
61 /// to the immediate-form arms, byte-identical). Otherwise
62 /// `a32_effective_base` materializes `ip` and the op is re-encoded against
63 /// `[ip, #residual]`, which is uniform for word/byte/halfword/signed forms.
64 /// IP (R12) is the scratch register the selector already treats as
65 /// clobberable across memory ops.
66 fn encode_arm_reg_offset_mem(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
67 let (addr, imm_max) = match op {
68 ArmOp::Ldr { addr, .. }
69 | ArmOp::Str { addr, .. }
70 | ArmOp::Ldrb { addr, .. }
71 | ArmOp::Strb { addr, .. } => (addr, A32_LDST_IMM12_MAX),
72 ArmOp::Ldrh { addr, .. }
73 | ArmOp::Strh { addr, .. }
74 | ArmOp::Ldrsb { addr, .. }
75 | ArmOp::Ldrsh { addr, .. } => (addr, A32_LDST_IMM8_MAX),
76 _ => return Ok(None),
77 };
78 if addr.offset_reg.is_none() && (addr.offset as u32) <= imm_max {
79 return Ok(None);
80 }
81 let mut bytes = Vec::new();
82 let (base, residual) = a32_effective_base(&mut bytes, addr, imm_max)?;
83 // Re-encode the op against [base, #residual] (immediate form, in range
84 // by construction → this recursion hits the immediate arms, not this
85 // helper again).
86 let imm_addr = MemAddr::imm(base, residual as i32);
87 let imm_op = match op {
88 ArmOp::Ldr { rd, .. } => ArmOp::Ldr {
89 rd: *rd,
90 addr: imm_addr,
91 },
92 ArmOp::Str { rd, .. } => ArmOp::Str {
93 rd: *rd,
94 addr: imm_addr,
95 },
96 ArmOp::Ldrb { rd, .. } => ArmOp::Ldrb {
97 rd: *rd,
98 addr: imm_addr,
99 },
100 ArmOp::Strb { rd, .. } => ArmOp::Strb {
101 rd: *rd,
102 addr: imm_addr,
103 },
104 ArmOp::Ldrh { rd, .. } => ArmOp::Ldrh {
105 rd: *rd,
106 addr: imm_addr,
107 },
108 ArmOp::Strh { rd, .. } => ArmOp::Strh {
109 rd: *rd,
110 addr: imm_addr,
111 },
112 ArmOp::Ldrsb { rd, .. } => ArmOp::Ldrsb {
113 rd: *rd,
114 addr: imm_addr,
115 },
116 ArmOp::Ldrsh { rd, .. } => ArmOp::Ldrsh {
117 rd: *rd,
118 addr: imm_addr,
119 },
120 _ => unreachable!(),
121 };
122 bytes.extend(self.encode_arm(&imm_op)?);
123 Ok(Some(bytes))
124 }
125
126 /// #594: A32 expansion of `ArmOp::CallIndirect` — mirror of the Thumb-2
127 /// arm (same contract: R11 holds the function-pointer table base, entry
128 /// `i` is a 4-byte code address, R12 is the encoder-scratch register):
129 ///
130 /// ```text
131 /// MOVW r12, #size ; #642: table size (compile-time immediate)
132 /// [MOVT r12, #size>>16] ; only when size exceeds 16 bits
133 /// CMP idx, r12 ; bounds guard: index >= size must TRAP
134 /// BLO +1 insn ; skip the trap when in bounds
135 /// UDF ; WASM Core §4.4.8 out-of-bounds trap
136 /// MOV r12, idx, LSL #2 ; table byte offset
137 /// LDR r12, [r11, r12] ; load function pointer
138 /// BLX r12 ; indirect call
139 /// ```
140 ///
141 /// #650, `table_byte_offset != 0` (a non-zero table of the contiguous
142 /// R11 region): the pointer load becomes
143 /// `ADD r12, r11, r12; LDR r12, [r12, #offset]` — offset 0 keeps the
144 /// single-load form (single-table modules byte-identical by
145 /// construction).
146 ///
147 /// #664, `null_check` (the table has null slots, linked as ZERO words
148 /// per the layout contract): `CMP r12, #0; BNE +1; UDF` between the
149 /// pointer load and the `BLX` — a call reaching an uninitialized slot
150 /// traps (§4.4.8). `false` keeps the expansion byte-identical.
151 ///
152 /// #676, `type_check` (heterogeneous table): the §4.4.8 type check is
153 /// discharged at RUNTIME against the type-id sidecar — after the bounds
154 /// guard, `MOV r12, idx, LSL #2; ADD r12, r11, r12;
155 /// LDR r12, [r12, #type_off]; CMP r12, #expected_id; BEQ +1; UDF`
156 /// (mirror of the Thumb-2 arm; the dispatch tail recomputes `idx*4`).
157 /// Null slots carry the reserved class id 0, subsuming the #664 null
158 /// trap. `None` (every homogeneous table — the verdict discharged at
159 /// COMPILE time by the closed-world verification, see the #642 selector
160 /// guard) emits nothing and keeps the expansion byte-identical.
161 fn encode_arm_call_indirect(
162 table_index_reg: &Reg,
163 table_size: u32,
164 table_byte_offset: u32,
165 null_check: bool,
166 type_check: Option<(u32, u32)>,
167 ) -> Vec<u8> {
168 let idx = reg_to_bits(table_index_reg);
169 let mut bytes = Vec::with_capacity(32);
170 // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
171 let size_lo = table_size & 0xFFFF;
172 let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
173 bytes.extend_from_slice(&movw.to_le_bytes());
174 // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
175 let size_hi = table_size >> 16;
176 if size_hi != 0 {
177 let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
178 bytes.extend_from_slice(&movt.to_le_bytes());
179 }
180 // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
181 let cmp: u32 = 0xE150_000C | (idx << 16);
182 bytes.extend_from_slice(&cmp.to_le_bytes());
183 // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
184 // imm24=0: target = branch + 8.
185 bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
186 // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
187 // guards): call_indirect out-of-bounds trap.
188 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
189 // #676: runtime type check for a heterogeneous table — load the
190 // indexed slot's structural class id from the type-id sidecar and
191 // trap on mismatch (§4.4.8). Mirror of the Thumb-2 arm; `None`
192 // emits nothing (homogeneous tables byte-identical by construction).
193 if let Some((expected_id, type_off)) = type_check {
194 // RQ-61-IMMRANGE (#1072): these `debug_assert`s are compiled out
195 // in release, where the `& 0xFF` / `& 0xFFF` masks below would
196 // silently TRUNCATE an out-of-range value (id 256 compares as 0,
197 // letting a NULL slot pass the §4.4.8 check). The enforcement
198 // claim is DEMONSTRATED, not assumed: the sole `Some` producer is
199 // `resolve_runtime_type_check` (instruction_selector.rs), which
200 // loud-declines id > 255 and offset > 4095, and
201 // `test_676_call_indirect_runtime_check_range_declines` trips
202 // both declines (mutation-checked: disabling either turns it red).
203 debug_assert!(expected_id <= 255, "selector enforces the CMP imm8 range");
204 debug_assert!(type_off <= 4095, "selector enforces the LDR imm12 range");
205 // MOV r12, idx, LSL #2 (same as the dispatch tail's scale).
206 bytes.extend_from_slice(&(0xE1A0C000u32 | (2 << 7) | idx).to_le_bytes());
207 // ADD r12, r11, r12 — data-processing ADD (register).
208 bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
209 // LDR r12, [r12, #type_off] — immediate offset, P=1 U=1 L=1.
210 bytes.extend_from_slice(&(0xE59CC000u32 | (type_off & 0xFFF)).to_le_bytes());
211 // CMP r12, #expected_id — data-processing CMP (immediate).
212 bytes.extend_from_slice(&(0xE35C_0000u32 | (expected_id & 0xFF)).to_le_bytes());
213 // BEQ +1 insn (skip the UDF when the class id matches) —
214 // cond=EQ(0000), imm24=0: target = branch + 8.
215 bytes.extend_from_slice(&0x0A00_0000u32.to_le_bytes());
216 // UDF — the §4.4.8 type-mismatch trap.
217 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
218 }
219 // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
220 // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
221 let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
222 bytes.extend_from_slice(&mov.to_le_bytes());
223 if table_byte_offset == 0 {
224 // Table 0 (base = R11 itself): the pre-#650 single-load form.
225 // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
226 let ldr: u32 = 0xE79BC00C;
227 bytes.extend_from_slice(&ldr.to_le_bytes());
228 } else {
229 // #650: fold the table's compile-time base offset into the
230 // pointer load via the LDR imm12 form.
231 assert!(
232 table_byte_offset <= 4095,
233 "call_indirect table base offset {table_byte_offset} exceeds \
234 LDR imm12 — the selector must have declined this (#650)"
235 );
236 // ADD r12, r11, r12 — data-processing ADD (register).
237 bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
238 // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
239 let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
240 bytes.extend_from_slice(&ldr.to_le_bytes());
241 }
242 // #664: null-slot trap — only when the table image has null slots
243 // (zero-linked words). A fully-initialized table keeps the pre-#664
244 // bytes identical by construction.
245 if null_check {
246 // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
247 bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
248 // BNE +1 insn (skip the UDF when the pointer is non-null) —
249 // cond=NE(0001), imm24=0: target = branch + 8.
250 bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
251 // UDF — the §4.4.8 uninitialized-element trap (same idiom as
252 // the bounds guard).
253 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
254 }
255 // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
256 let blx: u32 = 0xE12FFF3C;
257 bytes.extend_from_slice(&blx.to_le_bytes());
258 bytes
259 }
260
261 /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
262 /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
263 /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
264 /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
265 /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
266 /// register contract and semantics exactly (A32 conditional execution
267 /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
268 /// not handle; the caller's match encodes or loudly rejects those.
269 fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
270 use synth_synthesis::Condition;
271
272 /// A32 condition-field bits (instruction bits [31:28]).
273 fn cond_bits(cond: &Condition) -> u32 {
274 match cond {
275 Condition::EQ => 0x0,
276 Condition::NE => 0x1,
277 Condition::HS => 0x2, // CS: unsigned >=
278 Condition::LO => 0x3, // CC: unsigned <
279 Condition::HI => 0x8, // unsigned >
280 Condition::LS => 0x9, // unsigned <=
281 Condition::GE => 0xA,
282 Condition::LT => 0xB,
283 Condition::GT => 0xC,
284 Condition::LE => 0xD,
285 }
286 }
287 fn w(b: &mut Vec<u8>, word: u32) {
288 b.extend_from_slice(&word.to_le_bytes());
289 }
290 /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
291 fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
292 w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
293 }
294 /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
295 fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
296 mov_cond_imm(b, cond_bits(cond), rd, 1);
297 mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
298 }
299 /// CMP rn, rm (register form).
300 fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
301 w(b, 0xE150_0000 | (rn << 16) | rm);
302 }
303 /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
304 fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
305 w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
306 }
307 /// MOVW rd, #imm16.
308 fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
309 w(
310 b,
311 0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
312 );
313 }
314 /// MOVT rd, #imm16.
315 fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
316 w(
317 b,
318 0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
319 );
320 }
321 /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
322 /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
323 /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
324 /// semantics the Thumb-2 expansions rely on.
325 fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
326 w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
327 }
328 const LSL: u32 = 0;
329 const LSR: u32 = 1;
330 const ASR: u32 = 2;
331 /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
332 fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
333 w(
334 b,
335 0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
336 );
337 }
338 /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
339 /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
340 fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
341 w(b, base | (rn << 16) | (rd << 12) | rm);
342 }
343 /// Data-processing with an immediate-shifted register operand:
344 /// `<op> rd, rn, rm, <LSL|LSR|ASR> #imm` — the A32 barrel shifter
345 /// folds a shift into the second operand for free. #1021 uses this to
346 /// run the popcnt SWAR fold on R12 alone (no second scratch, so R11 —
347 /// the linear-memory base — is never touched).
348 fn dp_reg_shift(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32, ty: u32, imm: u32) {
349 w(
350 b,
351 base | (rn << 16) | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rm,
352 );
353 }
354 /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
355 /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
356 fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
357 w(
358 b,
359 0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
360 );
361 }
362 /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
363 fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
364 w(b, 0xE1E0_0000 | (lo << 12) | lo); // MVN lo, lo
365 w(b, 0xE1E0_0000 | (hi << 12) | hi); // MVN hi, hi
366 w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); // ADDS lo, lo, #1
367 w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); // ADC hi, hi, #0
368 }
369 /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
370 /// sign bit is clear" guard of the signed div/rem arms.
371 fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
372 w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
373 w(b, 0x5A00_0003); // BPL +4 insns (past negate64)
374 }
375 /// The 64-iteration shift-subtract division loop — A32 transcription
376 /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
377 /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
378 fn div_loop(b: &mut Vec<u8>, counter: u32) {
379 w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
380 let loop_start = b.len();
381 // quotient <<= 1
382 shift_imm(b, LSL, 5, 5, 1);
383 orr_lsr31(b, 5, 4);
384 shift_imm(b, LSL, 4, 4, 1);
385 // remainder <<= 1, OR in dividend MSB
386 shift_imm(b, LSL, 7, 7, 1);
387 orr_lsr31(b, 7, 6);
388 shift_imm(b, LSL, 6, 6, 1);
389 orr_lsr31(b, 6, 1);
390 // dividend <<= 1
391 shift_imm(b, LSL, 1, 1, 1);
392 orr_lsr31(b, 1, 0);
393 shift_imm(b, LSL, 0, 0, 1);
394 // if remainder >= divisor (64-bit unsigned): subtract, set q bit
395 w(b, 0xE157_0003); // CMP R7, R3 (high words)
396 w(b, 0x8A00_0002); // BHI .subtract (+2 insns)
397 w(b, 0x3A00_0004); // BLO .next (+4 insns)
398 w(b, 0xE156_0002); // CMP R6, R2 (low words, highs equal)
399 w(b, 0x3A00_0002); // BLO .next (+2 insns)
400 w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
401 w(b, 0xE0C7_7003); // SBC R7, R7, R3
402 w(b, 0xE384_4001); // ORR R4, R4, #1
403 // .next: decrement and loop
404 w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
405 let diff = (loop_start as i64) - (b.len() as i64 + 8);
406 w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
407 }
408 /// 32-bit population count on working register `x` — A32 transcription
409 /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
410 /// constant register, R12 the shifted temp. Both are clobbered.
411 fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
412 // x = x - ((x >> 1) & 0x55555555)
413 shift_imm(b, LSR, 12, x, 1);
414 movw(b, c, 0x5555);
415 movt(b, c, 0x5555);
416 dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
417 dp_reg(b, 0xE040_0000, x, x, 12); // SUB x, x, R12
418 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
419 movw(b, c, 0x3333);
420 movt(b, c, 0x3333);
421 dp_reg(b, 0xE000_0000, 12, x, c); // AND R12, x, c
422 shift_imm(b, LSR, x, x, 2);
423 dp_reg(b, 0xE000_0000, x, x, c); // AND x, x, c
424 dp_reg(b, 0xE080_0000, x, x, 12); // ADD x, x, R12
425 // x = (x + (x >> 4)) & 0x0F0F0F0F
426 shift_imm(b, LSR, 12, x, 4);
427 dp_reg(b, 0xE080_0000, x, x, 12); // ADD x, x, R12
428 movw(b, c, 0x0F0F);
429 movt(b, c, 0x0F0F);
430 dp_reg(b, 0xE000_0000, x, x, c); // AND x, x, c
431 // x = (x * 0x01010101) >> 24
432 movw(b, c, 0x0101);
433 movt(b, c, 0x0101);
434 w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
435 shift_imm(b, LSR, x, x, 24);
436 }
437
438 let mut b: Vec<u8> = Vec::new();
439 match op {
440 // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
441 // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
442 ArmOp::SetCond { rd, cond } => {
443 set_cond(&mut b, cond, reg_to_bits(rd));
444 }
445
446 // SelectMove: conditional register move (Thumb: IT cond; MOV).
447 ArmOp::SelectMove { rd, rm, cond } => {
448 w(
449 &mut b,
450 (cond_bits(cond) << 28)
451 | 0x01A0_0000
452 | (reg_to_bits(rd) << 12)
453 | reg_to_bits(rm),
454 );
455 }
456
457 // I64SetCond: compare two i64 register pairs, 0/1 into rd.
458 // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
459 // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
460 // operand-swap + condition mapping as the Thumb-2 arm.
461 ArmOp::I64SetCond {
462 rd,
463 rn_lo,
464 rn_hi,
465 rm_lo,
466 rm_hi,
467 cond,
468 } => {
469 let rd_b = reg_to_bits(rd);
470 let (n_lo, n_hi, m_lo, m_hi) = (
471 reg_to_bits(rn_lo),
472 reg_to_bits(rn_hi),
473 reg_to_bits(rm_lo),
474 reg_to_bits(rm_hi),
475 );
476 match cond {
477 Condition::EQ | Condition::NE => {
478 cmp_reg(&mut b, n_lo, m_lo);
479 // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
480 w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
481 set_cond(&mut b, cond, rd_b);
482 }
483 // (swap operands?, condition after SBCS) per the Thumb arm:
484 // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
485 Condition::LT => {
486 cmp_reg(&mut b, n_lo, m_lo);
487 sbcs(&mut b, rd_b, n_hi, m_hi);
488 set_cond(&mut b, &Condition::LT, rd_b);
489 }
490 Condition::GE => {
491 cmp_reg(&mut b, n_lo, m_lo);
492 sbcs(&mut b, rd_b, n_hi, m_hi);
493 set_cond(&mut b, &Condition::GE, rd_b);
494 }
495 Condition::GT => {
496 cmp_reg(&mut b, m_lo, n_lo);
497 sbcs(&mut b, rd_b, m_hi, n_hi);
498 set_cond(&mut b, &Condition::LT, rd_b);
499 }
500 Condition::LE => {
501 cmp_reg(&mut b, m_lo, n_lo);
502 sbcs(&mut b, rd_b, m_hi, n_hi);
503 set_cond(&mut b, &Condition::GE, rd_b);
504 }
505 Condition::LO => {
506 cmp_reg(&mut b, n_lo, m_lo);
507 sbcs(&mut b, rd_b, n_hi, m_hi);
508 set_cond(&mut b, &Condition::LO, rd_b);
509 }
510 Condition::HS => {
511 cmp_reg(&mut b, n_lo, m_lo);
512 sbcs(&mut b, rd_b, n_hi, m_hi);
513 set_cond(&mut b, &Condition::HS, rd_b);
514 }
515 Condition::HI => {
516 cmp_reg(&mut b, m_lo, n_lo);
517 sbcs(&mut b, rd_b, m_hi, n_hi);
518 set_cond(&mut b, &Condition::LO, rd_b);
519 }
520 Condition::LS => {
521 cmp_reg(&mut b, m_lo, n_lo);
522 sbcs(&mut b, rd_b, m_hi, n_hi);
523 set_cond(&mut b, &Condition::HS, rd_b);
524 }
525 }
526 }
527
528 // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
529 ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
530 let rd_b = reg_to_bits(rd);
531 w(
532 &mut b,
533 0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
534 );
535 set_cond(&mut b, &Condition::EQ, rd_b);
536 }
537
538 // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
539 // Thumb-2 delegation arms.
540 ArmOp::I64Eqz { rd, rnlo, rnhi } => {
541 return self
542 .encode_arm(&ArmOp::I64SetCondZ {
543 rd: *rd,
544 rn_lo: *rnlo,
545 rn_hi: *rnhi,
546 })
547 .map(Some);
548 }
549 ArmOp::I64Eq {
550 rd,
551 rnlo,
552 rnhi,
553 rmlo,
554 rmhi,
555 }
556 | ArmOp::I64Ne {
557 rd,
558 rnlo,
559 rnhi,
560 rmlo,
561 rmhi,
562 }
563 | ArmOp::I64LtS {
564 rd,
565 rnlo,
566 rnhi,
567 rmlo,
568 rmhi,
569 }
570 | ArmOp::I64LtU {
571 rd,
572 rnlo,
573 rnhi,
574 rmlo,
575 rmhi,
576 }
577 | ArmOp::I64LeS {
578 rd,
579 rnlo,
580 rnhi,
581 rmlo,
582 rmhi,
583 }
584 | ArmOp::I64LeU {
585 rd,
586 rnlo,
587 rnhi,
588 rmlo,
589 rmhi,
590 }
591 | ArmOp::I64GtS {
592 rd,
593 rnlo,
594 rnhi,
595 rmlo,
596 rmhi,
597 }
598 | ArmOp::I64GtU {
599 rd,
600 rnlo,
601 rnhi,
602 rmlo,
603 rmhi,
604 }
605 | ArmOp::I64GeS {
606 rd,
607 rnlo,
608 rnhi,
609 rmlo,
610 rmhi,
611 }
612 | ArmOp::I64GeU {
613 rd,
614 rnlo,
615 rnhi,
616 rmlo,
617 rmhi,
618 } => {
619 let cond = match op {
620 ArmOp::I64Eq { .. } => Condition::EQ,
621 ArmOp::I64Ne { .. } => Condition::NE,
622 ArmOp::I64LtS { .. } => Condition::LT,
623 ArmOp::I64LtU { .. } => Condition::LO,
624 ArmOp::I64LeS { .. } => Condition::LE,
625 ArmOp::I64LeU { .. } => Condition::LS,
626 ArmOp::I64GtS { .. } => Condition::GT,
627 ArmOp::I64GtU { .. } => Condition::HI,
628 ArmOp::I64GeS { .. } => Condition::GE,
629 _ => Condition::HS,
630 };
631 return self
632 .encode_arm(&ArmOp::I64SetCond {
633 rd: *rd,
634 rn_lo: *rnlo,
635 rn_hi: *rnhi,
636 rm_lo: *rmlo,
637 rm_hi: *rmhi,
638 cond,
639 })
640 .map(Some);
641 }
642
643 // I64Mul: cross products into R12, then UMULL — same sequence and
644 // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
645 ArmOp::I64Mul {
646 rd_lo,
647 rd_hi,
648 rn_lo,
649 rn_hi,
650 rm_lo,
651 rm_hi,
652 } => {
653 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
654 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
655 let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
656 // MUL R12, rn_lo, rm_hi (R12 = a_lo * b_hi)
657 w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
658 // MLA R12, rn_hi, rm_lo, R12 (R12 += a_hi * b_lo)
659 w(
660 &mut b,
661 0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
662 );
663 // UMULL rd_lo, rd_hi, rn_lo, rm_lo
664 w(
665 &mut b,
666 0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
667 );
668 // ADD rd_hi, rd_hi, R12
669 w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
670 }
671
672 // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
673 // the Thumb-2 arms. #1048: the expansion must never write its own
674 // input operands — the pre-#1048 A32 arms masked the amount in
675 // place (`AND ml, ml, #63`) and used the amount's home high
676 // register as scratch, identically to the Thumb-2 defect. R12
677 // (encoder scratch, never allocatable, #212) is the ONLY
678 // temporary; the masked amount is re-derived from the untouched
679 // rm_lo where a second live temp would otherwise be needed.
680 // Register-controlled shifts >= 32 yield 0, which the small path
681 // relies on for n = 0. Same #1039-style loud alias guards as the
682 // Thumb-2 arms.
683 ArmOp::I64Shl {
684 rd_lo,
685 rd_hi,
686 rn_lo,
687 rn_hi,
688 rm_lo,
689 rm_hi: _,
690 } => {
691 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
692 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
693 let ml = reg_to_bits(rm_lo);
694 if dh == nl || dh == ml {
695 return Err(synth_core::Error::synthesis(format!(
696 "I64Shl (A32): rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
697 )));
698 }
699 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
700 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
701 w(&mut b, 0x5A00_0007); // BPL .large
702 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
703 shift_reg(&mut b, LSL, dh, nh, 12); // dh = hi << n
704 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
705 shift_reg(&mut b, LSR, 12, nl, 12); // r12 = lo >> (32-n)
706 w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | 12); // ORR dh, dh, r12
707 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
708 shift_reg(&mut b, LSL, dl, nl, 12); // dl = lo << n
709 w(&mut b, 0xEA00_0001); // B .done
710 shift_reg(&mut b, LSL, dh, nl, 12); // .large: dh = lo << (n-32)
711 w(&mut b, 0xE3A0_0000 | (dl << 12)); // MOV dl, #0
712 }
713 ArmOp::I64ShrU {
714 rd_lo,
715 rd_hi,
716 rn_lo,
717 rn_hi,
718 rm_lo,
719 rm_hi: _,
720 } => {
721 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
722 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
723 let ml = reg_to_bits(rm_lo);
724 if dl == nh || dl == ml {
725 return Err(synth_core::Error::synthesis(format!(
726 "I64ShrU (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
727 )));
728 }
729 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
730 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
731 w(&mut b, 0x5A00_0007); // BPL .large
732 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
733 shift_reg(&mut b, LSR, dl, nl, 12); // dl = lo >> n
734 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
735 shift_reg(&mut b, LSL, 12, nh, 12); // r12 = hi << (32-n)
736 w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
737 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
738 shift_reg(&mut b, LSR, dh, nh, 12); // dh = hi >> n
739 w(&mut b, 0xEA00_0001); // B .done
740 shift_reg(&mut b, LSR, dl, nh, 12); // .large: dl = hi >> (n-32)
741 w(&mut b, 0xE3A0_0000 | (dh << 12)); // MOV dh, #0
742 }
743 ArmOp::I64ShrS {
744 rd_lo,
745 rd_hi,
746 rn_lo,
747 rn_hi,
748 rm_lo,
749 rm_hi: _,
750 } => {
751 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
752 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
753 let ml = reg_to_bits(rm_lo);
754 if dl == nh || dl == ml {
755 return Err(synth_core::Error::synthesis(format!(
756 "I64ShrS (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
757 )));
758 }
759 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
760 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
761 w(&mut b, 0x5A00_0007); // BPL .large
762 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
763 shift_reg(&mut b, LSR, dl, nl, 12); // dl = lo >> n
764 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
765 shift_reg(&mut b, LSL, 12, nh, 12); // r12 = hi << (32-n)
766 w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
767 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
768 shift_reg(&mut b, ASR, dh, nh, 12); // dh = hi >> n (arith)
769 w(&mut b, 0xEA00_0001); // B .done
770 shift_reg(&mut b, ASR, dl, nh, 12); // .large: dl = hi >> (n-32)
771 w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
772 }
773
774 // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
775 // the same fixed-register core as the Thumb-2 arms — value in
776 // R0:R1, amount in R2, scratch R3 + R12.
777 ArmOp::I64Rotl {
778 rdlo,
779 rdhi,
780 rnlo,
781 rnhi,
782 shift,
783 } => {
784 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
785 for word in [
786 0xE202_203Fu32, // AND R2, R2, #63 (mask amount mod 64)
787 0xE252_3020, // SUBS R3, R2, #32 (R3 = n-32, sets N)
788 0x5A00_0007, // BPL .large (n >= 32)
789 // --- small rotation (n < 32) ---
790 0xE262_3020, // RSB R3, R2, #32 (R3 = 32-n)
791 0xE1A0_C330, // LSR R12, R0, R3 (lo >> (32-n))
792 0xE1A0_3331, // LSR R3, R1, R3 (hi >> (32-n))
793 0xE1A0_1211, // LSL R1, R1, R2 (hi << n)
794 0xE181_100C, // ORR R1, R1, R12 (new_hi)
795 0xE1A0_0210, // LSL R0, R0, R2 (lo << n)
796 0xE180_0003, // ORR R0, R0, R3 (new_lo)
797 0xEA00_0007, // B .done
798 // --- large rotation (n >= 32), R3 = m = n-32 ---
799 0xE263_2020, // RSB R2, R3, #32 (R2 = 32-m = 64-n)
800 0xE1A0_C231, // LSR R12, R1, R2 (hi >> (64-n))
801 0xE1A0_2230, // LSR R2, R0, R2 (lo >> (64-n))
802 0xE1A0_0310, // LSL R0, R0, R3 (lo << m)
803 0xE1A0_1311, // LSL R1, R1, R3 (hi << m)
804 0xE180_C00C, // ORR R12, R0, R12 (new_hi = (lo<<m)|(hi>>(64-n)))
805 0xE181_0002, // ORR R0, R1, R2 (new_lo = (hi<<m)|(lo>>(64-n)))
806 0xE1A0_100C, // MOV R1, R12 (new_hi into place)
807 ] {
808 w(&mut b, word);
809 }
810 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
811 }
812 ArmOp::I64Rotr {
813 rdlo,
814 rdhi,
815 rnlo,
816 rnhi,
817 shift,
818 } => {
819 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
820 for word in [
821 0xE202_203Fu32, // AND R2, R2, #63 (mask amount mod 64)
822 0xE252_3020, // SUBS R3, R2, #32 (R3 = n-32, sets N)
823 0x5A00_0007, // BPL .large (n >= 32)
824 // --- small rotation (n < 32) ---
825 0xE262_3020, // RSB R3, R2, #32 (R3 = 32-n)
826 0xE1A0_C311, // LSL R12, R1, R3 (hi << (32-n))
827 0xE1A0_3310, // LSL R3, R0, R3 (lo << (32-n))
828 0xE1A0_0230, // LSR R0, R0, R2 (lo >> n)
829 0xE180_000C, // ORR R0, R0, R12 (new_lo)
830 0xE1A0_1231, // LSR R1, R1, R2 (hi >> n)
831 0xE181_1003, // ORR R1, R1, R3 (new_hi)
832 0xEA00_0007, // B .done
833 // --- large rotation (n >= 32), R3 = m = n-32 ---
834 0xE263_2020, // RSB R2, R3, #32 (R2 = 32-m = 64-n)
835 0xE1A0_C210, // LSL R12, R0, R2 (lo << (64-n))
836 0xE1A0_2211, // LSL R2, R1, R2 (hi << (64-n))
837 0xE1A0_1331, // LSR R1, R1, R3 (hi >> m)
838 0xE181_C00C, // ORR R12, R1, R12 (new_lo = (hi>>m)|(lo<<(64-n)))
839 0xE1A0_1330, // LSR R1, R0, R3 (lo >> m)
840 0xE181_1002, // ORR R1, R1, R2 (new_hi = (lo>>m)|(hi<<(64-n)))
841 0xE1A0_000C, // MOV R0, R12 (new_lo into place)
842 ] {
843 w(&mut b, word);
844 }
845 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
846 }
847
848 // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
849 // execution replaces the Thumb branches; like the Thumb arm, the
850 // high word of the result pair (rnhi) is cleared last.
851 ArmOp::I64Clz { rd, rnlo, rnhi } => {
852 let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
853 w(&mut b, 0xE350_0000 | (hi << 16)); // CMP rnhi, #0
854 w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); // CLZNE rd, rnhi
855 w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); // CLZEQ rd, rnlo
856 w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
857 // #1048: the former trailing `MOV rnhi, #0` is GONE — it
858 // wrote the OPERAND's home high register (see the Thumb-2
859 // I64Clz comment). Callers that relied on the implicit clear
860 // emit their own explicit hi-zero op.
861 }
862
863 // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
864 // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
865 // conditional ADD.
866 ArmOp::I64Ctz { rd, rnlo, rnhi } => {
867 let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
868 w(&mut b, 0xE350_0000 | (lo << 16)); // CMP rnlo, #0
869 w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); // RBITNE rd, rnlo
870 w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); // RBITEQ rd, rnhi
871 w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); // CLZ rd, rd
872 w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
873 // #1048: no trailing `MOV rnhi, #0` — see I64Clz above.
874 }
875
876 // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
877 // 16 bits, mirroring the Thumb-2 arm).
878 ArmOp::I64Const { rdlo, rdhi, value } => {
879 let lo32 = *value as u32;
880 let hi32 = (*value >> 32) as u32;
881 movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
882 if lo32 > 0xFFFF {
883 movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
884 }
885 movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
886 if hi32 > 0xFFFF {
887 movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
888 }
889 }
890
891 // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
892 // A register offset is materialized into IP once (the #206/#372
893 // hazard: dropping it would read the wrong address).
894 // RQ-63-ARMI64OFF (#1165): an offset the pair form cannot hold
895 // (> 0xFFB, so the high half's +4 leaves imm12) used to be a loud
896 // decline here — 46 of the 110 core-module declines in v0.62's ARM
897 // census, all on the `-b arm` (Arm32) default target. It is now
898 // MATERIALIZED (MOVW/MOVT + ADD, the A32 mirror of #382's Thumb-2
899 // `i64_effective_base`); in-range offsets stay byte-identical.
900 ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
901 let (base, off) = a32_effective_base(&mut b, addr, A32_I64_PAIR_IMM12_MAX)?;
902 let base = reg_to_bits(&base);
903 let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
904 0xE590_0000 // LDR
905 } else {
906 0xE580_0000 // STR
907 };
908 w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
909 w(
910 &mut b,
911 opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
912 );
913 }
914
915 // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
916 ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
917 if rdlo != rn {
918 w(
919 &mut b,
920 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
921 );
922 }
923 w(
924 &mut b,
925 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
926 );
927 }
928
929 // I64ExtendI32U: rdlo = rn; rdhi = 0.
930 ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
931 if rdlo != rn {
932 w(
933 &mut b,
934 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
935 );
936 }
937 w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
938 }
939
940 // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
941 ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
942 w(
943 &mut b,
944 0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
945 );
946 w(
947 &mut b,
948 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
949 );
950 }
951 ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
952 w(
953 &mut b,
954 0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
955 );
956 w(
957 &mut b,
958 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
959 );
960 }
961 ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
962 if rdlo != rnlo {
963 w(
964 &mut b,
965 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
966 );
967 }
968 w(
969 &mut b,
970 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
971 );
972 }
973
974 // I32WrapI64: take the low word. When rd == rnlo this is a genuine
975 // no-op (the one case where a NOP word is the correct encoding).
976 ArmOp::I32WrapI64 { rd, rnlo } => {
977 w(
978 &mut b,
979 0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
980 );
981 }
982
983 // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
984 // The selector emits these as separate Adds/Adc ops; the fused
985 // variants are verification-constructed, but they encode for real.
986 ArmOp::I64Add {
987 rdlo,
988 rdhi,
989 rnlo,
990 rnhi,
991 rmlo,
992 rmhi,
993 } => {
994 dp_reg(
995 &mut b,
996 0xE090_0000, // ADDS
997 reg_to_bits(rdlo),
998 reg_to_bits(rnlo),
999 reg_to_bits(rmlo),
1000 );
1001 dp_reg(
1002 &mut b,
1003 0xE0A0_0000, // ADC
1004 reg_to_bits(rdhi),
1005 reg_to_bits(rnhi),
1006 reg_to_bits(rmhi),
1007 );
1008 }
1009 ArmOp::I64Sub {
1010 rdlo,
1011 rdhi,
1012 rnlo,
1013 rnhi,
1014 rmlo,
1015 rmhi,
1016 } => {
1017 dp_reg(
1018 &mut b,
1019 0xE050_0000, // SUBS
1020 reg_to_bits(rdlo),
1021 reg_to_bits(rnlo),
1022 reg_to_bits(rmlo),
1023 );
1024 dp_reg(
1025 &mut b,
1026 0xE0C0_0000, // SBC
1027 reg_to_bits(rdhi),
1028 reg_to_bits(rnhi),
1029 reg_to_bits(rmhi),
1030 );
1031 }
1032
1033 // I64And / I64Or / I64Xor: two independent word ops.
1034 ArmOp::I64And {
1035 rdlo,
1036 rdhi,
1037 rnlo,
1038 rnhi,
1039 rmlo,
1040 rmhi,
1041 }
1042 | ArmOp::I64Or {
1043 rdlo,
1044 rdhi,
1045 rnlo,
1046 rnhi,
1047 rmlo,
1048 rmhi,
1049 }
1050 | ArmOp::I64Xor {
1051 rdlo,
1052 rdhi,
1053 rnlo,
1054 rnhi,
1055 rmlo,
1056 rmhi,
1057 } => {
1058 let base = match op {
1059 ArmOp::I64And { .. } => 0xE000_0000, // AND
1060 ArmOp::I64Or { .. } => 0xE180_0000, // ORR
1061 _ => 0xE020_0000, // EOR
1062 };
1063 dp_reg(
1064 &mut b,
1065 base,
1066 reg_to_bits(rdlo),
1067 reg_to_bits(rnlo),
1068 reg_to_bits(rmlo),
1069 );
1070 dp_reg(
1071 &mut b,
1072 base,
1073 reg_to_bits(rdhi),
1074 reg_to_bits(rnhi),
1075 reg_to_bits(rmhi),
1076 );
1077 }
1078
1079 // I64DivU: binary long division — A32 transcription of the Thumb-2
1080 // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1081 // shift-subtract core, quotient to R0:R1, result to rd pair).
1082 ArmOp::I64DivU {
1083 rdlo,
1084 rdhi,
1085 rnlo,
1086 rnhi,
1087 rmlo,
1088 rmhi,
1089 elide_zero_guard,
1090 } => {
1091 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1092 // #494 phase 2b: elided only under a certificate-discharged
1093 // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1094 if !elide_zero_guard {
1095 emit_a32_i64_divisor_zero_trap(&mut b);
1096 }
1097 w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1098 for r in 4..8u32 {
1099 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1100 }
1101 div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1102 w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1103 w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1104 w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1105 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1106 }
1107
1108 // I64DivS: sign-extract, unsigned core, conditional negate —
1109 // A32 transcription of the Thumb-2 arm.
1110 ArmOp::I64DivS {
1111 rdlo,
1112 rdhi,
1113 rnlo,
1114 rnhi,
1115 rmlo,
1116 rmhi,
1117 elide_zero_guard,
1118 elide_overflow_guard,
1119 } => {
1120 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1121 // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1122 // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1123 // the #633 overflow guard falls ONLY to
1124 // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1125 // divisor-nonzero fact alone must keep it.
1126 if !elide_zero_guard {
1127 emit_a32_i64_divisor_zero_trap(&mut b);
1128 }
1129 if !elide_overflow_guard {
1130 // #633: INT64_MIN / -1 overflows — trap like the i32 path
1131 // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1132 emit_a32_i64_divs_overflow_trap(&mut b);
1133 }
1134 w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1135 w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1136 skip_negate_if_positive(&mut b, 1);
1137 negate64(&mut b, 0, 1);
1138 skip_negate_if_positive(&mut b, 3);
1139 negate64(&mut b, 2, 3);
1140 for r in 4..8u32 {
1141 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1142 }
1143 div_loop(&mut b, 8); // counter in R8 (saved above)
1144 w(&mut b, 0xE1A0_0004); // MOV R0, R4
1145 w(&mut b, 0xE1A0_1005); // MOV R1, R5
1146 skip_negate_if_positive(&mut b, 9);
1147 negate64(&mut b, 0, 1);
1148 w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1149 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1150 }
1151
1152 // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1153 ArmOp::I64RemU {
1154 rdlo,
1155 rdhi,
1156 rnlo,
1157 rnhi,
1158 rmlo,
1159 rmhi,
1160 elide_zero_guard,
1161 } => {
1162 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1163 if !elide_zero_guard {
1164 emit_a32_i64_divisor_zero_trap(&mut b);
1165 }
1166 w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1167 for r in 4..8u32 {
1168 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1169 }
1170 div_loop(&mut b, 8);
1171 w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1172 w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1173 w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1174 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1175 }
1176
1177 // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1178 ArmOp::I64RemS {
1179 rdlo,
1180 rdhi,
1181 rnlo,
1182 rnhi,
1183 rmlo,
1184 rmhi,
1185 elide_zero_guard,
1186 } => {
1187 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1188 if !elide_zero_guard {
1189 emit_a32_i64_divisor_zero_trap(&mut b);
1190 }
1191 w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1192 w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1193 skip_negate_if_positive(&mut b, 1);
1194 negate64(&mut b, 0, 1);
1195 skip_negate_if_positive(&mut b, 3);
1196 negate64(&mut b, 2, 3);
1197 for r in 4..8u32 {
1198 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1199 }
1200 div_loop(&mut b, 8);
1201 w(&mut b, 0xE1A0_0006); // MOV R0, R6
1202 w(&mut b, 0xE1A0_1007); // MOV R1, R7
1203 skip_negate_if_positive(&mut b, 9);
1204 negate64(&mut b, 0, 1);
1205 w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1206 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1207 }
1208
1209 // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1210 // mirroring the Thumb-2 arm's #1021 register contract: R12 is the
1211 // ONLY scratch. The previous transcription copied the old Thumb
1212 // contract's R11 borrow — but R11 is the linear-memory base on
1213 // this path too, so it inherited the same live miscompile. A32
1214 // has no ThumbExpandImm for 0xXYXYXYXY masks, so instead the
1215 // barrel shifter folds each shift into the mask AND itself
1216 // (`AND R12, R12, rd, LSR #n`), and step 2 recovers `x & C` from
1217 // one term via `x - (((x >> 2) & C) << 2) = x & C` — the second
1218 // temp disappears algebraically. Straight-line, no PUSH/POP,
1219 // nothing to skip on a trap edge.
1220 ArmOp::Popcnt { rd, rm } => {
1221 let rd_b = reg_to_bits(rd);
1222 // Defensive (#1021), same contract as the Thumb-2 arm.
1223 if rd_b >= 11 {
1224 return Err(synth_core::Error::synthesis(
1225 "Popcnt destination must be R0-R10: R11 is the linear-memory \
1226 base and R12 is the expansion's scratch (#1021)",
1227 ));
1228 }
1229 if rd != rm {
1230 w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1231 }
1232 // x = x - ((x >> 1) & 0x55555555)
1233 movw(&mut b, 12, 0x5555);
1234 movt(&mut b, 12, 0x5555);
1235 dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 1); // AND R12, R12, rd, LSR #1
1236 dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 12); // SUB rd, rd, R12
1237 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333), one temp:
1238 // R12 = (x >> 2) & C; x - (R12 << 2) = x & C; then + R12.
1239 movw(&mut b, 12, 0x3333);
1240 movt(&mut b, 12, 0x3333);
1241 dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 2); // AND R12, R12, rd, LSR #2
1242 dp_reg_shift(&mut b, 0xE040_0000, rd_b, rd_b, 12, LSL, 2); // SUB rd, rd, R12, LSL #2
1243 dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 12); // ADD rd, rd, R12
1244 // x = (x + (x >> 4)) & 0x0F0F0F0F
1245 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 4); // ADD rd, rd, rd, LSR #4
1246 movw(&mut b, 12, 0x0F0F);
1247 movt(&mut b, 12, 0x0F0F);
1248 dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1249 // x += x >> 8; x += x >> 16; x &= 0x3F
1250 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 8);
1251 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 16);
1252 w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1253 }
1254
1255 // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1256 // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1257 // result word rnhi cleared last, mirroring the Thumb contract).
1258 ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1259 let hi = reg_to_bits(rnhi);
1260 w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1261 // #632 audit: route rnlo through R12 so a pair living at
1262 // (R3,R4) cannot read a clobbered R4 (sources read before any
1263 // scratch register they could occupy is written).
1264 w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1265 w(&mut b, 0xE1A0_5000 | hi); // MOV R5, rnhi
1266 w(&mut b, 0xE1A0_400C); // MOV R4, R12
1267 popcnt_word(&mut b, 4, 3);
1268 popcnt_word(&mut b, 5, 3);
1269 // #632: carry the count across the scratch restore in R12 —
1270 // rd is allocator-assigned and can land inside {R3,R4,R5};
1271 // the old `ADD rd, R4, R5` before the POP was destroyed by
1272 // the restore. R12 is never allocatable and never restored.
1273 dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1274 w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1275 w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1276 // #1048: no trailing `MOV rnhi, #0` — the hi-word clear wrote
1277 // the OPERAND's home high register; callers emit it explicitly.
1278 }
1279
1280 _ => return Ok(None),
1281 }
1282 Ok(Some(b))
1283 }
1284
1285 fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1286 // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1287 // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1288 // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1289 // so the value silently vanished. Mirror of the #594 CallIndirect
1290 // early-return: if the expansion helper covers the op, its bytes are
1291 // the encoding.
1292 if let Some(bytes) = self.encode_arm_expanded(op)? {
1293 return Ok(bytes);
1294 }
1295 // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1296 // returns the 12-bit immediate, so the immediate-form arms below
1297 // silently DROP `addr.offset_reg` — a runtime address index vanished,
1298 // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1299 // to the wrong address). Compute the effective base into IP and re-encode
1300 // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1301 if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1302 return Ok(bytes);
1303 }
1304 // #594: call_indirect was encoded as a literal NOP on the A32 path
1305 // (`--target cortex-r5`) — the call never happened and the function
1306 // silently returned garbage. Emit the same three-instruction expansion
1307 // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1308 // MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1309 if let ArmOp::CallIndirect {
1310 table_index_reg,
1311 table_size,
1312 table_byte_offset,
1313 null_check,
1314 type_check,
1315 ..
1316 } = op
1317 {
1318 return Ok(Self::encode_arm_call_indirect(
1319 table_index_reg,
1320 *table_size,
1321 *table_byte_offset,
1322 *null_check,
1323 *type_check,
1324 ));
1325 }
1326 let instr: u32 = match op {
1327 // Data processing instructions
1328 ArmOp::Add { rd, rn, op2 } => {
1329 let rd_bits = reg_to_bits(rd);
1330 let rn_bits = reg_to_bits(rn);
1331 let (op2_bits, i_flag) = encode_operand2(op2)?;
1332
1333 // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1334 0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1335 | (i_flag << 25)
1336 | (rn_bits << 16)
1337 | (rd_bits << 12)
1338 | op2_bits
1339 }
1340
1341 ArmOp::Sub { rd, rn, op2 } => {
1342 let rd_bits = reg_to_bits(rd);
1343 let rn_bits = reg_to_bits(rn);
1344 let (op2_bits, i_flag) = encode_operand2(op2)?;
1345
1346 // SUB encoding: opcode=0010
1347 0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1348 }
1349
1350 // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1351 ArmOp::Adds { rd, rn, op2 } => {
1352 let rd_bits = reg_to_bits(rd);
1353 let rn_bits = reg_to_bits(rn);
1354 let (op2_bits, i_flag) = encode_operand2(op2)?;
1355
1356 // ADDS encoding: opcode=0100, S=1
1357 0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1358 }
1359
1360 ArmOp::Adc { rd, rn, op2 } => {
1361 let rd_bits = reg_to_bits(rd);
1362 let rn_bits = reg_to_bits(rn);
1363 let (op2_bits, i_flag) = encode_operand2(op2)?;
1364
1365 // ADC encoding: opcode=0101
1366 0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1367 }
1368
1369 ArmOp::Subs { rd, rn, op2 } => {
1370 let rd_bits = reg_to_bits(rd);
1371 let rn_bits = reg_to_bits(rn);
1372 let (op2_bits, i_flag) = encode_operand2(op2)?;
1373
1374 // SUBS encoding: opcode=0010, S=1
1375 0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1376 }
1377
1378 ArmOp::Sbc { rd, rn, op2 } => {
1379 let rd_bits = reg_to_bits(rd);
1380 let rn_bits = reg_to_bits(rn);
1381 let (op2_bits, i_flag) = encode_operand2(op2)?;
1382
1383 // SBC encoding: opcode=0110
1384 0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1385 }
1386
1387 ArmOp::Mul { rd, rn, rm } => {
1388 let rd_bits = reg_to_bits(rd);
1389 let rn_bits = reg_to_bits(rn);
1390 let rm_bits = reg_to_bits(rm);
1391
1392 // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1393 0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1394 }
1395
1396 ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1397 let rdlo_bits = reg_to_bits(rdlo);
1398 let rdhi_bits = reg_to_bits(rdhi);
1399 let rn_bits = reg_to_bits(rn);
1400 let rm_bits = reg_to_bits(rm);
1401
1402 // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1403 0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1404 }
1405
1406 ArmOp::Sdiv { rd, rn, rm } => {
1407 let rd_bits = reg_to_bits(rd);
1408 let rn_bits = reg_to_bits(rn);
1409 let rm_bits = reg_to_bits(rm);
1410
1411 // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1412 // ARMv7-M and above
1413 0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1414 }
1415
1416 ArmOp::Udiv { rd, rn, rm } => {
1417 let rd_bits = reg_to_bits(rd);
1418 let rn_bits = reg_to_bits(rn);
1419 let rm_bits = reg_to_bits(rm);
1420
1421 // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1422 // ARMv7-M and above
1423 0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1424 }
1425
1426 ArmOp::Mls { rd, rn, rm, ra } => {
1427 let rd_bits = reg_to_bits(rd);
1428 let rn_bits = reg_to_bits(rn);
1429 let rm_bits = reg_to_bits(rm);
1430 let ra_bits = reg_to_bits(ra);
1431
1432 // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1433 // Rd = Ra - (Rn * Rm)
1434 0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1435 }
1436
1437 ArmOp::Mla { rd, rn, rm, ra } => {
1438 let rd_bits = reg_to_bits(rd);
1439 let rn_bits = reg_to_bits(rn);
1440 let rm_bits = reg_to_bits(rm);
1441 let ra_bits = reg_to_bits(ra);
1442
1443 // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1444 // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1445 0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1446 }
1447
1448 ArmOp::And { rd, rn, op2 } => {
1449 let rd_bits = reg_to_bits(rd);
1450 let rn_bits = reg_to_bits(rn);
1451 let (op2_bits, i_flag) = encode_operand2(op2)?;
1452
1453 // AND encoding: opcode=0000
1454 0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1455 }
1456
1457 ArmOp::Orr { rd, rn, op2 } => {
1458 let rd_bits = reg_to_bits(rd);
1459 let rn_bits = reg_to_bits(rn);
1460 let (op2_bits, i_flag) = encode_operand2(op2)?;
1461
1462 // ORR encoding: opcode=1100
1463 0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1464 }
1465
1466 ArmOp::Eor { rd, rn, op2 } => {
1467 let rd_bits = reg_to_bits(rd);
1468 let rn_bits = reg_to_bits(rn);
1469 let (op2_bits, i_flag) = encode_operand2(op2)?;
1470
1471 // EOR encoding: opcode=0001
1472 0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1473 }
1474
1475 // Shift instructions
1476 ArmOp::Lsl { rd, rn, shift } => {
1477 let rd_bits = reg_to_bits(rd);
1478 let rn_bits = reg_to_bits(rn);
1479 let shift_bits = *shift & 0x1F;
1480
1481 // LSL encoding: MOV with shift
1482 0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1483 }
1484
1485 ArmOp::Lsr { rd, rn, shift } => {
1486 let rd_bits = reg_to_bits(rd);
1487 let rn_bits = reg_to_bits(rn);
1488 let shift_bits = *shift & 0x1F;
1489
1490 // LSR encoding
1491 0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1492 }
1493
1494 ArmOp::Asr { rd, rn, shift } => {
1495 let rd_bits = reg_to_bits(rd);
1496 let rn_bits = reg_to_bits(rn);
1497 let shift_bits = *shift & 0x1F;
1498
1499 // ASR encoding
1500 0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1501 }
1502
1503 ArmOp::Ror { rd, rn, shift } => {
1504 let rd_bits = reg_to_bits(rd);
1505 let rn_bits = reg_to_bits(rn);
1506 let shift_bits = *shift & 0x1F;
1507
1508 // ROR encoding: MOV with ROR shift
1509 0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1510 }
1511
1512 // Register-based shifts (ARM32)
1513 // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1514 ArmOp::LslReg { rd, rn, rm } => {
1515 let rd_bits = reg_to_bits(rd);
1516 let rn_bits = reg_to_bits(rn);
1517 let rm_bits = reg_to_bits(rm);
1518 0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1519 }
1520 ArmOp::LsrReg { rd, rn, rm } => {
1521 let rd_bits = reg_to_bits(rd);
1522 let rn_bits = reg_to_bits(rn);
1523 let rm_bits = reg_to_bits(rm);
1524 0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1525 }
1526 ArmOp::AsrReg { rd, rn, rm } => {
1527 let rd_bits = reg_to_bits(rd);
1528 let rn_bits = reg_to_bits(rn);
1529 let rm_bits = reg_to_bits(rm);
1530 0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1531 }
1532 ArmOp::RorReg { rd, rn, rm } => {
1533 let rd_bits = reg_to_bits(rd);
1534 let rn_bits = reg_to_bits(rn);
1535 let rm_bits = reg_to_bits(rm);
1536 0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1537 }
1538
1539 // RSB (Reverse Subtract): Rd = imm - Rn
1540 ArmOp::Rsb { rd, rn, imm } => {
1541 let rd_bits = reg_to_bits(rd);
1542 let rn_bits = reg_to_bits(rn);
1543 // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1544 // Opcode for RSB = 0011, I=1 (immediate), S=0
1545 //
1546 // #681 class audit: the A32 imm12 is a rotate(4):imm8 modified
1547 // immediate; `*imm & 0xFF` silently encoded a WRONG constant
1548 // for imm > 0xFF (#378 masking class). All current emitters use
1549 // imm 32, so erroring here is byte-identical for real codegen.
1550 if *imm > 0xFF {
1551 return Err(synth_core::Error::synthesis(
1552 "A32 RSB immediate > 0xFF requires a rotated-immediate encoding \
1553 (not supported) — materialize into a register",
1554 ));
1555 }
1556 0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1557 }
1558
1559 // Bit manipulation instructions
1560 ArmOp::Clz { rd, rm } => {
1561 let rd_bits = reg_to_bits(rd);
1562 let rm_bits = reg_to_bits(rm);
1563
1564 // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1565 // ARMv5T and above
1566 0xE16F0F10 | (rd_bits << 12) | rm_bits
1567 }
1568
1569 ArmOp::Rbit { rd, rm } => {
1570 let rd_bits = reg_to_bits(rd);
1571 let rm_bits = reg_to_bits(rm);
1572
1573 // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1574 // ARMv6T2 and above
1575 0xE6FF0F30 | (rd_bits << 12) | rm_bits
1576 }
1577
1578 ArmOp::Sxtb { rd, rm } => {
1579 let rd_bits = reg_to_bits(rd);
1580 let rm_bits = reg_to_bits(rm);
1581
1582 // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1583 // ARMv6 and above. rotate=00 for no rotation
1584 0xE6AF0070 | (rd_bits << 12) | rm_bits
1585 }
1586
1587 ArmOp::Sxth { rd, rm } => {
1588 let rd_bits = reg_to_bits(rd);
1589 let rm_bits = reg_to_bits(rm);
1590
1591 // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1592 // ARMv6 and above. rotate=00 for no rotation
1593 0xE6BF0070 | (rd_bits << 12) | rm_bits
1594 }
1595
1596 ArmOp::Uxtb { rd, rm } => {
1597 let rd_bits = reg_to_bits(rd);
1598 let rm_bits = reg_to_bits(rm);
1599 // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1600 0xE6EF0070 | (rd_bits << 12) | rm_bits
1601 }
1602
1603 ArmOp::Uxth { rd, rm } => {
1604 let rd_bits = reg_to_bits(rd);
1605 let rm_bits = reg_to_bits(rm);
1606 // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1607 0xE6FF0070 | (rd_bits << 12) | rm_bits
1608 }
1609
1610 // Move instructions
1611 ArmOp::Mov { rd, op2 } => {
1612 let rd_bits = reg_to_bits(rd);
1613 let (op2_bits, i_flag) = encode_operand2(op2)?;
1614
1615 // MOV encoding: opcode=1101
1616 0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1617 }
1618
1619 ArmOp::Mvn { rd, op2 } => {
1620 let rd_bits = reg_to_bits(rd);
1621 let (op2_bits, i_flag) = encode_operand2(op2)?;
1622
1623 // MVN encoding: opcode=1111
1624 0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1625 }
1626
1627 // MOVW - Move Wide (ARM32)
1628 // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1629 ArmOp::Movw { rd, imm16 } => {
1630 let rd_bits = reg_to_bits(rd);
1631 let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1632 let imm12 = (*imm16 as u32) & 0xFFF;
1633 0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1634 }
1635
1636 // MOVT - Move Top (ARM32)
1637 // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1638 ArmOp::Movt { rd, imm16 } => {
1639 let rd_bits = reg_to_bits(rd);
1640 let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1641 let imm12 = (*imm16 as u32) & 0xFFF;
1642 0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1643 }
1644
1645 // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1646 // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1647 ArmOp::MovwSym { rd, addend, .. } => {
1648 let rd_bits = reg_to_bits(rd);
1649 let v = (*addend as u32) & 0xffff;
1650 0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1651 }
1652 ArmOp::MovtSym { rd, addend, .. } => {
1653 let rd_bits = reg_to_bits(rd);
1654 let v = ((*addend as u32) >> 16) & 0xffff;
1655 0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1656 }
1657
1658 // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1659 // not used for relocatable native-pointer objects; fail loudly rather
1660 // than miscompile if it is ever reached here.
1661 ArmOp::LdrSym { .. } => {
1662 return Err(synth_core::Error::synthesis(
1663 "LdrSym (literal-pool address load) is Thumb-2-only",
1664 ));
1665 }
1666
1667 // Compare
1668 ArmOp::Cmp { rn, op2 } => {
1669 let rn_bits = reg_to_bits(rn);
1670 let (op2_bits, i_flag) = encode_operand2(op2)?;
1671
1672 // CMP encoding: opcode=1010, S=1
1673 0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1674 }
1675
1676 // Compare Negative (CMN) - computes Rn + op2 and sets flags
1677 ArmOp::Cmn { rn, op2 } => {
1678 let rn_bits = reg_to_bits(rn);
1679 let (op2_bits, i_flag) = encode_operand2(op2)?;
1680
1681 // CMN encoding: opcode=1011, S=1
1682 0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1683 }
1684
1685 // Load/Store
1686 ArmOp::Ldr { rd, addr } => {
1687 let rd_bits = reg_to_bits(rd);
1688 let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1689
1690 // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1691 // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1692 0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1693 }
1694
1695 ArmOp::Str { rd, addr } => {
1696 let rd_bits = reg_to_bits(rd);
1697 let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1698
1699 // STR encoding: L=0 (store)
1700 0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1701 }
1702
1703 // Sub-word loads (ARM32 encoding)
1704 ArmOp::Ldrb { rd, addr } => {
1705 let rd_bits = reg_to_bits(rd);
1706 let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1707 // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1708 0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1709 }
1710
1711 ArmOp::Ldrsb { rd, addr } => {
1712 let rd_bits = reg_to_bits(rd);
1713 let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1714 // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1715 let imm4h = (offset_val >> 4) & 0xF;
1716 let imm4l = offset_val & 0xF;
1717 0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1718 }
1719
1720 ArmOp::Ldrh { rd, addr } => {
1721 let rd_bits = reg_to_bits(rd);
1722 let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1723 // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1724 let imm4h = (offset_val >> 4) & 0xF;
1725 let imm4l = offset_val & 0xF;
1726 0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1727 }
1728
1729 ArmOp::Ldrsh { rd, addr } => {
1730 let rd_bits = reg_to_bits(rd);
1731 let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1732 // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1733 let imm4h = (offset_val >> 4) & 0xF;
1734 let imm4l = offset_val & 0xF;
1735 0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1736 }
1737
1738 // Sub-word stores (ARM32 encoding)
1739 ArmOp::Strb { rd, addr } => {
1740 let rd_bits = reg_to_bits(rd);
1741 let (base_bits, offset_bits) = encode_mem_addr(addr)?;
1742 // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1743 0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1744 }
1745
1746 ArmOp::Strh { rd, addr } => {
1747 let rd_bits = reg_to_bits(rd);
1748 let (base_bits, offset_val) = encode_mem_addr_imm8(addr)?;
1749 // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1750 let imm4h = (offset_val >> 4) & 0xF;
1751 let imm4l = offset_val & 0xF;
1752 0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1753 }
1754
1755 // Memory management (ARM32 encoding)
1756 ArmOp::MemorySize { rd } => {
1757 let rd_bits = reg_to_bits(rd);
1758 // MOV rd, R10, LSR #16 (memory size in bytes / 65536 = pages)
1759 // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1760 // LSR #16: shift5=10000, type=01
1761 0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1762 }
1763
1764 ArmOp::MemoryGrow { rd, .. } => {
1765 let rd_bits = reg_to_bits(rd);
1766 // On embedded, always fail: MOV rd, #-1
1767 0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1768 }
1769
1770 // Label pseudo-instruction: emits no machine code
1771 ArmOp::Label { .. } => {
1772 return Ok(Vec::new());
1773 }
1774
1775 // Branch instructions
1776 ArmOp::B { label: _ } => {
1777 // B encoding: cond(4) | 1010 | offset(24)
1778 // Simplified: branch to offset 0 (will be patched by linker/resolver)
1779 0xEA000000
1780 }
1781
1782 // Conditional branch to label (generic)
1783 ArmOp::Bcc { cond, label: _ } => {
1784 use synth_synthesis::Condition;
1785 let cond_bits: u32 = match cond {
1786 Condition::EQ => 0x0,
1787 Condition::NE => 0x1,
1788 Condition::HS => 0x2,
1789 Condition::LO => 0x3,
1790 Condition::HI => 0x8,
1791 Condition::LS => 0x9,
1792 Condition::GE => 0xA,
1793 Condition::LT => 0xB,
1794 Condition::GT => 0xC,
1795 Condition::LE => 0xD,
1796 };
1797 // B<cond> with offset 0 (will be patched)
1798 (cond_bits << 28) | 0x0A000000
1799 }
1800
1801 // BHS (Branch if Higher or Same) - used for bounds checking
1802 ArmOp::Bhs { label: _ } => {
1803 // BHS encoding: cond(2=HS) | 1010 | offset(24)
1804 0x2A000000 // BHS with offset 0
1805 }
1806
1807 // BLO (Branch if Lower) - complementary to BHS
1808 ArmOp::Blo { label: _ } => {
1809 // BLO encoding: cond(3=LO) | 1010 | offset(24)
1810 0x3A000000 // BLO with offset 0
1811 }
1812
1813 // Branch with numeric offset (in instructions)
1814 // ARM32 B instruction: offset is in instructions, stored as words
1815 // The offset is relative to PC+8 (due to ARM pipeline)
1816 ArmOp::BOffset { offset } => {
1817 // B encoding: cond(4) | 1010 | offset(24)
1818 // Offset is signed, in words (4-byte units)
1819 // ARM adds PC+8 to the offset, so we need to adjust:
1820 // target = PC + 8 + (offset * 4)
1821 // For backward branch of N instructions: offset = -(N + 2)
1822 // wrapping_sub keeps the encoder total under fuzzing (#186): an
1823 // extreme i32::MIN offset would otherwise overflow-panic; for any
1824 // real branch offset this is identical to `- 2`.
1825 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1826 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1827 0xEA000000 | offset_bits
1828 }
1829
1830 // Conditional branch with numeric offset
1831 ArmOp::BCondOffset { cond, offset } => {
1832 use synth_synthesis::Condition;
1833 let cond_bits: u32 = match cond {
1834 Condition::EQ => 0x0,
1835 Condition::NE => 0x1,
1836 Condition::HS => 0x2,
1837 Condition::LO => 0x3,
1838 Condition::HI => 0x8,
1839 Condition::LS => 0x9,
1840 Condition::GE => 0xA,
1841 Condition::LT => 0xB,
1842 Condition::GT => 0xC,
1843 Condition::LE => 0xD,
1844 };
1845 // B<cond> encoding: cond(4) | 1010 | offset(24)
1846 // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1847 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1848 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1849 (cond_bits << 28) | 0x0A000000 | offset_bits
1850 }
1851
1852 ArmOp::Bl { label: _ } => {
1853 // BL encoding: cond(4) | 1011 | offset(24). Relocatable
1854 // placeholder; an R_ARM_CALL relocation patches the target.
1855 //
1856 // #1040: the placeholder must carry an embedded addend of -8,
1857 // not 0. A32 `BL` computes `target = P + 8 + (imm24 << 2)`, so
1858 // under REL semantics a 0 addend (`eb000000`) resolves two
1859 // instructions PAST the callee entry — the A32 twin of the
1860 // Thumb #174 bug. The correct word is what `gas` emits for
1861 // `bl <extern>` in ARM mode:
1862 // ebfffffe -> `bl <self>` (imm24 = -2, offset = -8),
1863 // which nets to exactly S. Verified against
1864 // `arm-none-eabi-as -march=armv7-r`, which emits `ebfffffe`
1865 // with an R_ARM_CALL relocation.
1866 0xEBFFFFFE
1867 }
1868
1869 ArmOp::Bx { rm } => {
1870 let rm_bits = reg_to_bits(rm);
1871
1872 // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1873 0xE12FFF10 | rm_bits
1874 }
1875
1876 ArmOp::Blx { rm } => {
1877 let rm_bits = reg_to_bits(rm);
1878
1879 // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1880 0xE12FFF30 | rm_bits
1881 }
1882
1883 ArmOp::Push { regs } => {
1884 // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1885 let mut reg_list: u32 = 0;
1886 for r in regs {
1887 reg_list |= 1 << reg_to_bits(r);
1888 }
1889 0xE92D0000 | reg_list
1890 }
1891
1892 ArmOp::Pop { regs } => {
1893 // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1894 let mut reg_list: u32 = 0;
1895 for r in regs {
1896 reg_list |= 1 << reg_to_bits(r);
1897 }
1898 0xE8BD0000 | reg_list
1899 }
1900
1901 ArmOp::Nop => {
1902 // NOP encoding: MOV R0, R0
1903 0xE1A00000
1904 }
1905
1906 ArmOp::Udf { imm } => {
1907 // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1908 // We only use imm8, so split into imm4_hi and imm4_lo
1909 let imm8 = *imm as u32;
1910 0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1911 }
1912
1913 // #615: handled by the `encode_arm_expanded` early return at the
1914 // top of this function — a real MOV{cond}/MOV pair now, never a
1915 // silent NOP again.
1916 ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1917 unreachable!("handled by encode_arm_expanded (#615)")
1918 }
1919
1920 // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1921 // models these, but NO codegen path constructs them (the selector
1922 // lowers select/locals/globals/br_table/call to real instruction
1923 // sequences before the encoder). Encoding one as a NOP silently
1924 // dropped the operation (#615 class); a typed Err keeps the
1925 // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1926 // while making any future reachability LOUD.
1927 ArmOp::Select { .. }
1928 | ArmOp::LocalGet { .. }
1929 | ArmOp::LocalSet { .. }
1930 | ArmOp::LocalTee { .. }
1931 | ArmOp::GlobalGet { .. }
1932 | ArmOp::GlobalSet { .. }
1933 | ArmOp::BrTable { .. }
1934 | ArmOp::Call { .. } => {
1935 return Err(synth_core::Error::synthesis(format!(
1936 "verification-only pseudo-op {op:?} reached the A32 encoder — \
1937 codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1938 )));
1939 }
1940
1941 // #594: CallIndirect is expanded to a real multi-instruction
1942 // sequence by the early return at the top of this function —
1943 // it must NEVER fall through to a silent NOP again.
1944 ArmOp::CallIndirect { .. } => {
1945 unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1946 }
1947
1948 // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1949 // multi-instruction sequence by `encode_arm_expanded` — the
1950 // "encode as NOP for now" era ended with the value silently
1951 // vanishing on `--target cortex-r5`.
1952 ArmOp::I64Add { .. }
1953 | ArmOp::I64Sub { .. }
1954 | ArmOp::I64DivS { .. }
1955 | ArmOp::I64DivU { .. }
1956 | ArmOp::I64RemS { .. }
1957 | ArmOp::I64RemU { .. }
1958 | ArmOp::I64Clz { .. }
1959 | ArmOp::I64Ctz { .. }
1960 | ArmOp::I64Popcnt { .. }
1961 | ArmOp::I64And { .. }
1962 | ArmOp::I64Or { .. }
1963 | ArmOp::I64Xor { .. }
1964 | ArmOp::I64Eqz { .. }
1965 | ArmOp::I64Eq { .. }
1966 | ArmOp::I64Ne { .. }
1967 | ArmOp::I64LtS { .. }
1968 | ArmOp::I64LtU { .. }
1969 | ArmOp::I64LeS { .. }
1970 | ArmOp::I64LeU { .. }
1971 | ArmOp::I64GtS { .. }
1972 | ArmOp::I64GtU { .. }
1973 | ArmOp::I64GeS { .. }
1974 | ArmOp::I64GeU { .. }
1975 | ArmOp::I64Const { .. }
1976 | ArmOp::I64Ldr { .. }
1977 | ArmOp::I64Str { .. }
1978 | ArmOp::I64ExtendI32S { .. }
1979 | ArmOp::I64ExtendI32U { .. }
1980 | ArmOp::I64Extend8S { .. }
1981 | ArmOp::I64Extend16S { .. }
1982 | ArmOp::I64Extend32S { .. }
1983 | ArmOp::I32WrapI64 { .. } => {
1984 unreachable!("handled by encode_arm_expanded (#615)")
1985 }
1986
1987 // f32 VFP single-precision instructions
1988 ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1989 ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1990 ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1991 ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1992 ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1993 ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1994 ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1995
1996 // f32 pseudo-ops — multi-instruction sequences
1997 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1998 ArmOp::F32Ceil { sd, sm } => {
1999 return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
2000 }
2001 ArmOp::F32Floor { sd, sm } => {
2002 return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
2003 }
2004 ArmOp::F32Trunc { sd, sm } => {
2005 return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2006 }
2007 ArmOp::F32Nearest { sd, sm } => {
2008 return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2009 }
2010 ArmOp::F32Min { sd, sn, sm } => {
2011 return self.encode_arm_f32_minmax(sd, sn, sm, true);
2012 }
2013 ArmOp::F32Max { sd, sn, sm } => {
2014 return self.encode_arm_f32_minmax(sd, sn, sm, false);
2015 }
2016 ArmOp::F32Copysign { sd, sn, sm } => {
2017 return self.encode_arm_f32_copysign(sd, sn, sm);
2018 }
2019
2020 // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2021 ArmOp::F32Eq { rd, sn, sm } => {
2022 return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2023 }
2024 ArmOp::F32Ne { rd, sn, sm } => {
2025 return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2026 }
2027 ArmOp::F32Lt { rd, sn, sm } => {
2028 return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2029 }
2030 ArmOp::F32Le { rd, sn, sm } => {
2031 return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2032 }
2033 ArmOp::F32Gt { rd, sn, sm } => {
2034 return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2035 }
2036 ArmOp::F32Ge { rd, sn, sm } => {
2037 return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2038 }
2039
2040 // f32 const — multi-instruction: MOVW + MOVT + VMOV
2041 ArmOp::F32Const { sd, value } => {
2042 return self.encode_arm_f32_const(sd, *value);
2043 }
2044
2045 ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2046 ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2047
2048 // f32 conversions — multi-instruction sequences
2049 ArmOp::F32ConvertI32S { sd, rm } => {
2050 return self.encode_arm_f32_convert_i32(sd, rm, true);
2051 }
2052 ArmOp::F32ConvertI32U { sd, rm } => {
2053 return self.encode_arm_f32_convert_i32(sd, rm, false);
2054 }
2055 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2056 return Err(synth_core::Error::synthesis(
2057 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2058 ));
2059 }
2060 ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2061 ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2062 ArmOp::I32TruncF32S { rd, sm } => {
2063 return self.encode_arm_i32_trunc_f32(rd, sm, true);
2064 }
2065 ArmOp::I32TruncF32U { rd, sm } => {
2066 return self.encode_arm_i32_trunc_f32(rd, sm, false);
2067 }
2068
2069 // f64 VFP double-precision instructions (ARM32)
2070 // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2071 ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2072 ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2073 ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2074 ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2075 ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2076 ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2077 ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2078
2079 // f64 pseudo-ops
2080 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2081 ArmOp::F64Ceil { dd, dm } => {
2082 return self.encode_arm_f64_rounding(dd, dm, 0b01);
2083 }
2084 ArmOp::F64Floor { dd, dm } => {
2085 return self.encode_arm_f64_rounding(dd, dm, 0b10);
2086 }
2087 ArmOp::F64Trunc { dd, dm } => {
2088 return self.encode_arm_f64_rounding(dd, dm, 0b11);
2089 }
2090 ArmOp::F64Nearest { dd, dm } => {
2091 return self.encode_arm_f64_rounding(dd, dm, 0b00);
2092 }
2093 ArmOp::F64Min { dd, dn, dm } => {
2094 return self.encode_arm_f64_minmax(dd, dn, dm, true);
2095 }
2096 ArmOp::F64Max { dd, dn, dm } => {
2097 return self.encode_arm_f64_minmax(dd, dn, dm, false);
2098 }
2099 ArmOp::F64Copysign { dd, dn, dm } => {
2100 return self.encode_arm_f64_copysign(dd, dn, dm);
2101 }
2102
2103 // f64 comparisons
2104 ArmOp::F64Eq { rd, dn, dm } => {
2105 return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2106 }
2107 ArmOp::F64Ne { rd, dn, dm } => {
2108 return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2109 }
2110 ArmOp::F64Lt { rd, dn, dm } => {
2111 return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2112 }
2113 ArmOp::F64Le { rd, dn, dm } => {
2114 return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2115 }
2116 ArmOp::F64Gt { rd, dn, dm } => {
2117 return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2118 }
2119 ArmOp::F64Ge { rd, dn, dm } => {
2120 return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2121 }
2122
2123 ArmOp::F64Const { dd, value } => {
2124 return self.encode_arm_f64_const(dd, *value);
2125 }
2126
2127 ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2128 ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2129
2130 ArmOp::F64ConvertI32S { dd, rm } => {
2131 return self.encode_arm_f64_convert_i32(dd, rm, true);
2132 }
2133 ArmOp::F64ConvertI32U { dd, rm } => {
2134 return self.encode_arm_f64_convert_i32(dd, rm, false);
2135 }
2136 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2137 return Err(synth_core::Error::synthesis(
2138 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2139 ));
2140 }
2141 ArmOp::F64PromoteF32 { dd, sm } => {
2142 return self.encode_arm_f64_promote_f32(dd, sm);
2143 }
2144 // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2145 // has none — the selector declines every float op there), so the
2146 // A32 encoder refuses loudly instead of shipping an untested
2147 // encoding (#615: never a silent wrong byte).
2148 ArmOp::F32DemoteF64 { .. } => {
2149 return Err(synth_core::Error::synthesis(
2150 "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2151 ));
2152 }
2153 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2154 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2155 }
2156 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2157 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2158 }
2159 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2160 return Err(synth_core::Error::synthesis(
2161 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2162 ));
2163 }
2164 ArmOp::I32TruncF64S { rd, dm } => {
2165 return self.encode_arm_i32_trunc_f64(rd, dm, true);
2166 }
2167 ArmOp::I32TruncF64U { rd, dm } => {
2168 return self.encode_arm_i32_trunc_f64(rd, dm, false);
2169 }
2170 // #615: multi-instruction i64 sequences — expanded to real A32 by
2171 // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2172 ArmOp::I64SetCond { .. }
2173 | ArmOp::I64SetCondZ { .. }
2174 | ArmOp::I64Mul { .. }
2175 | ArmOp::I64Shl { .. }
2176 | ArmOp::I64ShrS { .. }
2177 | ArmOp::I64ShrU { .. }
2178 | ArmOp::I64Rotl { .. }
2179 | ArmOp::I64Rotr { .. } => {
2180 unreachable!("handled by encode_arm_expanded (#615)")
2181 }
2182
2183 // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2184 ArmOp::MveLoad { .. }
2185 | ArmOp::MveStore { .. }
2186 | ArmOp::MveConst { .. }
2187 | ArmOp::MveAnd { .. }
2188 | ArmOp::MveOrr { .. }
2189 | ArmOp::MveEor { .. }
2190 | ArmOp::MveMvn { .. }
2191 | ArmOp::MveBic { .. }
2192 | ArmOp::MveAddI { .. }
2193 | ArmOp::MveSubI { .. }
2194 | ArmOp::MveMulI { .. }
2195 | ArmOp::MveNegI { .. }
2196 | ArmOp::MveCmpEqI { .. }
2197 | ArmOp::MveCmpNeI { .. }
2198 | ArmOp::MveCmpLtS { .. }
2199 | ArmOp::MveCmpLtU { .. }
2200 | ArmOp::MveCmpGtS { .. }
2201 | ArmOp::MveCmpGtU { .. }
2202 | ArmOp::MveCmpLeS { .. }
2203 | ArmOp::MveCmpLeU { .. }
2204 | ArmOp::MveCmpGeS { .. }
2205 | ArmOp::MveCmpGeU { .. }
2206 | ArmOp::MveDup { .. }
2207 | ArmOp::MveExtractLane { .. }
2208 | ArmOp::MveInsertLane { .. }
2209 | ArmOp::MveAddF32 { .. }
2210 | ArmOp::MveSubF32 { .. }
2211 | ArmOp::MveMulF32 { .. }
2212 | ArmOp::MveNegF32 { .. }
2213 | ArmOp::MveAbsF32 { .. }
2214 | ArmOp::MveCmpEqF32 { .. }
2215 | ArmOp::MveCmpNeF32 { .. }
2216 | ArmOp::MveCmpLtF32 { .. }
2217 | ArmOp::MveCmpLeF32 { .. }
2218 | ArmOp::MveCmpGtF32 { .. }
2219 | ArmOp::MveCmpGeF32 { .. }
2220 | ArmOp::MveDupF32 { .. }
2221 | ArmOp::MveExtractLaneF32 { .. }
2222 | ArmOp::MveReplaceLaneF32 { .. }
2223 | ArmOp::MveDivF32 { .. }
2224 | ArmOp::MveSqrtF32 { .. } => {
2225 // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2226 // is no A32 encoding. The selector only emits MVE ops for
2227 // Thumb targets — a NOP here silently dropped the vector op
2228 // if that invariant ever broke (#615 class). Err keeps the
2229 // encoder total and the failure loud.
2230 return Err(synth_core::Error::synthesis(format!(
2231 "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2232 )));
2233 }
2234 };
2235
2236 // ARM32 instructions are little-endian
2237 Ok(instr.to_le_bytes().to_vec())
2238 }
2239
2240 // === ARM32 VFP multi-instruction helpers ===
2241
2242 /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2243 fn encode_arm_f32_compare(
2244 &self,
2245 rd: &Reg,
2246 sn: &VfpReg,
2247 sm: &VfpReg,
2248 cond_code: u32,
2249 ) -> Result<Vec<u8>> {
2250 let mut bytes = Vec::new();
2251
2252 // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2253 let sn_num = vfp_sreg_to_num(sn)?;
2254 let sm_num = vfp_sreg_to_num(sm)?;
2255 let (vd, d) = encode_sreg(sn_num);
2256 let (vm, m) = encode_sreg(sm_num);
2257 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2258 bytes.extend_from_slice(&vcmp.to_le_bytes());
2259
2260 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2261 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2262
2263 // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2264 let rd_bits = reg_to_bits(rd);
2265 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2266 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2267
2268 // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2269 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2270 bytes.extend_from_slice(&mov_one.to_le_bytes());
2271
2272 Ok(bytes)
2273 }
2274
2275 /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2276 fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2277 let mut bytes = Vec::new();
2278 let bits = value.to_bits();
2279
2280 // Use R12 as temp register for constant loading
2281 let rt: u32 = 12; // R12/IP
2282
2283 // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2284 let lo16 = bits & 0xFFFF;
2285 let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2286 bytes.extend_from_slice(&movw.to_le_bytes());
2287
2288 // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2289 let hi16 = (bits >> 16) & 0xFFFF;
2290 let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2291 bytes.extend_from_slice(&movt.to_le_bytes());
2292
2293 // VMOV Sd, R12
2294 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2295 bytes.extend_from_slice(&vmov.to_le_bytes());
2296
2297 Ok(bytes)
2298 }
2299
2300 /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2301 fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2302 let mut bytes = Vec::new();
2303
2304 // VMOV Sd, Rm — move integer to VFP register
2305 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2306 bytes.extend_from_slice(&vmov.to_le_bytes());
2307
2308 // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2309 // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2310 // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2311 // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2312 // the two were previously swapped, silently making `convert_i32_s`
2313 // an unsigned conversion).
2314 let sd_num = vfp_sreg_to_num(sd)?;
2315 let (vd, d) = encode_sreg(sd_num);
2316 let (vm, m) = encode_sreg(sd_num); // same register as source
2317 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2318 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2319 bytes.extend_from_slice(&vcvt.to_le_bytes());
2320
2321 Ok(bytes)
2322 }
2323
2324 /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2325 /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2326 /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2327 /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2328 /// Simplified: convert to int (toward zero for trunc) then back to float.
2329 /// Encode F32 rounding as ARM32.
2330 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2331 ///
2332 /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2333 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2334 /// which honours FPSCR rmode), then restores FPSCR.
2335 fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2336 let mut bytes = Vec::new();
2337 let sm_num = vfp_sreg_to_num(sm)?;
2338 let sd_num = vfp_sreg_to_num(sd)?;
2339 let (vd_s, d_s) = encode_sreg(sd_num);
2340 let (vm_s, m_s) = encode_sreg(sm_num);
2341
2342 if mode == 0b11 {
2343 // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2344 // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2345 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2346 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2347 } else {
2348 // ceil/floor/nearest: manipulate FPSCR rounding mode
2349 let rt: u32 = 12; // R12/IP as temp
2350
2351 // VMRS R12, FPSCR
2352 let vmrs = 0xEEF10A10 | (rt << 12);
2353 bytes.extend_from_slice(&vmrs.to_le_bytes());
2354
2355 // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2356 // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2357 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2358 bytes.extend_from_slice(&bic.to_le_bytes());
2359
2360 // ORR R12, R12, #(mode << 22) — set desired rounding mode
2361 if mode != 0 {
2362 // mode<<22: rotation=5, imm8=mode
2363 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2364 bytes.extend_from_slice(&orr.to_le_bytes());
2365 }
2366
2367 // VMSR FPSCR, R12
2368 let vmsr = 0xEEE10A10 | (rt << 12);
2369 bytes.extend_from_slice(&vmsr.to_le_bytes());
2370
2371 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2372 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2373 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2374
2375 // Restore FPSCR: clear rmode bits back to nearest (default)
2376 bytes.extend_from_slice(&vmrs.to_le_bytes());
2377 bytes.extend_from_slice(&bic.to_le_bytes());
2378 bytes.extend_from_slice(&vmsr.to_le_bytes());
2379 }
2380
2381 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2382 let (vd2, d2) = encode_sreg(sd_num);
2383 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2384 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2385
2386 Ok(bytes)
2387 }
2388
2389 /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2390 fn encode_arm_f32_minmax(
2391 &self,
2392 sd: &VfpReg,
2393 sn: &VfpReg,
2394 sm: &VfpReg,
2395 is_min: bool,
2396 ) -> Result<Vec<u8>> {
2397 let mut bytes = Vec::new();
2398 let sn_num = vfp_sreg_to_num(sn)?;
2399 let sm_num = vfp_sreg_to_num(sm)?;
2400 let sd_num = vfp_sreg_to_num(sd)?;
2401
2402 // VMOV Sd, Sn (start with first operand)
2403 let (vd, d) = encode_sreg(sd_num);
2404 let (vn, n) = encode_sreg(sn_num);
2405 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2406 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2407
2408 // VCMP.F32 Sn, Sm
2409 let (vm, m) = encode_sreg(sm_num);
2410 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2411 bytes.extend_from_slice(&vcmp.to_le_bytes());
2412
2413 // VMRS APSR_nzcv, FPSCR
2414 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2415
2416 // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2417 // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2418 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2419
2420 // VMOV{cond} Sd, Sm — conditional VMOV
2421 let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2422 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2423
2424 Ok(bytes)
2425 }
2426
2427 /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2428 fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2429 let mut bytes = Vec::new();
2430
2431 // VMOV R12, Sm (get sign source bits)
2432 let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2433 bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2434
2435 // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2436 let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2437 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2438
2439 // AND R12, R12, #0x80000000 (keep only sign bit)
2440 // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2441 // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2442 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2443 bytes.extend_from_slice(&and_sign.to_le_bytes());
2444
2445 // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2446 // R0 = register 0, so Rn and Rd fields are 0
2447 let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2448 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2449
2450 // ORR R0, R0, R12 (combine sign + magnitude)
2451 // R0 = register 0, so Rn and Rd fields are 0
2452 let orr = 0xE1800000u32 | 12;
2453 bytes.extend_from_slice(&orr.to_le_bytes());
2454
2455 // VMOV Sd, R0
2456 let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2457 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2458
2459 Ok(bytes)
2460 }
2461
2462 /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2463 fn encode_arm_f64_compare(
2464 &self,
2465 rd: &Reg,
2466 dn: &VfpReg,
2467 dm: &VfpReg,
2468 cond_code: u32,
2469 ) -> Result<Vec<u8>> {
2470 let mut bytes = Vec::new();
2471
2472 // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2473 let dn_num = vfp_dreg_to_num(dn)?;
2474 let dm_num = vfp_dreg_to_num(dm)?;
2475 let (vd, d) = encode_dreg(dn_num);
2476 let (vm, m) = encode_dreg(dm_num);
2477 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2478 bytes.extend_from_slice(&vcmp.to_le_bytes());
2479
2480 // VMRS APSR_nzcv, FPSCR
2481 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2482
2483 // MOV rd, #0
2484 let rd_bits = reg_to_bits(rd);
2485 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2486 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2487
2488 // MOVcond rd, #1
2489 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2490 bytes.extend_from_slice(&mov_one.to_le_bytes());
2491
2492 Ok(bytes)
2493 }
2494
2495 /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2496 fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2497 let mut bytes = Vec::new();
2498 let bits = value.to_bits();
2499 let lo32 = bits as u32;
2500 let hi32 = (bits >> 32) as u32;
2501
2502 // Load low 32 bits into R0 (Rd field = 0 for R0)
2503 let lo16 = lo32 & 0xFFFF;
2504 let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2505 bytes.extend_from_slice(&movw_r0.to_le_bytes());
2506 let hi16 = (lo32 >> 16) & 0xFFFF;
2507 let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2508 bytes.extend_from_slice(&movt_r0.to_le_bytes());
2509
2510 // Load high 32 bits into R12
2511 let lo16 = hi32 & 0xFFFF;
2512 let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2513 bytes.extend_from_slice(&movw_r12.to_le_bytes());
2514 let hi16 = (hi32 >> 16) & 0xFFFF;
2515 let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2516 bytes.extend_from_slice(&movt_r12.to_le_bytes());
2517
2518 // VMOV Dd, R0, R12
2519 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2520 bytes.extend_from_slice(&vmov.to_le_bytes());
2521
2522 Ok(bytes)
2523 }
2524
2525 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2526 fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2527 let mut bytes = Vec::new();
2528
2529 // Use S0 as intermediate: VMOV S0, Rm
2530 let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2531 bytes.extend_from_slice(&vmov.to_le_bytes());
2532
2533 // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2534 // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2535 let dd_num = vfp_dreg_to_num(dd)?;
2536 let (vd, d) = encode_dreg(dd_num);
2537 let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2538 // S0 is register 0: Vm=0, M=0
2539 let vcvt = base | (d << 22) | (vd << 12);
2540 bytes.extend_from_slice(&vcvt.to_le_bytes());
2541
2542 Ok(bytes)
2543 }
2544
2545 /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2546 fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2547 let dd_num = vfp_dreg_to_num(dd)?;
2548 let sm_num = vfp_sreg_to_num(sm)?;
2549 let (vd, d) = encode_dreg(dd_num);
2550 let (vm, m) = encode_sreg(sm_num);
2551
2552 // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2553 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2554 Ok(vcvt.to_le_bytes().to_vec())
2555 }
2556
2557 /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2558 fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2559 let mut bytes = Vec::new();
2560 let dm_num = vfp_dreg_to_num(dm)?;
2561 let (vm, m) = encode_dreg(dm_num);
2562
2563 // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2564 // S0: Vd=0, D=0
2565 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2566 let vcvt = base | (m << 5) | vm;
2567 bytes.extend_from_slice(&vcvt.to_le_bytes());
2568
2569 // VMOV Rd, S0
2570 let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2571 bytes.extend_from_slice(&vmov.to_le_bytes());
2572
2573 Ok(bytes)
2574 }
2575
2576 /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2577 /// Encode F64 rounding as ARM32.
2578 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2579 ///
2580 /// For trunc: uses VCVTR.S32.F64 (always truncates).
2581 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2582 /// then restores FPSCR.
2583 fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2584 let mut bytes = Vec::new();
2585 let dm_num = vfp_dreg_to_num(dm)?;
2586 let dd_num = vfp_dreg_to_num(dd)?;
2587 let (vm, m) = encode_dreg(dm_num);
2588 let (vd, d) = encode_dreg(dd_num);
2589
2590 if mode == 0b11 {
2591 // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2592 let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2593 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2594 } else {
2595 // ceil/floor/nearest: manipulate FPSCR rounding mode
2596 let rt: u32 = 12;
2597
2598 // VMRS R12, FPSCR
2599 let vmrs = 0xEEF10A10 | (rt << 12);
2600 bytes.extend_from_slice(&vmrs.to_le_bytes());
2601
2602 // BIC R12, R12, #(3 << 22)
2603 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2604 bytes.extend_from_slice(&bic.to_le_bytes());
2605
2606 // ORR R12, R12, #(mode << 22)
2607 if mode != 0 {
2608 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2609 bytes.extend_from_slice(&orr.to_le_bytes());
2610 }
2611
2612 // VMSR FPSCR, R12
2613 let vmsr = 0xEEE10A10 | (rt << 12);
2614 bytes.extend_from_slice(&vmsr.to_le_bytes());
2615
2616 // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2617 let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2618 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2619
2620 // Restore FPSCR
2621 bytes.extend_from_slice(&vmrs.to_le_bytes());
2622 bytes.extend_from_slice(&bic.to_le_bytes());
2623 bytes.extend_from_slice(&vmsr.to_le_bytes());
2624 }
2625
2626 // VCVT.F64.S32 Dd, S0 (convert back to double)
2627 let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2628 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2629
2630 Ok(bytes)
2631 }
2632
2633 /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2634 fn encode_arm_f64_minmax(
2635 &self,
2636 dd: &VfpReg,
2637 dn: &VfpReg,
2638 dm: &VfpReg,
2639 is_min: bool,
2640 ) -> Result<Vec<u8>> {
2641 let mut bytes = Vec::new();
2642 let dn_num = vfp_dreg_to_num(dn)?;
2643 let dm_num = vfp_dreg_to_num(dm)?;
2644 let dd_num = vfp_dreg_to_num(dd)?;
2645
2646 // VMOV.F64 Dd, Dn (start with first operand)
2647 let (vd, d) = encode_dreg(dd_num);
2648 let (vn, n) = encode_dreg(dn_num);
2649 let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2650 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2651
2652 // VCMP.F64 Dn, Dm
2653 let (vm, m) = encode_dreg(dm_num);
2654 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2655 bytes.extend_from_slice(&vcmp.to_le_bytes());
2656
2657 // VMRS APSR_nzcv, FPSCR
2658 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2659
2660 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2661 let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2662 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2663
2664 Ok(bytes)
2665 }
2666
2667 /// Encode F64 copysign as ARM32
2668 fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2669 let mut bytes = Vec::new();
2670
2671 // VMOV R0, R12, Dm (get sign source bits)
2672 let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2673 bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2674
2675 // VMOV R1, R2, Dn (get magnitude source bits)
2676 // We use R1 (lo) and R2 (hi) for the magnitude
2677 let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2678 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2679
2680 // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2681 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2682 bytes.extend_from_slice(&and_sign.to_le_bytes());
2683
2684 // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2685 let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2686 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2687
2688 // ORR R2, R2, R12 (combine sign + magnitude)
2689 let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2690 bytes.extend_from_slice(&orr.to_le_bytes());
2691
2692 // VMOV Dd, R1, R2
2693 let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2694 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2695
2696 Ok(bytes)
2697 }
2698
2699 /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2700 fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2701 let mut bytes = Vec::new();
2702
2703 // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2704 // We use Sm as both source and destination for the intermediate result
2705 let sm_num = vfp_sreg_to_num(sm)?;
2706 let (vd, d) = encode_sreg(sm_num);
2707 let (vm, m) = encode_sreg(sm_num);
2708 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2709 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2710 bytes.extend_from_slice(&vcvt.to_le_bytes());
2711
2712 // VMOV Rd, Sm — move result back to core register
2713 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2714 bytes.extend_from_slice(&vmov.to_le_bytes());
2715
2716 Ok(bytes)
2717 }
2718
2719 /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2720 fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2721 // Thumb-2 supports both 16-bit and 32-bit instructions
2722 // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2723 match op {
2724 // === 16-bit Thumb encodings ===
2725 ArmOp::Add { rd, rn, op2 } => {
2726 let rd_bits = reg_to_bits(rd) as u16;
2727 let rn_bits = reg_to_bits(rn) as u16;
2728
2729 if let Operand2::Reg(rm) = op2 {
2730 let rm_bits = reg_to_bits(rm) as u16;
2731 // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2732 // high registers (e.g. R12, the MemLoad/MemStore base
2733 // scratch) the bits overflow into adjacent fields, silently
2734 // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2735 // was emitted as `adds r4,r5,r1`. Guard on all three regs
2736 // being low and fall back to 32-bit ADD.W otherwise, exactly
2737 // as the Sub handler below does.
2738 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2739 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2740 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2741 Ok(instr.to_le_bytes().to_vec())
2742 } else {
2743 // ADD.W Rd, Rn, Rm (32-bit) for high registers
2744 self.encode_thumb32_add_reg_raw(
2745 rd_bits as u32,
2746 rn_bits as u32,
2747 rm_bits as u32,
2748 )
2749 }
2750 } else if let Operand2::Imm(imm) = op2 {
2751 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2752 // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2753 let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2754 Ok(instr.to_le_bytes().to_vec())
2755 } else {
2756 // Use 32-bit ADD for larger immediates
2757 self.encode_thumb32_add(rd, rn, *imm as u32)
2758 }
2759 } else {
2760 // Fallback to 32-bit encoding
2761 self.encode_thumb32_add(rd, rn, 0)
2762 }
2763 }
2764
2765 ArmOp::Sub { rd, rn, op2 } => {
2766 let rd_bits = reg_to_bits(rd) as u16;
2767 let rn_bits = reg_to_bits(rn) as u16;
2768
2769 if let Operand2::Reg(rm) = op2 {
2770 let rm_bits = reg_to_bits(rm) as u16;
2771 // 16-bit SUBS can only use low registers (R0-R7)
2772 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2773 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2774 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2775 Ok(instr.to_le_bytes().to_vec())
2776 } else {
2777 // Use 32-bit SUB.W for high registers
2778 self.encode_thumb32_sub_reg_raw(
2779 rd_bits as u32,
2780 rn_bits as u32,
2781 rm_bits as u32,
2782 )
2783 }
2784 } else if let Operand2::Imm(imm) = op2 {
2785 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2786 // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2787 let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2788 Ok(instr.to_le_bytes().to_vec())
2789 } else {
2790 self.encode_thumb32_sub(rd, rn, *imm as u32)
2791 }
2792 } else {
2793 self.encode_thumb32_sub(rd, rn, 0)
2794 }
2795 }
2796
2797 ArmOp::Mov { rd, op2 } => {
2798 let rd_bits = reg_to_bits(rd) as u16;
2799
2800 if let Operand2::Imm(imm) = op2 {
2801 // #498: the old test here was the SIGNED `*imm <= 255`,
2802 // so a negative immediate (e.g. -1) fell into the 16-bit
2803 // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2804 // #0xFF). A positive imm above 0xFFFF was equally wrong:
2805 // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2806 // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2807 // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2808 // produces the wide shape today (both selectors
2809 // materialize wide constants as explicit Movw/Movt or
2810 // Movw+Mvn), so this is byte-identical on shipped paths —
2811 // it retires the latent wrong-value encodings the
2812 // `estimator_encoder_agreement` oracle had pinned.
2813 let uimm = *imm as u32;
2814 if uimm <= 255 && rd_bits < 8 {
2815 // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2816 let imm_bits = (*imm as u16) & 0xFF;
2817 let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2818 Ok(instr.to_le_bytes().to_vec())
2819 } else if uimm <= 0xFFFF {
2820 // Use 32-bit MOVW for 16-bit immediates
2821 self.encode_thumb32_movw(rd, uimm)
2822 } else {
2823 // Full 32-bit value: MOVW low16 + MOVT high16
2824 let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2825 bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2826 Ok(bytes)
2827 }
2828 } else if let Operand2::Reg(rm) = op2 {
2829 let rm_bits = reg_to_bits(rm) as u16;
2830 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2831 // D = Rd[3], Rd[2:0] in lower bits
2832 let d_bit = (rd_bits >> 3) & 1;
2833 let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2834 Ok(instr.to_le_bytes().to_vec())
2835 } else {
2836 let instr: u16 = 0xBF00; // NOP fallback
2837 Ok(instr.to_le_bytes().to_vec())
2838 }
2839 }
2840
2841 ArmOp::Push { regs } => {
2842 // Thumb-2 PUSH encoding:
2843 // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2844 // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2845 let mut reg_list: u16 = 0;
2846 let mut need_32bit = false;
2847 for r in regs {
2848 let bit = reg_to_bits(r);
2849 if bit >= 8 && *r != Reg::LR {
2850 need_32bit = true;
2851 }
2852 reg_list |= 1 << bit;
2853 }
2854 if !need_32bit {
2855 // 16-bit PUSH: 1011 010 M rrrrrrrr
2856 let m_bit = if reg_list & (1 << 14) != 0 {
2857 1u16
2858 } else {
2859 0u16
2860 };
2861 let low_regs = reg_list & 0xFF;
2862 let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2863 Ok(instr.to_le_bytes().to_vec())
2864 } else {
2865 // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2866 let hw1: u16 = 0xE92D;
2867 let hw2: u16 = reg_list;
2868 let mut bytes = hw1.to_le_bytes().to_vec();
2869 bytes.extend_from_slice(&hw2.to_le_bytes());
2870 Ok(bytes)
2871 }
2872 }
2873
2874 ArmOp::Pop { regs } => {
2875 // Thumb-2 POP encoding:
2876 // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2877 // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2878 let mut reg_list: u16 = 0;
2879 let mut need_32bit = false;
2880 for r in regs {
2881 let bit = reg_to_bits(r);
2882 if bit >= 8 && *r != Reg::PC {
2883 need_32bit = true;
2884 }
2885 reg_list |= 1 << bit;
2886 }
2887 if !need_32bit {
2888 // 16-bit POP: 1011 110 P rrrrrrrr
2889 let p_bit = if reg_list & (1 << 15) != 0 {
2890 1u16
2891 } else {
2892 0u16
2893 };
2894 let low_regs = reg_list & 0xFF;
2895 let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2896 Ok(instr.to_le_bytes().to_vec())
2897 } else {
2898 // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2899 let hw1: u16 = 0xE8BD;
2900 let hw2: u16 = reg_list;
2901 let mut bytes = hw1.to_le_bytes().to_vec();
2902 bytes.extend_from_slice(&hw2.to_le_bytes());
2903 Ok(bytes)
2904 }
2905 }
2906
2907 ArmOp::Nop => {
2908 let instr: u16 = 0xBF00; // NOP in Thumb-2
2909 Ok(instr.to_le_bytes().to_vec())
2910 }
2911
2912 ArmOp::Udf { imm } => {
2913 // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2914 // This triggers UsageFault/HardFault, used for WASM traps
2915 let instr: u16 = 0xDE00 | (*imm as u16);
2916 let bytes = instr.to_le_bytes().to_vec();
2917 encoding_contracts::verify_thumb16(&bytes);
2918 Ok(bytes)
2919 }
2920
2921 // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2922 // ADDS sets flags (carry), ADC uses carry from previous ADDS
2923 ArmOp::Adds { rd, rn, op2 } => {
2924 let rd_bits = reg_to_bits(rd) as u16;
2925 let rn_bits = reg_to_bits(rn) as u16;
2926
2927 if let Operand2::Reg(rm) = op2 {
2928 let rm_bits = reg_to_bits(rm) as u16;
2929 // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2930 // operands in R8-R11, which would overflow the 3-bit fields
2931 // and corrupt the operands (#178/#180 class). Guard and fall
2932 // back to 32-bit ADDS.W for high registers.
2933 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2934 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2935 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2936 Ok(instr.to_le_bytes().to_vec())
2937 } else {
2938 self.encode_thumb32_adds_reg_raw(
2939 rd_bits as u32,
2940 rn_bits as u32,
2941 rm_bits as u32,
2942 )
2943 }
2944 } else {
2945 // 32-bit Thumb-2 ADDS with immediate
2946 self.encode_thumb32_adds(rd, rn, 0)
2947 }
2948 }
2949
2950 // ADC: Add with Carry (Thumb-2 32-bit)
2951 // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2952 ArmOp::Adc { rd, rn, op2 } => {
2953 let rd_bits = reg_to_bits(rd);
2954 let rn_bits = reg_to_bits(rn);
2955
2956 if let Operand2::Reg(rm) = op2 {
2957 let rm_bits = reg_to_bits(rm);
2958 // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2959 let hw1: u16 = (0xEB40 | rn_bits) as u16;
2960 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2961
2962 let mut bytes = hw1.to_le_bytes().to_vec();
2963 bytes.extend_from_slice(&hw2.to_le_bytes());
2964 Ok(bytes)
2965 } else {
2966 // ADC with immediate - use 32-bit encoding
2967 let hw1: u16 = (0xF140 | rn_bits) as u16;
2968 let hw2: u16 = (rd_bits << 8) as u16;
2969 let mut bytes = hw1.to_le_bytes().to_vec();
2970 bytes.extend_from_slice(&hw2.to_le_bytes());
2971 Ok(bytes)
2972 }
2973 }
2974
2975 // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2976 ArmOp::Subs { rd, rn, op2 } => {
2977 let rd_bits = reg_to_bits(rd) as u16;
2978 let rn_bits = reg_to_bits(rn) as u16;
2979
2980 if let Operand2::Reg(rm) = op2 {
2981 let rm_bits = reg_to_bits(rm) as u16;
2982 // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2983 // would overflow the 3-bit fields (#178/#180 class). Guard
2984 // and fall back to 32-bit SUBS.W for high registers.
2985 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2986 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2987 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2988 Ok(instr.to_le_bytes().to_vec())
2989 } else {
2990 self.encode_thumb32_subs_reg_raw(
2991 rd_bits as u32,
2992 rn_bits as u32,
2993 rm_bits as u32,
2994 )
2995 }
2996 } else {
2997 // 32-bit Thumb-2 SUBS with immediate
2998 self.encode_thumb32_subs(rd, rn, 0)
2999 }
3000 }
3001
3002 // SBC: Subtract with Carry (Thumb-2 32-bit)
3003 // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
3004 ArmOp::Sbc { rd, rn, op2 } => {
3005 let rd_bits = reg_to_bits(rd);
3006 let rn_bits = reg_to_bits(rn);
3007
3008 if let Operand2::Reg(rm) = op2 {
3009 let rm_bits = reg_to_bits(rm);
3010 // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3011 let hw1: u16 = (0xEB60 | rn_bits) as u16;
3012 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3013
3014 let mut bytes = hw1.to_le_bytes().to_vec();
3015 bytes.extend_from_slice(&hw2.to_le_bytes());
3016 Ok(bytes)
3017 } else {
3018 // SBC with immediate - use 32-bit encoding
3019 let hw1: u16 = (0xF160 | rn_bits) as u16;
3020 let hw2: u16 = (rd_bits << 8) as u16;
3021 let mut bytes = hw1.to_le_bytes().to_vec();
3022 bytes.extend_from_slice(&hw2.to_le_bytes());
3023 Ok(bytes)
3024 }
3025 }
3026
3027 // === 32-bit Thumb-2 encodings ===
3028
3029 // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3030 ArmOp::Sdiv { rd, rn, rm } => {
3031 let rd_bits = reg_to_bits(rd);
3032 let rn_bits = reg_to_bits(rn);
3033 let rm_bits = reg_to_bits(rm);
3034 reg_bits_checked(rd_bits)?;
3035 reg_bits_checked(rn_bits)?;
3036 reg_bits_checked(rm_bits)?;
3037
3038 // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3039 // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3040 // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3041 let hw1: u16 = (0xFB90 | rn_bits) as u16;
3042 let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3043
3044 // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3045 let mut bytes = hw1.to_le_bytes().to_vec();
3046 bytes.extend_from_slice(&hw2.to_le_bytes());
3047 encoding_contracts::verify_thumb32(&bytes);
3048 Ok(bytes)
3049 }
3050
3051 // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3052 ArmOp::Udiv { rd, rn, rm } => {
3053 let rd_bits = reg_to_bits(rd);
3054 let rn_bits = reg_to_bits(rn);
3055 let rm_bits = reg_to_bits(rm);
3056 reg_bits_checked(rd_bits)?;
3057 reg_bits_checked(rn_bits)?;
3058 reg_bits_checked(rm_bits)?;
3059
3060 // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3061 let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3062 let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3063
3064 let mut bytes = hw1.to_le_bytes().to_vec();
3065 bytes.extend_from_slice(&hw2.to_le_bytes());
3066 encoding_contracts::verify_thumb32(&bytes);
3067 Ok(bytes)
3068 }
3069
3070 ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3071 let rdlo_bits = reg_to_bits(rdlo);
3072 let rdhi_bits = reg_to_bits(rdhi);
3073 let rn_bits = reg_to_bits(rn);
3074 let rm_bits = reg_to_bits(rm);
3075 reg_bits_checked(rdlo_bits)?;
3076 reg_bits_checked(rdhi_bits)?;
3077 reg_bits_checked(rn_bits)?;
3078 reg_bits_checked(rm_bits)?;
3079
3080 // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3081 let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3082 let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3083
3084 let mut bytes = hw1.to_le_bytes().to_vec();
3085 bytes.extend_from_slice(&hw2.to_le_bytes());
3086 encoding_contracts::verify_thumb32(&bytes);
3087 Ok(bytes)
3088 }
3089
3090 // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3091 ArmOp::Mul { rd, rn, rm } => {
3092 let rd_bits = reg_to_bits(rd);
3093 let rn_bits = reg_to_bits(rn);
3094 let rm_bits = reg_to_bits(rm);
3095
3096 // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3097 // 11111011 0000 Rn | 1111 Rd 0000 Rm
3098 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3099 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3100
3101 let mut bytes = hw1.to_le_bytes().to_vec();
3102 bytes.extend_from_slice(&hw2.to_le_bytes());
3103 Ok(bytes)
3104 }
3105
3106 // MLS: Rd = Ra - Rn * Rm
3107 ArmOp::Mls { rd, rn, rm, ra } => {
3108 let rd_bits = reg_to_bits(rd);
3109 let rn_bits = reg_to_bits(rn);
3110 let rm_bits = reg_to_bits(rm);
3111 let ra_bits = reg_to_bits(ra);
3112
3113 // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3114 // 11111011 0000 Rn | Ra Rd 0001 Rm
3115 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3116 let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3117
3118 let mut bytes = hw1.to_le_bytes().to_vec();
3119 bytes.extend_from_slice(&hw2.to_le_bytes());
3120 Ok(bytes)
3121 }
3122
3123 ArmOp::Mla { rd, rn, rm, ra } => {
3124 let rd_bits = reg_to_bits(rd);
3125 let rn_bits = reg_to_bits(rn);
3126 let rm_bits = reg_to_bits(rm);
3127 let ra_bits = reg_to_bits(ra);
3128
3129 // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3130 // bit-4 (0x10) op flag. rd = ra + rn*rm.
3131 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3132 let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3133
3134 let mut bytes = hw1.to_le_bytes().to_vec();
3135 bytes.extend_from_slice(&hw2.to_le_bytes());
3136 Ok(bytes)
3137 }
3138
3139 // AND (Thumb-2 32-bit)
3140 ArmOp::And { rd, rn, op2 } => {
3141 if let Operand2::Reg(rm) = op2 {
3142 let rd_bits = reg_to_bits(rd);
3143 let rn_bits = reg_to_bits(rn);
3144 let rm_bits = reg_to_bits(rm);
3145
3146 // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3147 let hw1: u16 = (0xEA00 | rn_bits) as u16;
3148 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3149
3150 let mut bytes = hw1.to_le_bytes().to_vec();
3151 bytes.extend_from_slice(&hw2.to_le_bytes());
3152 Ok(bytes)
3153 } else if let Operand2::Imm(imm) = op2 {
3154 let rd_bits = reg_to_bits(rd);
3155 let rn_bits = reg_to_bits(rn);
3156
3157 // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3158 // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3159 // encode it correctly (or error on an un-encodable value)
3160 // rather than packing raw bits, closing the silent-miscompile
3161 // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3162 // CMP (#255).
3163 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3164 synth_core::Error::synthesis(
3165 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3166 )
3167 })?;
3168 let i_bit = (field >> 11) & 1;
3169 let imm3 = (field >> 8) & 0x7;
3170 let imm8 = field & 0xFF;
3171
3172 let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3173 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3174
3175 let mut bytes = hw1.to_le_bytes().to_vec();
3176 bytes.extend_from_slice(&hw2.to_le_bytes());
3177 Ok(bytes)
3178 } else {
3179 // RegShift variant - fallback to NOP
3180 let instr: u16 = 0xBF00;
3181 Ok(instr.to_le_bytes().to_vec())
3182 }
3183 }
3184
3185 // ORR (Thumb-2 32-bit)
3186 ArmOp::Orr { rd, rn, op2 } => {
3187 if let Operand2::Reg(rm) = op2 {
3188 let rd_bits = reg_to_bits(rd);
3189 let rn_bits = reg_to_bits(rn);
3190 let rm_bits = reg_to_bits(rm);
3191
3192 // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3193 let hw1: u16 = (0xEA40 | rn_bits) as u16;
3194 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3195
3196 let mut bytes = hw1.to_le_bytes().to_vec();
3197 bytes.extend_from_slice(&hw2.to_le_bytes());
3198 Ok(bytes)
3199 } else if let Operand2::Imm(imm) = op2 {
3200 // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3201 // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3202 // larger modified immediates need ThumbExpandImm — return an
3203 // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3204 let imm_val = *imm as u32;
3205 if imm_val > 0xFF {
3206 return Err(synth_core::Error::synthesis(
3207 "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3208 ));
3209 }
3210 let rd_bits = reg_to_bits(rd);
3211 let rn_bits = reg_to_bits(rn);
3212 let hw1: u16 = (0xF040 | rn_bits) as u16;
3213 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3214 let mut bytes = hw1.to_le_bytes().to_vec();
3215 bytes.extend_from_slice(&hw2.to_le_bytes());
3216 Ok(bytes)
3217 } else {
3218 let instr: u16 = 0xBF00;
3219 Ok(instr.to_le_bytes().to_vec())
3220 }
3221 }
3222
3223 // EOR (Thumb-2 32-bit)
3224 ArmOp::Eor { rd, rn, op2 } => {
3225 if let Operand2::Reg(rm) = op2 {
3226 let rd_bits = reg_to_bits(rd);
3227 let rn_bits = reg_to_bits(rn);
3228 let rm_bits = reg_to_bits(rm);
3229
3230 // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3231 let hw1: u16 = (0xEA80 | rn_bits) as u16;
3232 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3233
3234 let mut bytes = hw1.to_le_bytes().to_vec();
3235 bytes.extend_from_slice(&hw2.to_le_bytes());
3236 Ok(bytes)
3237 } else if let Operand2::Imm(imm) = op2 {
3238 // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3239 // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3240 // error, not a silent NOP (Ok-or-Err, #180/#185).
3241 let imm_val = *imm as u32;
3242 if imm_val > 0xFF {
3243 return Err(synth_core::Error::synthesis(
3244 "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3245 ));
3246 }
3247 let rd_bits = reg_to_bits(rd);
3248 let rn_bits = reg_to_bits(rn);
3249 let hw1: u16 = (0xF080 | rn_bits) as u16;
3250 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3251 let mut bytes = hw1.to_le_bytes().to_vec();
3252 bytes.extend_from_slice(&hw2.to_le_bytes());
3253 Ok(bytes)
3254 } else {
3255 let instr: u16 = 0xBF00;
3256 Ok(instr.to_le_bytes().to_vec())
3257 }
3258 }
3259
3260 // Shift operations (16-bit for low registers)
3261 ArmOp::Lsl { rd, rn, shift } => {
3262 let rd_bits = reg_to_bits(rd) as u16;
3263 let rn_bits = reg_to_bits(rn) as u16;
3264 let shift_bits = (*shift as u16) & 0x1F;
3265
3266 if rd_bits < 8 && rn_bits < 8 {
3267 // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3268 let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3269 Ok(instr.to_le_bytes().to_vec())
3270 } else {
3271 // Use 32-bit encoding for high registers
3272 self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3273 }
3274 }
3275
3276 ArmOp::Lsr { rd, rn, shift } => {
3277 let rd_bits = reg_to_bits(rd) as u16;
3278 let rn_bits = reg_to_bits(rn) as u16;
3279 let shift_bits = (*shift as u16) & 0x1F;
3280
3281 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3282 // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3283 let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3284 Ok(instr.to_le_bytes().to_vec())
3285 } else {
3286 self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3287 }
3288 }
3289
3290 ArmOp::Asr { rd, rn, shift } => {
3291 let rd_bits = reg_to_bits(rd) as u16;
3292 let rn_bits = reg_to_bits(rn) as u16;
3293 let shift_bits = (*shift as u16) & 0x1F;
3294
3295 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3296 // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3297 let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3298 Ok(instr.to_le_bytes().to_vec())
3299 } else {
3300 self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3301 }
3302 }
3303
3304 ArmOp::Ror { rd, rn, shift } => {
3305 // ROR doesn't have a 16-bit immediate form, use 32-bit
3306 self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3307 }
3308
3309 // Register-based shifts (Thumb-2 32-bit)
3310 // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3311 // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3312 ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3313 ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3314 ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3315 ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3316
3317 // RSB (Reverse Subtract): Rd = imm - Rn
3318 // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3319 ArmOp::Rsb { rd, rn, imm } => {
3320 let rd_bits = reg_to_bits(rd);
3321 let rn_bits = reg_to_bits(rn);
3322
3323 // #681 class audit: the T2 `i:imm3:imm8` field is a
3324 // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3325 // (T4-style) form — packing a raw value > 0xFF silently encodes
3326 // a different constant (#253/#255 class). All current emitters
3327 // use imm 32 (shift complement), which expands to itself, so
3328 // this gate is byte-identical for existing codegen.
3329 let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3330 synth_core::Error::synthesis(
3331 "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3332 )
3333 })?;
3334 let i_bit = (field >> 11) & 1;
3335 let imm3 = (field >> 8) & 0x7;
3336 let imm8 = field & 0xFF;
3337
3338 // hw1: 11110 i 01110 0 Rn (S=0)
3339 let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3340 // hw2: 0 imm3 Rd imm8
3341 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3342
3343 let mut bytes = hw1.to_le_bytes().to_vec();
3344 bytes.extend_from_slice(&hw2.to_le_bytes());
3345 Ok(bytes)
3346 }
3347
3348 // CLZ (Thumb-2 32-bit)
3349 ArmOp::Clz { rd, rm } => {
3350 let rd_bits = reg_to_bits(rd);
3351 let rm_bits = reg_to_bits(rm);
3352
3353 // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3354 // 11111010 1011 Rm | 1111 1000 Rd Rm
3355 let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3356 let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3357
3358 let mut bytes = hw1.to_le_bytes().to_vec();
3359 bytes.extend_from_slice(&hw2.to_le_bytes());
3360 Ok(bytes)
3361 }
3362
3363 // RBIT (Thumb-2 32-bit)
3364 ArmOp::Rbit { rd, rm } => {
3365 let rd_bits = reg_to_bits(rd);
3366 let rm_bits = reg_to_bits(rm);
3367
3368 // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3369 // 11111010 1001 Rm | 1111 Rd 1010 Rm
3370 let hw1: u16 = (0xFA90 | rm_bits) as u16;
3371 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3372
3373 let mut bytes = hw1.to_le_bytes().to_vec();
3374 bytes.extend_from_slice(&hw2.to_le_bytes());
3375 Ok(bytes)
3376 }
3377
3378 // SXTB (16-bit for low registers)
3379 ArmOp::Sxtb { rd, rm } => {
3380 let rd_bits = reg_to_bits(rd) as u16;
3381 let rm_bits = reg_to_bits(rm) as u16;
3382
3383 if rd_bits < 8 && rm_bits < 8 {
3384 // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3385 let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3386 Ok(instr.to_le_bytes().to_vec())
3387 } else {
3388 // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3389 // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3390 let rd_bits32 = rd_bits as u32;
3391 let rm_bits32 = rm_bits as u32;
3392 let hw1: u16 = 0xFA4F;
3393 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3394 let mut bytes = hw1.to_le_bytes().to_vec();
3395 bytes.extend_from_slice(&hw2.to_le_bytes());
3396 Ok(bytes)
3397 }
3398 }
3399
3400 // SXTH (16-bit for low registers)
3401 ArmOp::Sxth { rd, rm } => {
3402 let rd_bits = reg_to_bits(rd) as u16;
3403 let rm_bits = reg_to_bits(rm) as u16;
3404
3405 if rd_bits < 8 && rm_bits < 8 {
3406 // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3407 let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3408 Ok(instr.to_le_bytes().to_vec())
3409 } else {
3410 // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3411 // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3412 let rd_bits32 = rd_bits as u32;
3413 let rm_bits32 = rm_bits as u32;
3414 let hw1: u16 = 0xFA0F;
3415 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3416 let mut bytes = hw1.to_le_bytes().to_vec();
3417 bytes.extend_from_slice(&hw2.to_le_bytes());
3418 Ok(bytes)
3419 }
3420 }
3421
3422 // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3423 ArmOp::Uxtb { rd, rm } => {
3424 let rd_bits = reg_to_bits(rd) as u16;
3425 let rm_bits = reg_to_bits(rm) as u16;
3426 if rd_bits < 8 && rm_bits < 8 {
3427 // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3428 let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3429 Ok(instr.to_le_bytes().to_vec())
3430 } else {
3431 // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3432 let hw1: u16 = 0xFA5F;
3433 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3434 let mut bytes = hw1.to_le_bytes().to_vec();
3435 bytes.extend_from_slice(&hw2.to_le_bytes());
3436 Ok(bytes)
3437 }
3438 }
3439
3440 // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3441 ArmOp::Uxth { rd, rm } => {
3442 let rd_bits = reg_to_bits(rd) as u16;
3443 let rm_bits = reg_to_bits(rm) as u16;
3444 if rd_bits < 8 && rm_bits < 8 {
3445 // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3446 let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3447 Ok(instr.to_le_bytes().to_vec())
3448 } else {
3449 // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3450 let hw1: u16 = 0xFA1F;
3451 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3452 let mut bytes = hw1.to_le_bytes().to_vec();
3453 bytes.extend_from_slice(&hw2.to_le_bytes());
3454 Ok(bytes)
3455 }
3456 }
3457
3458 // CMP (can be 16-bit for low registers)
3459 ArmOp::Cmp { rn, op2 } => {
3460 let rn_bits = reg_to_bits(rn) as u16;
3461
3462 if let Operand2::Imm(imm) = op2 {
3463 // Only use 16-bit encoding for non-negative immediates 0-255
3464 // Negative immediates must use 32-bit encoding
3465 if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3466 // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3467 let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3468 Ok(instr.to_le_bytes().to_vec())
3469 } else {
3470 self.encode_thumb32_cmp_imm(rn, *imm as u32)
3471 }
3472 } else if let Operand2::Reg(rm) = op2 {
3473 let rm_bits = reg_to_bits(rm) as u16;
3474 if rn_bits < 8 && rm_bits < 8 {
3475 // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3476 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3477 Ok(instr.to_le_bytes().to_vec())
3478 } else {
3479 // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3480 let n_bit = (rn_bits >> 3) & 1;
3481 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3482 Ok(instr.to_le_bytes().to_vec())
3483 }
3484 } else {
3485 let instr: u16 = 0xBF00;
3486 Ok(instr.to_le_bytes().to_vec())
3487 }
3488 }
3489
3490 // CMN (Compare Negative) - computes Rn + op2 and sets flags
3491 // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3492 ArmOp::Cmn { rn, op2 } => {
3493 let rn_bits = reg_to_bits(rn) as u16;
3494
3495 if let Operand2::Imm(imm) = op2 {
3496 // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3497 // modified immediate (the field sits in imm3=hw2[14:12],
3498 // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3499 // an un-encodable value — replacing the old silent `0xBF00`
3500 // NOP (the last of the silent-miscompile data-proc encoders).
3501 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3502 synth_core::Error::synthesis(
3503 "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3504 )
3505 })?;
3506 let i_bit = (field >> 11) & 1;
3507 let imm3 = (field >> 8) & 0x7;
3508 let imm8 = field & 0xFF;
3509 let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3510 let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3511 let mut bytes = hw1.to_le_bytes().to_vec();
3512 bytes.extend_from_slice(&hw2.to_le_bytes());
3513 Ok(bytes)
3514 } else if let Operand2::Reg(rm) = op2 {
3515 let rm_bits = reg_to_bits(rm) as u16;
3516 // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3517 // the 3-bit fields and corrupt the operands (#184, the #180
3518 // class). CMN has no high-register 16-bit form, so fall back
3519 // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3520 // Rd discarded as PC/1111).
3521 if rn_bits < 8 && rm_bits < 8 {
3522 // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3523 let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3524 Ok(instr.to_le_bytes().to_vec())
3525 } else {
3526 let hw1: u16 = 0xEB10 | rn_bits;
3527 let hw2: u16 = 0x0F00 | rm_bits;
3528 let mut bytes = hw1.to_le_bytes().to_vec();
3529 bytes.extend_from_slice(&hw2.to_le_bytes());
3530 Ok(bytes)
3531 }
3532 } else {
3533 Ok(vec![0xBF, 0x00])
3534 }
3535 }
3536
3537 // LDR (can be 16-bit for simple cases)
3538 ArmOp::Ldr { rd, addr } => {
3539 let rd_bits = reg_to_bits(rd);
3540 let base_bits = reg_to_bits(&addr.base);
3541
3542 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3543 if let Some(offset_reg) = &addr.offset_reg {
3544 let rm_bits = reg_to_bits(offset_reg);
3545
3546 // If there's also an immediate offset, we need to ADD it first
3547 if addr.offset != 0 {
3548 // Use R12 (IP) as scratch to avoid clobbering the address register
3549 // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3550 let scratch = Reg::R12;
3551 let mut bytes =
3552 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3553 bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3554 return Ok(bytes);
3555 }
3556
3557 // Simple register offset: LDR Rd, [Rn, Rm]
3558 // 16-bit: only if Rd, Rn, Rm < R8
3559 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3560 // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3561 let instr: u16 = 0x5800
3562 | ((rm_bits as u16) << 6)
3563 | ((base_bits as u16) << 3)
3564 | (rd_bits as u16);
3565 return Ok(instr.to_le_bytes().to_vec());
3566 }
3567
3568 // 32-bit register offset
3569 return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3570 }
3571
3572 // Immediate offset mode [base, #imm]
3573 let offset = addr.offset as u32;
3574
3575 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3576 // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3577 let imm5 = (offset >> 2) as u16;
3578 let instr: u16 =
3579 0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3580 Ok(instr.to_le_bytes().to_vec())
3581 } else {
3582 self.encode_thumb32_ldr(rd, &addr.base, offset)
3583 }
3584 }
3585
3586 // STR (can be 16-bit for simple cases)
3587 ArmOp::Str { rd, addr } => {
3588 let rd_bits = reg_to_bits(rd);
3589 let base_bits = reg_to_bits(&addr.base);
3590
3591 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3592 if let Some(offset_reg) = &addr.offset_reg {
3593 let rm_bits = reg_to_bits(offset_reg);
3594
3595 // If there's also an immediate offset, we need to ADD it first
3596 if addr.offset != 0 {
3597 // Use R12 (IP) as scratch to avoid clobbering the address register
3598 // ADD R12, Rm, #offset; STR Rd, [base, R12]
3599 let scratch = Reg::R12;
3600 let mut bytes =
3601 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3602 bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3603 return Ok(bytes);
3604 }
3605
3606 // Simple register offset: STR Rd, [Rn, Rm]
3607 // 16-bit: only if Rd, Rn, Rm < R8
3608 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3609 // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3610 let instr: u16 = 0x5000
3611 | ((rm_bits as u16) << 6)
3612 | ((base_bits as u16) << 3)
3613 | (rd_bits as u16);
3614 return Ok(instr.to_le_bytes().to_vec());
3615 }
3616
3617 // 32-bit register offset
3618 return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3619 }
3620
3621 // Immediate offset mode [base, #imm]
3622 let offset = addr.offset as u32;
3623
3624 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3625 // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3626 let imm5 = (offset >> 2) as u16;
3627 let instr: u16 =
3628 0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3629 Ok(instr.to_le_bytes().to_vec())
3630 } else {
3631 self.encode_thumb32_str(rd, &addr.base, offset)
3632 }
3633 }
3634
3635 // LDRB (Thumb-2)
3636 ArmOp::Ldrb { rd, addr } => {
3637 let rd_bits = reg_to_bits(rd);
3638 let base_bits = reg_to_bits(&addr.base);
3639
3640 if let Some(offset_reg) = &addr.offset_reg {
3641 if addr.offset != 0 {
3642 let scratch = Reg::R12;
3643 let mut bytes =
3644 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3645 bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3646 return Ok(bytes);
3647 }
3648 return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3649 }
3650
3651 let offset = addr.offset as u32;
3652 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3653 // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3654 let instr: u16 = 0x7800
3655 | ((offset as u16) << 6)
3656 | ((base_bits as u16) << 3)
3657 | (rd_bits as u16);
3658 Ok(instr.to_le_bytes().to_vec())
3659 } else {
3660 self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3661 }
3662 }
3663
3664 // LDRSB (Thumb-2)
3665 ArmOp::Ldrsb { rd, addr } => {
3666 let rd_bits = reg_to_bits(rd);
3667 let base_bits = reg_to_bits(&addr.base);
3668
3669 if let Some(offset_reg) = &addr.offset_reg {
3670 if addr.offset != 0 {
3671 let scratch = Reg::R12;
3672 let mut bytes =
3673 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3674 bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3675 return Ok(bytes);
3676 }
3677 return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3678 }
3679
3680 let offset = addr.offset as u32;
3681 // LDRSB has no 16-bit immediate form (only register)
3682 // For 16-bit reg form: only if Rd, Rn, Rm < R8
3683 if rd_bits < 8 && base_bits < 8 && offset == 0 {
3684 // No immediate 16-bit encoding for LDRSB; use 32-bit
3685 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3686 } else {
3687 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3688 }
3689 }
3690
3691 // LDRH (Thumb-2)
3692 ArmOp::Ldrh { rd, addr } => {
3693 let rd_bits = reg_to_bits(rd);
3694 let base_bits = reg_to_bits(&addr.base);
3695
3696 if let Some(offset_reg) = &addr.offset_reg {
3697 if addr.offset != 0 {
3698 let scratch = Reg::R12;
3699 let mut bytes =
3700 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3701 bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3702 return Ok(bytes);
3703 }
3704 return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3705 }
3706
3707 let offset = addr.offset as u32;
3708 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3709 // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3710 let imm5 = (offset >> 1) as u16;
3711 let instr: u16 =
3712 0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3713 Ok(instr.to_le_bytes().to_vec())
3714 } else {
3715 self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3716 }
3717 }
3718
3719 // LDRSH (Thumb-2)
3720 ArmOp::Ldrsh { rd, addr } => {
3721 if let Some(offset_reg) = &addr.offset_reg {
3722 if addr.offset != 0 {
3723 let scratch = Reg::R12;
3724 let mut bytes =
3725 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3726 bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3727 return Ok(bytes);
3728 }
3729 return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3730 }
3731
3732 let offset = addr.offset as u32;
3733 self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3734 }
3735
3736 // STRB (Thumb-2)
3737 ArmOp::Strb { rd, addr } => {
3738 let rd_bits = reg_to_bits(rd);
3739 let base_bits = reg_to_bits(&addr.base);
3740
3741 if let Some(offset_reg) = &addr.offset_reg {
3742 if addr.offset != 0 {
3743 let scratch = Reg::R12;
3744 let mut bytes =
3745 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3746 bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3747 return Ok(bytes);
3748 }
3749 return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3750 }
3751
3752 let offset = addr.offset as u32;
3753 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3754 // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3755 let instr: u16 = 0x7000
3756 | ((offset as u16) << 6)
3757 | ((base_bits as u16) << 3)
3758 | (rd_bits as u16);
3759 Ok(instr.to_le_bytes().to_vec())
3760 } else {
3761 self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3762 }
3763 }
3764
3765 // STRH (Thumb-2)
3766 ArmOp::Strh { rd, addr } => {
3767 let rd_bits = reg_to_bits(rd);
3768 let base_bits = reg_to_bits(&addr.base);
3769
3770 if let Some(offset_reg) = &addr.offset_reg {
3771 if addr.offset != 0 {
3772 let scratch = Reg::R12;
3773 let mut bytes =
3774 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3775 bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3776 return Ok(bytes);
3777 }
3778 return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3779 }
3780
3781 let offset = addr.offset as u32;
3782 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3783 // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3784 let imm5 = (offset >> 1) as u16;
3785 let instr: u16 =
3786 0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3787 Ok(instr.to_le_bytes().to_vec())
3788 } else {
3789 self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3790 }
3791 }
3792
3793 // MemorySize (Thumb-2)
3794 ArmOp::MemorySize { rd } => {
3795 // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3796 // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3797 let rd_bits = reg_to_bits(rd);
3798 let r10_bits = reg_to_bits(&Reg::R10);
3799 if rd_bits < 8 && r10_bits < 8 {
3800 let instr: u16 =
3801 0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3802 Ok(instr.to_le_bytes().to_vec())
3803 } else {
3804 // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3805 let imm5: u32 = 16;
3806 let imm3 = (imm5 >> 2) & 0x7;
3807 let imm2 = imm5 & 0x3;
3808 let hw1: u16 = 0xEA4F;
3809 let hw2: u16 =
3810 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3811 let mut bytes = hw1.to_le_bytes().to_vec();
3812 bytes.extend_from_slice(&hw2.to_le_bytes());
3813 Ok(bytes)
3814 }
3815 }
3816
3817 // MemoryGrow (Thumb-2)
3818 ArmOp::MemoryGrow { rd, .. } => {
3819 // On embedded with fixed memory, always return -1 (failure)
3820 // MVN rd, #0 → MOV rd, #-1
3821 // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3822 let rd_bits = reg_to_bits(rd);
3823 let hw1: u16 = 0xF06F; // MVN with i=0
3824 let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3825 let mut bytes = hw1.to_le_bytes().to_vec();
3826 bytes.extend_from_slice(&hw2.to_le_bytes());
3827 Ok(bytes)
3828 }
3829
3830 // BX (16-bit)
3831 ArmOp::Bx { rm } => {
3832 let rm_bits = reg_to_bits(rm) as u16;
3833 // BX Rm (16-bit): 0100 0111 0 Rm 000
3834 let instr: u16 = 0x4700 | (rm_bits << 3);
3835 Ok(instr.to_le_bytes().to_vec())
3836 }
3837
3838 // BLX (16-bit) - Branch with Link and Exchange
3839 // BLX Rm: 0100 0111 1 Rm 000
3840 ArmOp::Blx { rm } => {
3841 let rm_bits = reg_to_bits(rm) as u16;
3842 let instr: u16 = 0x4780 | (rm_bits << 3);
3843 Ok(instr.to_le_bytes().to_vec())
3844 }
3845
3846 // CallIndirect - indirect function call via table lookup
3847 // table_index_reg contains the table index
3848 // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3849 // UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3850 // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3851 // R11 region): the pointer load becomes
3852 // ADD R12,R11,R12; LDR R12,[R12,#offset]
3853 // #664, null_check (the table has null slots, linked as ZERO
3854 // words): the loaded pointer is null-checked before the BLX —
3855 // CMP.W R12,#0; BNE +1; UDF #0
3856 // #676, type_check (heterogeneous table — runtime §4.4.8 type
3857 // check against the type-id sidecar at R11+off): after the
3858 // bounds guard —
3859 // LSL R12,idx,#2; ADD R12,R11,R12;
3860 // LDR R12,[R12,#type_off]; CMP.W R12,#id;
3861 // BEQ +1; UDF #0
3862 // (the dispatch tail then recomputes idx*4 — idx stays live).
3863 ArmOp::CallIndirect {
3864 rd: _,
3865 type_idx: _,
3866 table_index_reg,
3867 table_size,
3868 table_byte_offset,
3869 null_check,
3870 type_check,
3871 } => {
3872 let idx_reg = reg_to_bits(table_index_reg);
3873 let mut bytes = Vec::new();
3874
3875 // The expansion:
3876 // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3877 // index >= table size. Without it an out-of-bounds index
3878 // reads past the table and BLXes whatever word lies there —
3879 // an uncontrolled indirect branch instead of a trap.
3880 // 2. Multiplies index by 4 (function pointer size)
3881 // 3. Loads function pointer from table (table base in R11)
3882 // 4. Calls the function via BLX
3883 //
3884 // Table base setup must be done by caller/runtime. The type
3885 // check §4.4.8 also requires is discharged at COMPILE time:
3886 // the selector only emits this op after verifying the closed-
3887 // world property that every table entry's signature equals the
3888 // expected type (the raw code-pointer table carries no runtime
3889 // type ids to compare) — see the #642 selector guard.
3890
3891 // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3892 // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3893 let size_lo = *table_size & 0xFFFF;
3894 let hw1: u16 =
3895 (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3896 let hw2: u16 =
3897 ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3898 bytes.extend_from_slice(&hw1.to_le_bytes());
3899 bytes.extend_from_slice(&hw2.to_le_bytes());
3900 // MOVT R12, #(size >> 16) — only when the table size exceeds
3901 // 16 bits (never in practice, but the guard must not compare
3902 // against a truncated size).
3903 let size_hi = *table_size >> 16;
3904 if size_hi != 0 {
3905 let hw1: u16 =
3906 (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3907 let hw2: u16 =
3908 ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3909 bytes.extend_from_slice(&hw1.to_le_bytes());
3910 bytes.extend_from_slice(&hw2.to_le_bytes());
3911 }
3912 // CMP idx, R12 — 16-bit T2 (high-register capable):
3913 // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3914 let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3915 bytes.extend_from_slice(&cmp.to_le_bytes());
3916 // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3917 // imm8=0: target = branch + 4. LO = unsigned lower.
3918 bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3919 // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3920 // the div-by-zero guards).
3921 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3922
3923 // #676: runtime type check — ONLY for a heterogeneous table
3924 // (mixed signatures, closed-world verdict impossible). Load
3925 // the indexed slot's structural class id from the type-id
3926 // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3927 // base + this table's base offset, a compile-time constant)
3928 // and compare it against the expected type's class id — a
3929 // mismatch is the WASM Core §4.4.8 type trap. Null slots
3930 // carry the reserved id 0, so this compare subsumes the
3931 // #664 null trap (the selector passes `null_check: false`).
3932 // `None` emits NOTHING: every homogeneous table keeps the
3933 // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3934 // only scratch (#212); the dispatch tail below recomputes
3935 // idx*4 — the index register is never clobbered here.
3936 if let Some((expected_id, type_off)) = type_check {
3937 // RQ-61-IMMRANGE (#1072): compiled out in release, where
3938 // the masks below silently TRUNCATE (id 256 compares as
3939 // 0 — a NULL slot would pass the §4.4.8 check). The
3940 // enforcement claim is DEMONSTRATED: the sole `Some`
3941 // producer, `resolve_runtime_type_check`, loud-declines
3942 // id > 255 / offset > 4095, tripped by
3943 // `test_676_call_indirect_runtime_check_range_declines`
3944 // (mutation-checked — see the A32 twin above).
3945 debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3946 debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3947 // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3948 // tail's index scale below).
3949 bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3950 bytes.extend_from_slice(
3951 &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3952 );
3953 // ADD.W R12, R11, R12 (the #650 base-add form).
3954 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3955 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3956 // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3957 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3958 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3959 bytes.extend_from_slice(
3960 &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3961 );
3962 // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3963 // (same form as the #664 null check's CMP.W R12, #0).
3964 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3965 bytes.extend_from_slice(
3966 &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
3967 );
3968 // BEQ +1 insn (skip the UDF when the class id matches) —
3969 // B<cond>.N imm8=0: target = branch + 4. EQ.
3970 bytes.extend_from_slice(&0xD000u16.to_le_bytes());
3971 // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
3972 // the bounds guard above).
3973 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3974 }
3975
3976 // LSL R12, idx_reg, #2 (multiply index by 4)
3977 // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3978 // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3979 // #597: the shift amount was previously shifted into bits 5:4 —
3980 // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3981 // destroyed the index and dispatched table entry 0 for every
3982 // call. imm2 lives at bits 7:6.
3983 let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3984 let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3985 bytes.extend_from_slice(&hw1.to_le_bytes());
3986 bytes.extend_from_slice(&hw2.to_le_bytes());
3987
3988 if *table_byte_offset == 0 {
3989 // Table 0 (base = R11 itself): the pre-#650 single-load
3990 // form — a single-table module's bytes stay identical BY
3991 // CONSTRUCTION.
3992 // LDR R12, [R11, R12] - load function pointer
3993 // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3994 // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3995 let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3996 let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3997 bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3998 bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3999 } else {
4000 // #650: table N of the contiguous R11 region — fold the
4001 // compile-time base offset into the pointer load via the
4002 // LDR imm12 form (R12 stays the only scratch, per the
4003 // #212 convention).
4004 assert!(
4005 *table_byte_offset <= 4095,
4006 "call_indirect table base offset {table_byte_offset} exceeds \
4007 LDR imm12 — the selector must have declined this (#650)"
4008 );
4009 // ADD.W R12, R11, R12 — T3 ADD (register):
4010 // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
4011 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4012 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4013 // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4014 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4015 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4016 bytes.extend_from_slice(
4017 &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4018 );
4019 }
4020
4021 // #664: null-slot trap — ONLY when the table image carries
4022 // null (uninitialized) slots, which the layout contract
4023 // requires to be linked as ZERO words. A fully-initialized
4024 // table skips this branch entirely, keeping the pre-#664
4025 // expansion byte-identical BY CONSTRUCTION (the #650
4026 // offset-0 trick).
4027 if *null_check {
4028 // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4029 // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4030 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4031 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4032 // BNE +1 insn (skip the UDF when the pointer is non-null)
4033 // — B<cond>.N imm8=0: target = branch + 4. NE.
4034 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4035 // UDF #0 — call_indirect null-funcref trap (WASM Core
4036 // §4.4.8: calling an uninitialized element traps; same
4037 // trap idiom as the bounds guard above).
4038 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4039 }
4040
4041 // BLX R12 (call function indirectly)
4042 // BLX Rm (16-bit): 0100 0111 1 Rm 000
4043 let blx: u16 = 0x47E0; // BLX R12
4044 bytes.extend_from_slice(&blx.to_le_bytes());
4045
4046 Ok(bytes)
4047 }
4048
4049 // Label pseudo-instruction: emits no machine code
4050 ArmOp::Label { .. } => Ok(Vec::new()),
4051
4052 // Conditional branch to label (generic) - offset 0, will be patched
4053 ArmOp::Bcc { cond, label: _ } => {
4054 use synth_synthesis::Condition;
4055 let cond_bits: u16 = match cond {
4056 Condition::EQ => 0x0,
4057 Condition::NE => 0x1,
4058 Condition::HS => 0x2,
4059 Condition::LO => 0x3,
4060 Condition::HI => 0x8,
4061 Condition::LS => 0x9,
4062 Condition::GE => 0xA,
4063 Condition::LT => 0xB,
4064 Condition::GT => 0xC,
4065 Condition::LE => 0xD,
4066 };
4067 // 16-bit B<cond> with offset 0: 1101 cond imm8
4068 let instr: u16 = 0xD000 | (cond_bits << 8);
4069 Ok(instr.to_le_bytes().to_vec())
4070 }
4071
4072 // Branch instructions
4073 ArmOp::B { label: _ } => {
4074 // Simplified: B.N with offset 0
4075 // For real usage, would need label resolution
4076 let instr: u16 = 0xE000; // B.N #0
4077 Ok(instr.to_le_bytes().to_vec())
4078 }
4079
4080 // BHS (Branch if Higher or Same) - used for bounds checking
4081 // Condition code: 0x2 (C set)
4082 ArmOp::Bhs { label: _ } => {
4083 // 16-bit B<cond> with offset 0: 1101 cond imm8
4084 // cond = 0x2 (HS)
4085 let instr: u16 = 0xD200; // BHS.N #0
4086 Ok(instr.to_le_bytes().to_vec())
4087 }
4088
4089 // BLO (Branch if Lower) - complementary to BHS
4090 // Condition code: 0x3 (C clear)
4091 ArmOp::Blo { label: _ } => {
4092 // 16-bit B<cond> with offset 0: 1101 cond imm8
4093 // cond = 0x3 (LO)
4094 let instr: u16 = 0xD300; // BLO.N #0
4095 Ok(instr.to_le_bytes().to_vec())
4096 }
4097
4098 // Branch with numeric offset (Thumb-2)
4099 // Thumb-2 B.W instruction: 32-bit with +-16MB range
4100 ArmOp::BOffset { offset } => {
4101 // offset is already the halfword displacement: (target - branch - 4) / 2
4102 // This is the raw encoded value, accounting for variable-length instructions
4103 let halfword_offset = *offset;
4104
4105 // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4106 // Range: -1024 to +1022 halfwords
4107 if (-1024..=1022).contains(&halfword_offset) {
4108 // 16-bit B.N encoding: 1110 0 imm11
4109 let imm11 = (halfword_offset as u16) & 0x7FF;
4110 let instr: u16 = 0xE000 | imm11;
4111 Ok(instr.to_le_bytes().to_vec())
4112 } else {
4113 // 32-bit B.W encoding for larger offsets
4114 // First halfword: 1111 0 S imm10
4115 // Second halfword: 10 J1 0 J2 imm11
4116 // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4117 // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4118
4119 // The B.W (T4) encoding packs the signed offset as:
4120 // S:I1:I2:imm10:imm11:0 (25-bit signed, halfword-aligned)
4121 // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4122 // Input halfword_offset already equals (target - PC - 4) / 2,
4123 // so the full byte offset = halfword_offset << 1.
4124 // The encoding fields split that 25-bit signed value (including the
4125 // implicit trailing zero) as: S | imm10 | imm11
4126 // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4127 let signed_offset = halfword_offset << 1; // byte offset
4128 let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4129 let uoffset = signed_offset as u32;
4130 let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4131 let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4132 let i1 = (uoffset >> 23) & 1; // bit 23
4133 let i2 = (uoffset >> 22) & 1; // bit 22
4134 let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4135 let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4136
4137 let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4138 let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4139
4140 let mut bytes = hw1.to_le_bytes().to_vec();
4141 bytes.extend_from_slice(&hw2.to_le_bytes());
4142 Ok(bytes)
4143 }
4144 }
4145
4146 // Conditional branch with numeric offset (Thumb-2)
4147 ArmOp::BCondOffset { cond, offset } => {
4148 use synth_synthesis::Condition;
4149 let cond_bits: u16 = match cond {
4150 Condition::EQ => 0x0,
4151 Condition::NE => 0x1,
4152 Condition::HS => 0x2,
4153 Condition::LO => 0x3,
4154 Condition::HI => 0x8,
4155 Condition::LS => 0x9,
4156 Condition::GE => 0xA,
4157 Condition::LT => 0xB,
4158 Condition::GT => 0xC,
4159 Condition::LE => 0xD,
4160 };
4161
4162 // offset is already the halfword displacement: (target - branch - 4) / 2
4163 // This is the raw imm8 value for 16-bit B<cond> encoding
4164 let halfword_offset = *offset;
4165
4166 // 16-bit B<cond> encoding: 1101 cond imm8
4167 // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4168 if (-128..=127).contains(&halfword_offset) {
4169 let imm8 = (halfword_offset as u16) & 0xFF;
4170 let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4171 Ok(instr.to_le_bytes().to_vec())
4172 } else {
4173 // 32-bit B<cond>.W (encoding T3) for larger offsets
4174 // First halfword: 1111 0 S cond(4) imm6
4175 // Second halfword: 10 J1 0 J2 imm11
4176 //
4177 // Per ARMv7-M, the branch BYTE offset is
4178 // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4179 // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4180 // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4181 // [16:11], 17, 18 and 19 directly (mirroring the T4
4182 // unconditional arm above).
4183 //
4184 // #740: this arm previously packed `halfword_offset >> 1`
4185 // into imm6:imm11 — HALVING the displacement — so every
4186 // wide conditional branch (span > 254 bytes) landed at half
4187 // its intended offset: gust_poll's loop-head `br_if` to an
4188 // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4189 // encodings were unaffected, which is why short-range CF
4190 // fixtures never caught it.
4191 if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4192 return Err(synth_core::Error::synthesis(format!(
4193 "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4194 the signed 20-bit encoding range (±1 MB) — refusing to \
4195 emit a truncated branch"
4196 )));
4197 }
4198 let u = halfword_offset as u32;
4199 let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4200 let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4201 let j1 = (u >> 17) & 1; // bit 17
4202 let j2 = (u >> 18) & 1; // bit 18
4203 let s = (u >> 19) & 1; // sign (range-checked above)
4204
4205 let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4206 let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4207
4208 let mut bytes = hw1.to_le_bytes().to_vec();
4209 bytes.extend_from_slice(&hw2.to_le_bytes());
4210 Ok(bytes)
4211 }
4212 }
4213
4214 ArmOp::Bl { label: _ } => {
4215 // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4216 // placeholder; an R_ARM_THM_CALL relocation patches the target
4217 // (see arm_backend.rs). The placeholder must carry an embedded
4218 // addend of -4 so the relocation nets to exactly the symbol S.
4219 //
4220 // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4221 // R_ARM_THM_CALL the linker resolves using the in-place addend;
4222 // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4223 // instruction past the callee entry (#174). The correct
4224 // placeholder is what `gas` emits for `bl <extern>`:
4225 // f7ff fffe -> `bl <self>` (S=1, J1=J2=1, imm = -4 addend),
4226 // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4227 // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4228 // the garbage `bl c0000c` and "truncated to fit" of #167.)
4229 let hw1: u16 = 0xF7FF;
4230 let hw2: u16 = 0xFFFE;
4231 let mut bytes = hw1.to_le_bytes().to_vec();
4232 bytes.extend_from_slice(&hw2.to_le_bytes());
4233 Ok(bytes)
4234 }
4235
4236 // MVN
4237 ArmOp::Mvn { rd, op2 } => {
4238 if let Operand2::Reg(rm) = op2 {
4239 let rd_bits = reg_to_bits(rd) as u16;
4240 let rm_bits = reg_to_bits(rm) as u16;
4241
4242 if rd_bits < 8 && rm_bits < 8 {
4243 // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4244 let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4245 Ok(instr.to_le_bytes().to_vec())
4246 } else {
4247 // 32-bit MVN
4248 let hw1: u16 = 0xEA6F_u16;
4249 let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4250 let mut bytes = hw1.to_le_bytes().to_vec();
4251 bytes.extend_from_slice(&hw2.to_le_bytes());
4252 Ok(bytes)
4253 }
4254 } else {
4255 let instr: u16 = 0xBF00;
4256 Ok(instr.to_le_bytes().to_vec())
4257 }
4258 }
4259
4260 // MOVW - Move Wide (Thumb-2 32-bit)
4261 ArmOp::Movw { rd, imm16 } => {
4262 self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4263 }
4264
4265 // MOVT - Move Top (Thumb-2 32-bit)
4266 ArmOp::Movt { rd, imm16 } => {
4267 self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4268 }
4269
4270 // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4271 // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4272 // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4273 // symbol's final address to the in-place addend (REL semantics).
4274 ArmOp::MovwSym { rd, addend, .. } => {
4275 self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4276 }
4277 ArmOp::MovtSym { rd, addend, .. } => {
4278 self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4279 }
4280
4281 // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4282 // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4283 // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4284 // relocation against `symbol+addend`, and patches the imm12 with the
4285 // real PC-relative distance once the pool offset is known.
4286 // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4287 // base = Align(PC,4) and PC = address of this instruction + 4.
4288 ArmOp::LdrSym { rd, .. } => {
4289 let rt = reg_to_bits(rd) as u16;
4290 let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4291 let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4292 let mut bytes = Vec::with_capacity(4);
4293 bytes.extend_from_slice(&hw1.to_le_bytes());
4294 bytes.extend_from_slice(&hw2.to_le_bytes());
4295 Ok(bytes)
4296 }
4297
4298 // SetCond: Materialize condition flag into register (0 or 1)
4299 // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4300 // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4301 // always sets flags (MOVS). We need to evaluate the condition BEFORE
4302 // any MOV instruction clobbers the flags from CMP.
4303 ArmOp::SetCond { rd, cond } => {
4304 let rd_bits = reg_to_bits(rd) as u16;
4305
4306 // Condition code encoding for IT block
4307 use synth_synthesis::Condition;
4308 let cond_bits: u16 = match cond {
4309 Condition::EQ => 0x0,
4310 Condition::NE => 0x1,
4311 Condition::LT => 0xB,
4312 Condition::LE => 0xD,
4313 Condition::GT => 0xC,
4314 Condition::GE => 0xA,
4315 Condition::LO => 0x3, // CC/LO (unsigned <)
4316 Condition::LS => 0x9, // LS (unsigned <=)
4317 Condition::HI => 0x8, // HI (unsigned >)
4318 Condition::HS => 0x2, // CS/HS (unsigned >=)
4319 };
4320
4321 // ITE <cond>: encodes If-Then-Else block
4322 // The mask field depends on firstcond[0]:
4323 // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4324 // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4325 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4326 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4327
4328 // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4329 // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4330 // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4331 // turns MOVS into CMP (00100 → 00101), corrupting the result
4332 // (this mis-materialized gale's `has_waiter`, so its `local.set`
4333 // stored a stale register → the binary-sem WAKE dispatch read
4334 // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4335 // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4336 // is fine inside the ITE (the materialized value is the result;
4337 // the flags are not consumed afterwards).
4338 let mut bytes = ite_instr.to_le_bytes().to_vec();
4339 let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4340 if rd_bits <= 7 {
4341 let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4342 bytes.extend_from_slice(&m.to_le_bytes());
4343 } else {
4344 // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4345 let hw1: u16 = 0xF04F;
4346 let hw2: u16 = (rd_bits << 8) | imm;
4347 bytes.extend_from_slice(&hw1.to_le_bytes());
4348 bytes.extend_from_slice(&hw2.to_le_bytes());
4349 }
4350 };
4351 push_mov(&mut bytes, 1); // Then branch (condition true) → 1
4352 push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4353 Ok(bytes)
4354 }
4355
4356 // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4357 // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4358 // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4359 // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4360 ArmOp::I64SetCond {
4361 rd,
4362 rn_lo,
4363 rn_hi,
4364 rm_lo,
4365 rm_hi,
4366 cond,
4367 } => {
4368 use synth_synthesis::Condition;
4369 let rd_bits = reg_to_bits(rd) as u16;
4370 let mut bytes = Vec::new();
4371
4372 // Helper: encode CMP Rn, Rm (16-bit)
4373 let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4374 rm: &synth_synthesis::Reg|
4375 -> Vec<u8> {
4376 let rn_bits = reg_to_bits(rn) as u16;
4377 let rm_bits = reg_to_bits(rm) as u16;
4378 if rn_bits < 8 && rm_bits < 8 {
4379 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4380 instr.to_le_bytes().to_vec()
4381 } else {
4382 let n_bit = (rn_bits >> 3) & 1;
4383 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4384 instr.to_le_bytes().to_vec()
4385 }
4386 };
4387
4388 // Helper: encode ITE <cond> (2 bytes)
4389 let encode_ite = |cond_bits: u16| -> Vec<u8> {
4390 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4391 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4392 ite_instr.to_le_bytes().to_vec()
4393 };
4394
4395 // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4396 let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4397 let mut b = encode_ite(cond_bits);
4398 if rd_bits < 8 {
4399 let mov_one: u16 = 0x2001 | (rd_bits << 8);
4400 let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4401 b.extend_from_slice(&mov_one.to_le_bytes());
4402 b.extend_from_slice(&mov_zero.to_le_bytes());
4403 } else {
4404 // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4405 // rd field; rd_bits<<8 overflows into bit 11 and
4406 // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4407 // CMP r0,#1): the boolean dies in the flags and the
4408 // consumer reads a stale register. Use the 32-bit
4409 // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4410 // flag-preserving. Same class as H-CODE-9 / #180.
4411 for imm in [1u16, 0u16] {
4412 let hw1: u16 = 0xF04F;
4413 let hw2: u16 = (rd_bits << 8) | imm;
4414 b.extend_from_slice(&hw1.to_le_bytes());
4415 b.extend_from_slice(&hw2.to_le_bytes());
4416 }
4417 }
4418 b
4419 };
4420
4421 match cond {
4422 Condition::EQ | Condition::NE => {
4423 // CMP rn_lo, rm_lo (compare low words)
4424 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4425
4426 // IT EQ (execute next instruction only if Z=1)
4427 let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4428 bytes.extend_from_slice(&it_eq.to_le_bytes());
4429
4430 // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4431 bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4432
4433 // ITE <cond>; MOV rd, #1; MOV rd, #0
4434 let cond_bits: u16 = match cond {
4435 Condition::EQ => 0x0,
4436 Condition::NE => 0x1,
4437 _ => unreachable!(),
4438 };
4439 bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4440 }
4441
4442 Condition::LT => {
4443 // CMP rn_lo, rm_lo (sets C flag for borrow)
4444 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4445
4446 // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4447 // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4448 let rn_hi_bits = reg_to_bits(rn_hi);
4449 let rm_hi_bits = reg_to_bits(rm_hi);
4450 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4451 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4452 bytes.extend_from_slice(&hw1.to_le_bytes());
4453 bytes.extend_from_slice(&hw2.to_le_bytes());
4454
4455 // ITE LT; MOV rd, #1; MOV rd, #0
4456 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4457 }
4458
4459 Condition::GT => {
4460 // GT(a,b) = LT(b,a): swap operands
4461 // CMP rm_lo, rn_lo (swapped)
4462 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4463
4464 // SBCS rd, rm_hi, rn_hi (swapped)
4465 let rm_hi_bits = reg_to_bits(rm_hi);
4466 let rn_hi_bits = reg_to_bits(rn_hi);
4467 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4468 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4469 bytes.extend_from_slice(&hw1.to_le_bytes());
4470 bytes.extend_from_slice(&hw2.to_le_bytes());
4471
4472 // ITE LT; MOV rd, #1; MOV rd, #0
4473 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4474 }
4475
4476 Condition::LE => {
4477 // LE(a,b) = !GT(a,b): use GT logic but invert result
4478 // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4479 // CMP rm_lo, rn_lo (swapped, same as GT)
4480 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4481
4482 // SBCS rd, rm_hi, rn_hi (swapped)
4483 let rm_hi_bits = reg_to_bits(rm_hi);
4484 let rn_hi_bits = reg_to_bits(rn_hi);
4485 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4486 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4487 bytes.extend_from_slice(&hw1.to_le_bytes());
4488 bytes.extend_from_slice(&hw2.to_le_bytes());
4489
4490 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4491 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4492 }
4493
4494 Condition::GE => {
4495 // GE(a,b) = !LT(a,b): use LT logic but invert result
4496 // CMP rn_lo, rm_lo (same as LT)
4497 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4498
4499 // SBCS rd, rn_hi, rm_hi (same as LT)
4500 let rn_hi_bits = reg_to_bits(rn_hi);
4501 let rm_hi_bits = reg_to_bits(rm_hi);
4502 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4503 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4504 bytes.extend_from_slice(&hw1.to_le_bytes());
4505 bytes.extend_from_slice(&hw2.to_le_bytes());
4506
4507 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4508 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4509 }
4510
4511 // Unsigned comparisons - same instruction sequence, different conditions
4512 Condition::LO => {
4513 // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4514 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4515 let rn_hi_bits = reg_to_bits(rn_hi);
4516 let rm_hi_bits = reg_to_bits(rm_hi);
4517 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4518 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4519 bytes.extend_from_slice(&hw1.to_le_bytes());
4520 bytes.extend_from_slice(&hw2.to_le_bytes());
4521 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4522 }
4523
4524 Condition::HI => {
4525 // HI (unsigned GT): swap operands and check LO
4526 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4527 let rm_hi_bits = reg_to_bits(rm_hi);
4528 let rn_hi_bits = reg_to_bits(rn_hi);
4529 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4530 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4531 bytes.extend_from_slice(&hw1.to_le_bytes());
4532 bytes.extend_from_slice(&hw2.to_le_bytes());
4533 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4534 }
4535
4536 Condition::LS => {
4537 // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4538 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4539 let rm_hi_bits = reg_to_bits(rm_hi);
4540 let rn_hi_bits = reg_to_bits(rn_hi);
4541 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4542 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4543 bytes.extend_from_slice(&hw1.to_le_bytes());
4544 bytes.extend_from_slice(&hw2.to_le_bytes());
4545 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4546 }
4547
4548 Condition::HS => {
4549 // HS (unsigned GE): !(a < b) = !(LO)
4550 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4551 let rn_hi_bits = reg_to_bits(rn_hi);
4552 let rm_hi_bits = reg_to_bits(rm_hi);
4553 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4554 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4555 bytes.extend_from_slice(&hw1.to_le_bytes());
4556 bytes.extend_from_slice(&hw2.to_le_bytes());
4557 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4558 }
4559 }
4560
4561 Ok(bytes)
4562 }
4563
4564 // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4565 // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4566 ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4567 let rd_bits = reg_to_bits(rd);
4568 let rn_lo_bits = reg_to_bits(rn_lo);
4569 let rn_hi_bits = reg_to_bits(rn_hi);
4570 let mut bytes = Vec::new();
4571
4572 // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4573 let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4574 let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4575 bytes.extend_from_slice(&hw1.to_le_bytes());
4576 bytes.extend_from_slice(&hw2.to_le_bytes());
4577
4578 // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4579 // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4580 // H-CODE-9: rd_bits<<8 overflowing the field compared the
4581 // WRONG register. Same hardening as the #311 SetCond fix.
4582 if rd_bits < 8 {
4583 let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4584 bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4585 } else {
4586 let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4587 let hw2: u16 = 0x0F00;
4588 bytes.extend_from_slice(&hw1.to_le_bytes());
4589 bytes.extend_from_slice(&hw2.to_le_bytes());
4590 }
4591
4592 // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4593 // #311 — see I64SetCond)
4594 let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4595 let ite_instr: u16 = 0xBF00 | mask;
4596 bytes.extend_from_slice(&ite_instr.to_le_bytes());
4597 if rd_bits < 8 {
4598 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4599 let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4600 bytes.extend_from_slice(&mov_one.to_le_bytes());
4601 bytes.extend_from_slice(&mov_zero.to_le_bytes());
4602 } else {
4603 for imm in [1u16, 0u16] {
4604 let hw1: u16 = 0xF04F;
4605 let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4606 bytes.extend_from_slice(&hw1.to_le_bytes());
4607 bytes.extend_from_slice(&hw2.to_le_bytes());
4608 }
4609 }
4610
4611 Ok(bytes)
4612 }
4613
4614 // I64Mul: 64-bit multiply using UMULL + MLA cross products
4615 // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4616 // Uses R12 as scratch register
4617 ArmOp::I64Mul {
4618 rd_lo,
4619 rd_hi,
4620 rn_lo,
4621 rn_hi,
4622 rm_lo,
4623 rm_hi,
4624 } => {
4625 let rd_lo_bits = reg_to_bits(rd_lo);
4626 let rd_hi_bits = reg_to_bits(rd_hi);
4627 let rn_lo_bits = reg_to_bits(rn_lo);
4628 let rn_hi_bits = reg_to_bits(rn_hi);
4629 let rm_lo_bits = reg_to_bits(rm_lo);
4630 let rm_hi_bits = reg_to_bits(rm_hi);
4631 let r12: u32 = 12; // IP scratch register
4632 let mut bytes = Vec::new();
4633
4634 // 1. MUL R12, rn_lo, rm_hi (R12 = a_lo * b_hi)
4635 // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4636 let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4637 let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4638 bytes.extend_from_slice(&hw1.to_le_bytes());
4639 bytes.extend_from_slice(&hw2.to_le_bytes());
4640
4641 // 2. MLA R12, rn_hi, rm_lo, R12 (R12 += a_hi * b_lo)
4642 // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4643 let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4644 let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4645 bytes.extend_from_slice(&hw1.to_le_bytes());
4646 bytes.extend_from_slice(&hw2.to_le_bytes());
4647
4648 // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo (rd_lo:rd_hi = a_lo * b_lo)
4649 // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4650 let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4651 let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4652 bytes.extend_from_slice(&hw1.to_le_bytes());
4653 bytes.extend_from_slice(&hw2.to_le_bytes());
4654
4655 // 4. ADD rd_hi, R12 (rd_hi += cross products)
4656 // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4657 let d_bit = (rd_hi_bits >> 3) & 1;
4658 let add_instr: u16 =
4659 (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4660 bytes.extend_from_slice(&add_instr.to_le_bytes());
4661
4662 Ok(bytes)
4663 }
4664
4665 // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4666 //
4667 // #1048: the expansion must NEVER write its own input operands.
4668 // The pre-#1048 sequence masked the amount IN PLACE
4669 // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4670 // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4671 // LSR) — so re-reading the amount after the shift returned a
4672 // mangled value (amt=64 read back 0, amt=67 read back 3). The
4673 // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4674 // as the ONLY temporary, re-deriving the masked amount from the
4675 // untouched rm_lo whenever a second live temp would otherwise be
4676 // needed. This matches the Rocq/SMT pseudo-op models
4677 // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4678 // exactly this non-clobbering contract all along.
4679 ArmOp::I64Shl {
4680 rd_lo,
4681 rd_hi,
4682 rn_lo,
4683 rn_hi,
4684 rm_lo,
4685 rm_hi: _,
4686 } => {
4687 let rd_lo_bits = reg_to_bits(rd_lo);
4688 let rd_hi_bits = reg_to_bits(rd_hi);
4689 let rn_lo_bits = reg_to_bits(rn_lo);
4690 let rn_hi_bits = reg_to_bits(rn_hi);
4691 let rm_lo_bits = reg_to_bits(rm_lo);
4692 let r12: u32 = 12; // the only scratch — never allocatable
4693 let mut bytes = Vec::new();
4694
4695 // #1039 house style: refuse a destination that would collide
4696 // with an input still needed after the destination is first
4697 // written, loudly — never misassemble. rd_hi is written before
4698 // rn_lo and rm_lo are last read; the in-place form
4699 // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4700 if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4701 return Err(synth_core::Error::synthesis(format!(
4702 "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4703 )));
4704 }
4705
4706 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4707 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4708 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4709 bytes.extend_from_slice(&hw1.to_le_bytes());
4710 bytes.extend_from_slice(&hw2.to_le_bytes());
4711
4712 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4713 let hw1: u16 = (0xF1B0 | r12) as u16;
4714 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4715 bytes.extend_from_slice(&hw1.to_le_bytes());
4716 bytes.extend_from_slice(&hw2.to_le_bytes());
4717
4718 // BPL .large (branch if n >= 32, offset = +14 halfwords)
4719 let bpl: u16 = 0xD50E;
4720 bytes.extend_from_slice(&bpl.to_le_bytes());
4721
4722 // --- Small shift (n < 32) ---
4723 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4724 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4725 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4726 bytes.extend_from_slice(&hw1.to_le_bytes());
4727 bytes.extend_from_slice(&hw2.to_le_bytes());
4728
4729 // LSL.W rd_hi, rn_hi, R12 (hi << n; rn_hi's last read)
4730 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4731 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4732 bytes.extend_from_slice(&hw1.to_le_bytes());
4733 bytes.extend_from_slice(&hw2.to_le_bytes());
4734
4735 // RSB.W R12, R12, #32 (R12 = 32-n)
4736 let hw1: u16 = (0xF1C0 | r12) as u16;
4737 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4738 bytes.extend_from_slice(&hw1.to_le_bytes());
4739 bytes.extend_from_slice(&hw2.to_le_bytes());
4740
4741 // LSR.W R12, rn_lo, R12 (overflow = lo >> (32-n); n=0 gives
4742 // a register shift by 32 which yields 0 — exact)
4743 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4744 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4745 bytes.extend_from_slice(&hw1.to_le_bytes());
4746 bytes.extend_from_slice(&hw2.to_le_bytes());
4747
4748 // ORR.W rd_hi, rd_hi, R12 (hi |= overflow bits from lo)
4749 let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4750 let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4751 bytes.extend_from_slice(&hw1.to_le_bytes());
4752 bytes.extend_from_slice(&hw2.to_le_bytes());
4753
4754 // AND.W R12, rm_lo, #63 (n once more for the low half)
4755 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4756 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4757 bytes.extend_from_slice(&hw1.to_le_bytes());
4758 bytes.extend_from_slice(&hw2.to_le_bytes());
4759
4760 // LSL.W rd_lo, rn_lo, R12 (lo << n)
4761 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4762 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4763 bytes.extend_from_slice(&hw1.to_le_bytes());
4764 bytes.extend_from_slice(&hw2.to_le_bytes());
4765
4766 // B .done — `.done` is the END of the expansion, i.e. PAST the
4767 // large-shift arm's trailing zero-fill. #916: that zero-fill is
4768 // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4769 // displacement is DERIVED from its real width instead of the
4770 // hard-coded 0xE002 — widening the MOV without this would
4771 // overshoot `.done` and turn a data miscompile into a
4772 // control-flow one. Thumb `B` reads PC as its own address + 4
4773 // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4774 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4775 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4776 bytes.extend_from_slice(&b_done.to_le_bytes());
4777
4778 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4779 // LSL.W rd_hi, rn_lo, R12 (hi = lo << (n-32))
4780 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4781 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4782 bytes.extend_from_slice(&hw1.to_le_bytes());
4783 bytes.extend_from_slice(&hw2.to_le_bytes());
4784
4785 // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4786 // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4787 // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4788 // cannot be reordered to dodge the displacement change.
4789 emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4790
4791 Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4792 }
4793
4794 // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4795 // n>=32. #1048: R12-only scratch, operands never written — see
4796 // the I64Shl comment for the full rationale.
4797 ArmOp::I64ShrU {
4798 rd_lo,
4799 rd_hi,
4800 rn_lo,
4801 rn_hi,
4802 rm_lo,
4803 rm_hi: _,
4804 } => {
4805 let rd_lo_bits = reg_to_bits(rd_lo);
4806 let rd_hi_bits = reg_to_bits(rd_hi);
4807 let rn_lo_bits = reg_to_bits(rn_lo);
4808 let rn_hi_bits = reg_to_bits(rn_hi);
4809 let rm_lo_bits = reg_to_bits(rm_lo);
4810 let r12: u32 = 12; // the only scratch — never allocatable
4811 let mut bytes = Vec::new();
4812
4813 // #1039 house style: rd_lo is written before rn_hi and rm_lo
4814 // are last read — refuse the collision loudly. The in-place
4815 // form rd == rn (select_default) has rd_lo == rn_lo and stays
4816 // legal.
4817 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4818 return Err(synth_core::Error::synthesis(format!(
4819 "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4820 )));
4821 }
4822
4823 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4824 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4825 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4826 bytes.extend_from_slice(&hw1.to_le_bytes());
4827 bytes.extend_from_slice(&hw2.to_le_bytes());
4828
4829 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4830 let hw1: u16 = (0xF1B0 | r12) as u16;
4831 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4832 bytes.extend_from_slice(&hw1.to_le_bytes());
4833 bytes.extend_from_slice(&hw2.to_le_bytes());
4834
4835 // BPL .large (+14 halfwords)
4836 let bpl: u16 = 0xD50E;
4837 bytes.extend_from_slice(&bpl.to_le_bytes());
4838
4839 // --- Small shift (n < 32) ---
4840 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4841 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4842 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4843 bytes.extend_from_slice(&hw1.to_le_bytes());
4844 bytes.extend_from_slice(&hw2.to_le_bytes());
4845
4846 // LSR.W rd_lo, rn_lo, R12 (lo >> n; rn_lo's last read)
4847 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4848 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4849 bytes.extend_from_slice(&hw1.to_le_bytes());
4850 bytes.extend_from_slice(&hw2.to_le_bytes());
4851
4852 // RSB.W R12, R12, #32 (R12 = 32-n)
4853 let hw1: u16 = (0xF1C0 | r12) as u16;
4854 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4855 bytes.extend_from_slice(&hw1.to_le_bytes());
4856 bytes.extend_from_slice(&hw2.to_le_bytes());
4857
4858 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
4859 // a register shift by 32 which yields 0 — exact)
4860 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4861 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4862 bytes.extend_from_slice(&hw1.to_le_bytes());
4863 bytes.extend_from_slice(&hw2.to_le_bytes());
4864
4865 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
4866 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4867 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4868 bytes.extend_from_slice(&hw1.to_le_bytes());
4869 bytes.extend_from_slice(&hw2.to_le_bytes());
4870
4871 // AND.W R12, rm_lo, #63 (n once more for the high half)
4872 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4873 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4874 bytes.extend_from_slice(&hw1.to_le_bytes());
4875 bytes.extend_from_slice(&hw2.to_le_bytes());
4876
4877 // LSR.W rd_hi, rn_hi, R12 (hi >> n, logical)
4878 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4879 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4880 bytes.extend_from_slice(&hw1.to_le_bytes());
4881 bytes.extend_from_slice(&hw2.to_le_bytes());
4882
4883 // B .done — see I64Shl: `.done` is the END of the expansion,
4884 // past the trailing zero-fill, so the displacement is derived
4885 // from that zero-fill's real width (#916).
4886 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4887 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4888 bytes.extend_from_slice(&b_done.to_le_bytes());
4889
4890 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4891 // LSR.W rd_lo, rn_hi, R12 (lo = hi >> (n-32))
4892 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4893 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4894 bytes.extend_from_slice(&hw1.to_le_bytes());
4895 bytes.extend_from_slice(&hw2.to_le_bytes());
4896
4897 // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4898 // load-bearing: the LSR.W above reads rn_hi, which may BE
4899 // rd_hi in the in-place case.
4900 emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4901
4902 Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4903 }
4904
4905 // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4906 // n>=32. #1048: R12-only scratch, operands never written — see
4907 // the I64Shl comment for the full rationale.
4908 ArmOp::I64ShrS {
4909 rd_lo,
4910 rd_hi,
4911 rn_lo,
4912 rn_hi,
4913 rm_lo,
4914 rm_hi: _,
4915 } => {
4916 let rd_lo_bits = reg_to_bits(rd_lo);
4917 let rd_hi_bits = reg_to_bits(rd_hi);
4918 let rn_lo_bits = reg_to_bits(rn_lo);
4919 let rn_hi_bits = reg_to_bits(rn_hi);
4920 let rm_lo_bits = reg_to_bits(rm_lo);
4921 let r12: u32 = 12; // the only scratch — never allocatable
4922 let mut bytes = Vec::new();
4923
4924 // #1039 house style: rd_lo is written before rn_hi and rm_lo
4925 // are last read (on BOTH arms of the diamond — the large arm's
4926 // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
4927 // rd_lo is written). The in-place form rd == rn stays legal.
4928 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4929 return Err(synth_core::Error::synthesis(format!(
4930 "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4931 )));
4932 }
4933
4934 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4935 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4936 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4937 bytes.extend_from_slice(&hw1.to_le_bytes());
4938 bytes.extend_from_slice(&hw2.to_le_bytes());
4939
4940 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4941 let hw1: u16 = (0xF1B0 | r12) as u16;
4942 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4943 bytes.extend_from_slice(&hw1.to_le_bytes());
4944 bytes.extend_from_slice(&hw2.to_le_bytes());
4945
4946 // BPL .large (+14 halfwords)
4947 let bpl: u16 = 0xD50E;
4948 bytes.extend_from_slice(&bpl.to_le_bytes());
4949
4950 // --- Small shift (n < 32) ---
4951 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4952 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4953 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4954 bytes.extend_from_slice(&hw1.to_le_bytes());
4955 bytes.extend_from_slice(&hw2.to_le_bytes());
4956
4957 // LSR.W rd_lo, rn_lo, R12 (lo >> n, logical for lo word)
4958 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4959 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4960 bytes.extend_from_slice(&hw1.to_le_bytes());
4961 bytes.extend_from_slice(&hw2.to_le_bytes());
4962
4963 // RSB.W R12, R12, #32 (R12 = 32-n)
4964 let hw1: u16 = (0xF1C0 | r12) as u16;
4965 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4966 bytes.extend_from_slice(&hw1.to_le_bytes());
4967 bytes.extend_from_slice(&hw2.to_le_bytes());
4968
4969 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
4970 // a register shift by 32 which yields 0 — exact)
4971 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4972 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4973 bytes.extend_from_slice(&hw1.to_le_bytes());
4974 bytes.extend_from_slice(&hw2.to_le_bytes());
4975
4976 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
4977 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4978 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4979 bytes.extend_from_slice(&hw1.to_le_bytes());
4980 bytes.extend_from_slice(&hw2.to_le_bytes());
4981
4982 // AND.W R12, rm_lo, #63 (n once more for the high half)
4983 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4984 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4985 bytes.extend_from_slice(&hw1.to_le_bytes());
4986 bytes.extend_from_slice(&hw2.to_le_bytes());
4987
4988 // ASR.W rd_hi, rn_hi, R12 (hi >> n, arithmetic/sign-extending)
4989 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4990 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4991 bytes.extend_from_slice(&hw1.to_le_bytes());
4992 bytes.extend_from_slice(&hw2.to_le_bytes());
4993
4994 // B .done (+3 halfwords, large shift is 8 bytes)
4995 let b_done: u16 = 0xE003;
4996 bytes.extend_from_slice(&b_done.to_le_bytes());
4997
4998 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4999 // ASR.W rd_lo, rn_hi, R12 (lo = hi >>> (n-32))
5000 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5001 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5002 bytes.extend_from_slice(&hw1.to_le_bytes());
5003 bytes.extend_from_slice(&hw2.to_le_bytes());
5004
5005 // ASR.W rd_hi, rn_hi, #31 (hi = sign extension, all 0s or all 1s)
5006 // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
5007 // imm5=31=11111 → imm3=111, imm2=11
5008 let hw1: u16 = 0xEA4F;
5009 let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
5010 bytes.extend_from_slice(&hw1.to_le_bytes());
5011 bytes.extend_from_slice(&hw2.to_le_bytes());
5012
5013 Ok(bytes) // Total: 48 bytes
5014 }
5015
5016 // I64Rotl: 64-bit rotate left (#610 rewrite).
5017 // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5018 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5019 //
5020 // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5021 // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5022 // pre-#610 expansion wrote through the selector's registers with
5023 // colliding R3/R4 scratch and restored the saved R4 OVER the
5024 // result). Relies on ARM register-shift semantics: amounts >= 32
5025 // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5026 ArmOp::I64Rotl {
5027 rdlo,
5028 rdhi,
5029 rnlo,
5030 rnhi,
5031 shift,
5032 } => {
5033 let mut bytes = Vec::new();
5034 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5035
5036 let core: [u16; 35] = [
5037 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5038 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5039 0xD50E, // BPL .large (n >= 32)
5040 // --- small rotation (n < 32) ---
5041 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5042 0xFA20, 0xFC03, // LSR.W R12, R0, R3 (lo >> (32-n))
5043 0xFA21, 0xF303, // LSR.W R3, R1, R3 (hi >> (32-n))
5044 0xFA01, 0xF102, // LSL.W R1, R1, R2 (hi << n)
5045 0xEA41, 0x010C, // ORR.W R1, R1, R12 (new_hi)
5046 0xFA00, 0xF002, // LSL.W R0, R0, R2 (lo << n)
5047 0xEA40, 0x0003, // ORR.W R0, R0, R3 (new_lo)
5048 0xE00E, // B .done
5049 // --- large rotation (n >= 32), R3 = m = n-32 ---
5050 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5051 0xFA21, 0xFC02, // LSR.W R12, R1, R2 (hi >> (64-n))
5052 0xFA20, 0xF202, // LSR.W R2, R0, R2 (lo >> (64-n))
5053 0xFA00, 0xF003, // LSL.W R0, R0, R3 (lo << m)
5054 0xFA01, 0xF103, // LSL.W R1, R1, R3 (hi << m)
5055 0xEA40, 0x0C0C, // ORR.W R12, R0, R12 (new_hi = (lo<<m)|(hi>>(64-n)))
5056 0xEA41, 0x0002, // ORR.W R0, R1, R2 (new_lo = (hi<<m)|(lo>>(64-n)))
5057 0x4661, // MOV R1, R12 (new_hi into place)
5058 // .done: result in R0:R1
5059 ];
5060 for hw in core {
5061 bytes.extend_from_slice(&hw.to_le_bytes());
5062 }
5063
5064 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5065 Ok(bytes) // Total: 102 bytes
5066 }
5067
5068 // I64Rotr: 64-bit rotate right (#610 rewrite).
5069 // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5070 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5071 //
5072 // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5073 // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5074 ArmOp::I64Rotr {
5075 rdlo,
5076 rdhi,
5077 rnlo,
5078 rnhi,
5079 shift,
5080 } => {
5081 let mut bytes = Vec::new();
5082 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5083
5084 let core: [u16; 35] = [
5085 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5086 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5087 0xD50E, // BPL .large (n >= 32)
5088 // --- small rotation (n < 32) ---
5089 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5090 0xFA01, 0xFC03, // LSL.W R12, R1, R3 (hi << (32-n))
5091 0xFA00, 0xF303, // LSL.W R3, R0, R3 (lo << (32-n))
5092 0xFA20, 0xF002, // LSR.W R0, R0, R2 (lo >> n)
5093 0xEA40, 0x000C, // ORR.W R0, R0, R12 (new_lo)
5094 0xFA21, 0xF102, // LSR.W R1, R1, R2 (hi >> n)
5095 0xEA41, 0x0103, // ORR.W R1, R1, R3 (new_hi)
5096 0xE00E, // B .done
5097 // --- large rotation (n >= 32), R3 = m = n-32 ---
5098 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5099 0xFA00, 0xFC02, // LSL.W R12, R0, R2 (lo << (64-n))
5100 0xFA01, 0xF202, // LSL.W R2, R1, R2 (hi << (64-n))
5101 0xFA21, 0xF103, // LSR.W R1, R1, R3 (hi >> m)
5102 0xEA41, 0x0C0C, // ORR.W R12, R1, R12 (new_lo = (hi>>m)|(lo<<(64-n)))
5103 0xFA20, 0xF103, // LSR.W R1, R0, R3 (lo >> m)
5104 0xEA41, 0x0102, // ORR.W R1, R1, R2 (new_hi = (lo>>m)|(hi<<(64-n)))
5105 0x4660, // MOV R0, R12 (new_lo into place)
5106 // .done: result in R0:R1
5107 ];
5108 for hw in core {
5109 bytes.extend_from_slice(&hw.to_le_bytes());
5110 }
5111
5112 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5113 Ok(bytes) // Total: 102 bytes
5114 }
5115
5116 // I64Clz: Count leading zeros in 64-bit value
5117 // If hi != 0: result = CLZ(hi)
5118 // If hi == 0: result = 32 + CLZ(lo)
5119 //
5120 // Layout (using CMP+BNE approach for consistency):
5121 // 0: CMP.W rnhi, #0 (4 bytes)
5122 // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5123 // 6: CLZ.W rd, rnhi (4 bytes)
5124 // 10: B .done (2 bytes) - branch forward to offset 22
5125 // 12: NOP (2 bytes) - padding for alignment
5126 // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5127 // 18: ADD.W rd, rd, #32 (4 bytes)
5128 // 22: .done
5129 ArmOp::I64Clz { rd, rnlo, rnhi } => {
5130 let rd_bits = reg_to_bits(rd);
5131 let rn_lo_bits = reg_to_bits(rnlo);
5132 let rn_hi_bits = reg_to_bits(rnhi);
5133 let mut bytes = Vec::new();
5134
5135 // CMP.W rnhi, #0 (4 bytes at offset 0)
5136 let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5137 let hw2: u16 = 0x0F00;
5138 bytes.extend_from_slice(&hw1.to_le_bytes());
5139 bytes.extend_from_slice(&hw2.to_le_bytes());
5140
5141 // BEQ .hi_zero (2 bytes at offset 4)
5142 // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5143 let beq: u16 = 0xD003;
5144 bytes.extend_from_slice(&beq.to_le_bytes());
5145
5146 // CLZ.W rd, rnhi (4 bytes at offset 6)
5147 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5148 let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5149 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5150 bytes.extend_from_slice(&hw1.to_le_bytes());
5151 bytes.extend_from_slice(&hw2.to_le_bytes());
5152
5153 // B .done (2 bytes at offset 10)
5154 // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5155 let b_done: u16 = 0xE004;
5156 bytes.extend_from_slice(&b_done.to_le_bytes());
5157
5158 // NOP (2 bytes at offset 12) - padding
5159 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5160
5161 // .hi_zero: (offset 14)
5162 // CLZ.W rd, rnlo (4 bytes)
5163 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5164 let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5165 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5166 bytes.extend_from_slice(&hw1.to_le_bytes());
5167 bytes.extend_from_slice(&hw2.to_le_bytes());
5168
5169 // ADD.W rd, rd, #32 (4 bytes at offset 18)
5170 let hw1: u16 = (0xF100 | rd_bits) as u16;
5171 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5172 bytes.extend_from_slice(&hw1.to_le_bytes());
5173 bytes.extend_from_slice(&hw2.to_le_bytes());
5174
5175 // .done: (offset 22 — the end of the expansion)
5176 //
5177 // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5178 // is GONE. It was aimed at the RESULT's high half but wrote
5179 // the OPERAND's home high register — a real executed
5180 // miscompile on the direct selector, which allocates a fresh
5181 // destination pair and zeroes its own dst_hi, leaving the
5182 // operand's hi limb destroyed for any later re-read. The
5183 // Rocq/SMT models of I64ClzPseudo always said "writes rd
5184 // ONLY"; the callers that relied on the implicit clear
5185 // (select_default, optimizer_bridge) now emit their own
5186 // explicit hi-zero op. `B .done` above targets offset 22 =
5187 // past-the-end, `BEQ` targets offset 14 — no displacement
5188 // moves.
5189
5190 Ok(bytes) // 22 bytes, register-independent
5191 }
5192
5193 // I64Ctz: Count trailing zeros in 64-bit value
5194 // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5195 // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5196 //
5197 // Layout:
5198 // 0: CMP.W rnlo, #0 (4 bytes)
5199 // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5200 // 6: RBIT.W rd, rnlo (4 bytes)
5201 // 10: CLZ.W rd, rd (4 bytes)
5202 // 14: B .done (2 bytes) - branch to offset 30
5203 // 16: NOP (2 bytes) - padding
5204 // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5205 // 22: CLZ.W rd, rd (4 bytes)
5206 // 26: ADD.W rd, rd, #32 (4 bytes)
5207 // 30: .done
5208 ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5209 let rd_bits = reg_to_bits(rd);
5210 let rn_lo_bits = reg_to_bits(rnlo);
5211 let rn_hi_bits = reg_to_bits(rnhi);
5212 let mut bytes = Vec::new();
5213
5214 // CMP.W rnlo, #0 (4 bytes at offset 0)
5215 let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5216 let hw2: u16 = 0x0F00;
5217 bytes.extend_from_slice(&hw1.to_le_bytes());
5218 bytes.extend_from_slice(&hw2.to_le_bytes());
5219
5220 // BEQ .lo_zero (2 bytes at offset 4)
5221 // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5222 let beq: u16 = 0xD005;
5223 bytes.extend_from_slice(&beq.to_le_bytes());
5224
5225 // RBIT.W rd, rnlo (4 bytes at offset 6)
5226 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5227 let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5228 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5229 bytes.extend_from_slice(&hw1.to_le_bytes());
5230 bytes.extend_from_slice(&hw2.to_le_bytes());
5231
5232 // CLZ.W rd, rd (4 bytes at offset 10)
5233 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5234 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5235 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5236 bytes.extend_from_slice(&hw1.to_le_bytes());
5237 bytes.extend_from_slice(&hw2.to_le_bytes());
5238
5239 // B .done (2 bytes at offset 14)
5240 // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5241 let b_done: u16 = 0xE006;
5242 bytes.extend_from_slice(&b_done.to_le_bytes());
5243
5244 // NOP (2 bytes at offset 16) - padding
5245 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5246
5247 // .lo_zero: (offset 18)
5248 // RBIT.W rd, rnhi (4 bytes)
5249 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5250 let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5251 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5252 bytes.extend_from_slice(&hw1.to_le_bytes());
5253 bytes.extend_from_slice(&hw2.to_le_bytes());
5254
5255 // CLZ.W rd, rd (4 bytes at offset 22)
5256 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5257 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5258 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5259 bytes.extend_from_slice(&hw1.to_le_bytes());
5260 bytes.extend_from_slice(&hw2.to_le_bytes());
5261
5262 // ADD.W rd, rd, #32 (4 bytes at offset 26)
5263 let hw1: u16 = (0xF100 | rd_bits) as u16;
5264 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5265 bytes.extend_from_slice(&hw1.to_le_bytes());
5266 bytes.extend_from_slice(&hw2.to_le_bytes());
5267
5268 // .done: (offset 30 — the end of the expansion)
5269 // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5270 // wrote the OPERAND's home high register (see the I64Clz
5271 // comment above). `B .done` targets offset 30 = past-the-end,
5272 // `BEQ` targets offset 18 — no displacement moves.
5273
5274 Ok(bytes) // 30 bytes, register-independent
5275 }
5276
5277 // I64Popcnt: Population count of 64-bit value
5278 // result = POPCNT(lo) + POPCNT(hi)
5279 // Using SIMD-style parallel bit counting algorithm
5280 ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5281 let rd_bits = reg_to_bits(rd);
5282 let rn_lo_bits = reg_to_bits(rnlo);
5283 let rn_hi_bits = reg_to_bits(rnhi);
5284 let r12: u32 = 12; // IP scratch
5285 let r3: u32 = 3; // Scratch for hi popcnt result
5286 let mut bytes = Vec::new();
5287
5288 // PUSH {R3, R4, R5} - save scratch registers
5289 bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5290
5291 // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5292 // Using lookup table approach for each byte would be too large
5293 // Using shift-and-add approach instead
5294
5295 // For simplicity and correctness, use the efficient parallel algorithm
5296 // but implement it as a series of inline operations
5297
5298 // Marshal the operand pair into the fixed scratch regs, routing
5299 // rnlo through R12 (#632 audit): writing R4 first corrupted the
5300 // rnhi read for a pair living at (R3,R4) — every source is read
5301 // before any scratch register it could occupy is written.
5302 // MOV R12, rnlo
5303 let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5304 bytes.extend_from_slice(&mov.to_le_bytes());
5305 // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5306 let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5307 bytes.extend_from_slice(&mov.to_le_bytes());
5308 // MOV R4, R12
5309 bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5310
5311 // --- POPCNT for R4 (lo word) ---
5312 // Step 1: x = x - ((x >> 1) & 0x55555555)
5313 // LSR.W R12, R4, #1
5314 let hw1: u16 = 0xEA4F;
5315 let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5316 bytes.extend_from_slice(&hw1.to_le_bytes());
5317 bytes.extend_from_slice(&hw2.to_le_bytes());
5318
5319 // Load 0x55555555 into R3 using MOVW/MOVT
5320 // MOVW R3, #0x5555
5321 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5322 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5323 // MOVT R3, #0x5555
5324 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5325 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5326
5327 // AND.W R12, R12, R3
5328 let hw1: u16 = (0xEA00 | r12) as u16;
5329 let hw2: u16 = ((r12 << 8) | r3) as u16;
5330 bytes.extend_from_slice(&hw1.to_le_bytes());
5331 bytes.extend_from_slice(&hw2.to_le_bytes());
5332
5333 // SUB.W R4, R4, R12
5334 let hw1: u16 = (0xEBA0 | 4) as u16;
5335 let hw2: u16 = ((4 << 8) | r12) as u16;
5336 bytes.extend_from_slice(&hw1.to_le_bytes());
5337 bytes.extend_from_slice(&hw2.to_le_bytes());
5338
5339 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5340 // Load 0x33333333 into R3
5341 // MOVW R3, #0x3333
5342 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5343 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5344 // MOVT R3, #0x3333
5345 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5346 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5347
5348 // AND.W R12, R4, R3
5349 let hw1: u16 = (0xEA00 | 4) as u16;
5350 let hw2: u16 = ((r12 << 8) | r3) as u16;
5351 bytes.extend_from_slice(&hw1.to_le_bytes());
5352 bytes.extend_from_slice(&hw2.to_le_bytes());
5353
5354 // LSR.W R4, R4, #2
5355 let hw1: u16 = 0xEA4F;
5356 let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5357 bytes.extend_from_slice(&hw1.to_le_bytes());
5358 bytes.extend_from_slice(&hw2.to_le_bytes());
5359
5360 // AND.W R4, R4, R3
5361 let hw1: u16 = (0xEA00 | 4) as u16;
5362 let hw2: u16 = ((4 << 8) | r3) as u16;
5363 bytes.extend_from_slice(&hw1.to_le_bytes());
5364 bytes.extend_from_slice(&hw2.to_le_bytes());
5365
5366 // ADD.W R4, R4, R12
5367 let hw1: u16 = (0xEB00 | 4) as u16;
5368 let hw2: u16 = ((4 << 8) | r12) as u16;
5369 bytes.extend_from_slice(&hw1.to_le_bytes());
5370 bytes.extend_from_slice(&hw2.to_le_bytes());
5371
5372 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5373 // LSR.W R12, R4, #4
5374 // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5375 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5376 let hw1: u16 = 0xEA4F;
5377 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5378 bytes.extend_from_slice(&hw1.to_le_bytes());
5379 bytes.extend_from_slice(&hw2.to_le_bytes());
5380
5381 // ADD.W R4, R4, R12
5382 let hw1: u16 = (0xEB00 | 4) as u16;
5383 let hw2: u16 = ((4 << 8) | r12) as u16;
5384 bytes.extend_from_slice(&hw1.to_le_bytes());
5385 bytes.extend_from_slice(&hw2.to_le_bytes());
5386
5387 // Load 0x0F0F0F0F into R3
5388 // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5389 // hw1 = 11110 1 10 0100 0000 = 0xF640
5390 // hw2 = 0 111 0011 00001111 = 0x730F
5391 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5392 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5393 // MOVT R3, #0x0F0F
5394 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5395 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5396
5397 // AND.W R4, R4, R3
5398 let hw1: u16 = (0xEA00 | 4) as u16;
5399 let hw2: u16 = ((4 << 8) | r3) as u16;
5400 bytes.extend_from_slice(&hw1.to_le_bytes());
5401 bytes.extend_from_slice(&hw2.to_le_bytes());
5402
5403 // Step 4: x = x * 0x01010101 >> 24
5404 // Load 0x01010101 into R3
5405 // MOVW R3, #0x0101
5406 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5407 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5408 // MOVT R3, #0x0101
5409 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5410 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5411
5412 // MUL R4, R4, R3
5413 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5414 let hw1: u16 = (0xFB00 | 4) as u16;
5415 let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5416 bytes.extend_from_slice(&hw1.to_le_bytes());
5417 bytes.extend_from_slice(&hw2.to_le_bytes());
5418
5419 // LSR.W R4, R4, #24
5420 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5421 let hw1: u16 = 0xEA4F;
5422 let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5423 bytes.extend_from_slice(&hw1.to_le_bytes());
5424 bytes.extend_from_slice(&hw2.to_le_bytes());
5425
5426 // --- POPCNT for R5 (hi word) - same algorithm ---
5427 // Step 1
5428 let hw1: u16 = 0xEA4F;
5429 let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5430 bytes.extend_from_slice(&hw1.to_le_bytes());
5431 bytes.extend_from_slice(&hw2.to_le_bytes());
5432
5433 // Load 0x55555555 into R3
5434 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5435 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5436 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5437 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5438
5439 let hw1: u16 = (0xEA00 | r12) as u16;
5440 let hw2: u16 = ((r12 << 8) | r3) as u16;
5441 bytes.extend_from_slice(&hw1.to_le_bytes());
5442 bytes.extend_from_slice(&hw2.to_le_bytes());
5443
5444 let hw1: u16 = (0xEBA0 | 5) as u16;
5445 let hw2: u16 = ((5 << 8) | r12) as u16;
5446 bytes.extend_from_slice(&hw1.to_le_bytes());
5447 bytes.extend_from_slice(&hw2.to_le_bytes());
5448
5449 // Step 2
5450 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5451 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5452 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5453 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5454
5455 let hw1: u16 = (0xEA00 | 5) as u16;
5456 let hw2: u16 = ((r12 << 8) | r3) as u16;
5457 bytes.extend_from_slice(&hw1.to_le_bytes());
5458 bytes.extend_from_slice(&hw2.to_le_bytes());
5459
5460 let hw1: u16 = 0xEA4F;
5461 let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5462 bytes.extend_from_slice(&hw1.to_le_bytes());
5463 bytes.extend_from_slice(&hw2.to_le_bytes());
5464
5465 let hw1: u16 = (0xEA00 | 5) as u16;
5466 let hw2: u16 = ((5 << 8) | r3) as u16;
5467 bytes.extend_from_slice(&hw1.to_le_bytes());
5468 bytes.extend_from_slice(&hw2.to_le_bytes());
5469
5470 let hw1: u16 = (0xEB00 | 5) as u16;
5471 let hw2: u16 = ((5 << 8) | r12) as u16;
5472 bytes.extend_from_slice(&hw1.to_le_bytes());
5473 bytes.extend_from_slice(&hw2.to_le_bytes());
5474
5475 // Step 3: LSR.W R12, R5, #4
5476 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5477 let hw1: u16 = 0xEA4F;
5478 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5479 bytes.extend_from_slice(&hw1.to_le_bytes());
5480 bytes.extend_from_slice(&hw2.to_le_bytes());
5481
5482 let hw1: u16 = (0xEB00 | 5) as u16;
5483 let hw2: u16 = ((5 << 8) | r12) as u16;
5484 bytes.extend_from_slice(&hw1.to_le_bytes());
5485 bytes.extend_from_slice(&hw2.to_le_bytes());
5486
5487 // Load 0x0F0F0F0F into R3 (for hi-word)
5488 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5489 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5490 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5491 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5492
5493 let hw1: u16 = (0xEA00 | 5) as u16;
5494 let hw2: u16 = ((5 << 8) | r3) as u16;
5495 bytes.extend_from_slice(&hw1.to_le_bytes());
5496 bytes.extend_from_slice(&hw2.to_le_bytes());
5497
5498 // Step 4
5499 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5500 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5501 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5502 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5503
5504 // MUL R5, R5, R3
5505 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5506 let hw1: u16 = (0xFB00 | 5) as u16;
5507 let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5508 bytes.extend_from_slice(&hw1.to_le_bytes());
5509 bytes.extend_from_slice(&hw2.to_le_bytes());
5510
5511 // LSR.W R5, R5, #24
5512 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5513 let hw1: u16 = 0xEA4F;
5514 let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5515 bytes.extend_from_slice(&hw1.to_le_bytes());
5516 bytes.extend_from_slice(&hw2.to_le_bytes());
5517
5518 // #632: the count must be carried ACROSS the scratch restore
5519 // in a register the POP cannot touch. rd is allocator-assigned
5520 // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5521 // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5522 // result one instruction after computing it (0 for every input
5523 // under qemu). R12 is encoder scratch: never allocatable (#212)
5524 // and never in a restore set, so no choice of rd can collide.
5525 // ADD.W R12, R4, R5
5526 bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5527 bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5528
5529 // POP {R3, R4, R5}
5530 bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5531
5532 // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5533 // also total over rd = R8, where the old ADDS T1 3-bit field
5534 // silently corrupted the encoding (#178/#180 class).
5535 let mov: u16 =
5536 (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5537 bytes.extend_from_slice(&mov.to_le_bytes());
5538
5539 // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5540 // is GONE — it wrote the OPERAND's home high register (see
5541 // the I64Clz comment). Callers that relied on the implicit
5542 // clear emit their own explicit hi-zero op.
5543
5544 Ok(bytes)
5545 }
5546
5547 // I64Extend8S: Sign-extend low 8 bits to 64 bits
5548 // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5549 ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5550 let rdlo_bits = reg_to_bits(rdlo);
5551 let rdhi_bits = reg_to_bits(rdhi);
5552 let rnlo_bits = reg_to_bits(rnlo);
5553 let mut bytes = Vec::new();
5554
5555 // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5556 // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5557 let hw1: u16 = 0xFA4F_u16;
5558 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5559 bytes.extend_from_slice(&hw1.to_le_bytes());
5560 bytes.extend_from_slice(&hw2.to_le_bytes());
5561
5562 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5563 // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5564 // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5565 // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5566 let hw1: u16 = 0xEA4F;
5567 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5568 bytes.extend_from_slice(&hw1.to_le_bytes());
5569 bytes.extend_from_slice(&hw2.to_le_bytes());
5570
5571 Ok(bytes)
5572 }
5573
5574 // I64Extend16S: Sign-extend low 16 bits to 64 bits
5575 // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5576 ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5577 let rdlo_bits = reg_to_bits(rdlo);
5578 let rdhi_bits = reg_to_bits(rdhi);
5579 let rnlo_bits = reg_to_bits(rnlo);
5580 let mut bytes = Vec::new();
5581
5582 // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5583 // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5584 let hw1: u16 = 0xFA0F_u16;
5585 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5586 bytes.extend_from_slice(&hw1.to_le_bytes());
5587 bytes.extend_from_slice(&hw2.to_le_bytes());
5588
5589 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5590 let hw1: u16 = 0xEA4F;
5591 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5592 bytes.extend_from_slice(&hw1.to_le_bytes());
5593 bytes.extend_from_slice(&hw2.to_le_bytes());
5594
5595 Ok(bytes)
5596 }
5597
5598 // I64Extend32S: Sign-extend low 32 bits to 64 bits
5599 // Result: rdlo = rnlo, rdhi = rnlo >> 31
5600 ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5601 let rdlo_bits = reg_to_bits(rdlo);
5602 let rdhi_bits = reg_to_bits(rdhi);
5603 let rnlo_bits = reg_to_bits(rnlo);
5604 let mut bytes = Vec::new();
5605
5606 // MOV rdlo, rnlo (if different)
5607 if rdlo_bits != rnlo_bits {
5608 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5609 let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5610 let mov: u16 = 0x4600
5611 | (d_bit << 7)
5612 | ((rnlo_bits as u16) << 3)
5613 | ((rdlo_bits & 0x7) as u16);
5614 bytes.extend_from_slice(&mov.to_le_bytes());
5615 }
5616
5617 // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5618 let hw1: u16 = 0xEA4F;
5619 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5620 bytes.extend_from_slice(&hw1.to_le_bytes());
5621 bytes.extend_from_slice(&hw2.to_le_bytes());
5622
5623 Ok(bytes)
5624 }
5625
5626 // SelectMove: IT <cond>; MOV{cond} rd, rm
5627 // Conditional move: only execute MOV if condition is true
5628 ArmOp::SelectMove { rd, rm, cond } => {
5629 let rd_bits = reg_to_bits(rd) as u16;
5630 let rm_bits = reg_to_bits(rm) as u16;
5631
5632 // Condition code encoding for IT block
5633 use synth_synthesis::Condition;
5634 let cond_bits: u16 = match cond {
5635 Condition::EQ => 0x0, // Equal
5636 Condition::NE => 0x1, // Not equal
5637 Condition::HS => 0x2, // Higher or same (unsigned >=)
5638 Condition::LO => 0x3, // Lower (unsigned <)
5639 Condition::HI => 0x8, // Higher (unsigned >)
5640 Condition::LS => 0x9, // Lower or same (unsigned <=)
5641 Condition::GE => 0xA, // Greater or equal (signed)
5642 Condition::LT => 0xB, // Less than (signed)
5643 Condition::GT => 0xC, // Greater than (signed)
5644 Condition::LE => 0xD, // Less or equal (signed)
5645 };
5646
5647 // IT <cond>: single Then block (mask = 0x8 for T only)
5648 // IT instruction: 1011 1111 firstcond mask
5649 let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5650
5651 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5652 // This MOV will only execute if condition is true due to IT block
5653 let d_bit = (rd_bits >> 3) & 1;
5654 let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5655
5656 // Emit: IT <cond>, MOV rd, rm
5657 let mut bytes = it_instr.to_le_bytes().to_vec();
5658 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5659 Ok(bytes)
5660 }
5661
5662 // Popcnt: Population count (count set bits)
5663 // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5664 // x = x - ((x >> 1) & 0x55555555);
5665 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5666 // x = (x + (x >> 4)) & 0x0F0F0F0F;
5667 // x = x + (x >> 8);
5668 // x = x + (x >> 16);
5669 // return x & 0x3F;
5670 //
5671 // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5672 // previous expansion borrowed R11 as a second temp — but R11 is
5673 // the WASM linear-memory base, materialized at entry and read by
5674 // every later LDR/STR, and it is NOT in the pushed set, so the
5675 // clobber leaked to the CALLER too (a live memory-safety
5676 // miscompile: loads through `base = x >> 16`). The second temp is
5677 // eliminated the way the healthy i64.popcnt discipline implies —
5678 // never touch an unsaved register — but without its PUSH/POP
5679 // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5680 // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5681 // each AND takes its mask from the instruction itself and R12
5682 // alone carries every intermediate. Straight-line, no branches,
5683 // no stack traffic — nothing to skip on a trap edge.
5684 ArmOp::Popcnt { rd, rm } => {
5685 let rd_bits = reg_to_bits(rd);
5686 // Defensive (#1021): rd = R11/R12/SP/PC would silently
5687 // corrupt the linear-memory base, the expansion's own
5688 // scratch, or the stack. The selector never assigns them
5689 // (pool R0-R8); refuse loudly if that ever changes.
5690 if rd_bits >= 11 {
5691 return Err(synth_core::Error::synthesis(
5692 "Popcnt destination must be R0-R10: R11 is the linear-memory \
5693 base and R12 is the expansion's scratch (#1021)",
5694 ));
5695 }
5696 let mut bytes = Vec::new();
5697
5698 // First, move rm to rd if they're different
5699 if rd != rm {
5700 let rm_bits = reg_to_bits(rm) as u16;
5701 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5702 let d_bit = ((rd_bits as u16) >> 3) & 1;
5703 let mov_instr: u16 =
5704 0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5705 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5706 }
5707
5708 // Step 1: x = x - ((x >> 1) & 0x55555555)
5709 // R12 = rd >> 1
5710 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5711 // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5712 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5713 // rd = rd - R12
5714 bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5715
5716 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5717 // R12 = rd & 0x33333333
5718 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5719 12,
5720 rd_bits,
5721 0x3333_3333,
5722 )?);
5723 // rd = rd >> 2
5724 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5725 // rd = rd & 0x33333333
5726 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5727 rd_bits,
5728 rd_bits,
5729 0x3333_3333,
5730 )?);
5731 // rd = rd + R12
5732 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5733
5734 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5735 // R12 = rd >> 4
5736 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5737 // rd = rd + R12
5738 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5739 // rd = rd & 0x0F0F0F0F
5740 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5741 rd_bits,
5742 rd_bits,
5743 0x0F0F_0F0F,
5744 )?);
5745
5746 // Step 4: x = x + (x >> 8)
5747 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5748 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5749
5750 // Step 5: x = x + (x >> 16)
5751 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5752 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5753
5754 // Step 6: return x & 0x3F
5755 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5756
5757 Ok(bytes)
5758 }
5759
5760 // I64DivU: 64-bit unsigned division using binary long division
5761 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5762 // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5763 //
5764 // #610: the fixed-ABI wrapper marshals the selector-assigned
5765 // operand registers into the core's fixed regs and lands the
5766 // result in rd — pre-#610 this arm IGNORED its register fields,
5767 // so the selector read its rd pair (e.g. R4:R5) after the core's
5768 // own POP restored the stale caller values over it: 0 for every
5769 // input. A zero divisor now traps (UDF #0), per WASM semantics.
5770 ArmOp::I64DivU {
5771 rdlo,
5772 rdhi,
5773 rnlo,
5774 rnhi,
5775 rmlo,
5776 rmhi,
5777 elide_zero_guard,
5778 } => {
5779 let mut bytes = Vec::new();
5780 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5781 // #494 phase 2b: elided only under a certificate-discharged
5782 // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5783 if !elide_zero_guard {
5784 emit_i64_divisor_zero_trap(&mut bytes);
5785 }
5786
5787 // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5788 // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5789 // Encoding: 1011 0100 1111 0000 = 0xB4F0
5790 bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5791
5792 // Initialize quotient (R4:R5) = 0
5793 bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5794 bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5795
5796 // Initialize remainder (R6:R7) = 0
5797 bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5798 bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5799
5800 // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5801 // MOV.W R12, #64: F04F 0C40
5802 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5803 bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5804
5805 // Loop start
5806 let loop_start = bytes.len();
5807
5808 // === Loop body: process one bit ===
5809
5810 // 1. Shift quotient R4:R5 left by 1
5811 // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5812 // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5813 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5814 // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5815 // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5816 // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5817 // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5818 bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5819 bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5820 // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5821 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5822
5823 // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5824 // LSLS R7, R7, #1
5825 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5826 // ORR.W R7, R7, R6, LSR #31
5827 bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5828 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5829 // LSLS R6, R6, #1
5830 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5831 // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5832 bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5833 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5834
5835 // 3. Shift dividend R0:R1 left by 1
5836 // LSLS R1, R1, #1
5837 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5838 // ORR.W R1, R1, R0, LSR #31
5839 bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5840 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5841 // LSLS R0, R0, #1
5842 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5843
5844 // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5845 // Compare high words first: CMP R7, R3
5846 // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5847 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5848 // BHI means R7 > R3 (unsigned) - definitely subtract
5849 // BLO means R7 < R3 - definitely don't subtract
5850 // BEQ means need to check low words
5851
5852 // If high > divisor high: branch to subtract (forward +offset)
5853 // BHI.N +6 (skip CMP, skip BLO, do subtract)
5854 // BHI: 1101 1000 offset8 where cond=1000 (HI)
5855 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5856
5857 // If high < divisor high: branch past subtract
5858 // BLO.N +10 (skip to decrement)
5859 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5860
5861 // High words equal, compare low: CMP R6, R2
5862 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5863 // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5864 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5865
5866 // === Subtract block: remainder -= divisor, quotient |= 1 ===
5867 // SUBS R6, R6, R2
5868 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5869 // SBC R7, R7, R3 (with borrow)
5870 // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5871 bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5872 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5873 // ORR R4, R4, #1 (set bit 0 of quotient low)
5874 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5875 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5876
5877 // === Decrement counter and loop ===
5878 // SUBS.W R12, R12, #1 (decrement loop counter)
5879 // SUBS.W R12, R12, #1: F1BC 0C01
5880 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5881 bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5882
5883 // BNE back to loop_start
5884 let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5885 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5886 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5887 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5888
5889 // === Loop done, move quotient to R0:R1 ===
5890 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5891 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5892
5893 // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5894 // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5895 // Encoding: 1011 1100 1111 0000 = 0xBCF0
5896 bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5897
5898 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5899 Ok(bytes)
5900 }
5901
5902 // I64DivS: 64-bit signed division
5903 // Converts to unsigned, divides, then applies sign
5904 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5905 // -> R0:R1 = quotient (signed)
5906 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5907 ArmOp::I64DivS {
5908 rdlo,
5909 rdhi,
5910 rnlo,
5911 rnhi,
5912 rmlo,
5913 rmhi,
5914 elide_zero_guard,
5915 elide_overflow_guard,
5916 } => {
5917 let mut bytes = Vec::new();
5918 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5919 // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5920 // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5921 // the #633 overflow guard falls ONLY to
5922 // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5923 // divisor-nonzero fact alone must keep it.
5924 if !elide_zero_guard {
5925 emit_i64_divisor_zero_trap(&mut bytes);
5926 }
5927 if !elide_overflow_guard {
5928 // #633: INT64_MIN / -1 overflows — trap like the i32 path
5929 // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5930 emit_i64_divs_overflow_trap(&mut bytes);
5931 }
5932
5933 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5934 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5935 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5936
5937 // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5938 // EOR.W R9, R1, R3
5939 bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5940 bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5941
5942 // If dividend negative (R1 MSB set), negate it
5943 // TST R1, R1 (check sign)
5944 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5945 // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5946 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5947
5948 // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5949 // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5950 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5951 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5952 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5953 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5954 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5955
5956 // If divisor negative (R3 MSB set), negate it
5957 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5958 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5959
5960 // Negate R2:R3
5961 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5962 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5963 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5964 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5965 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5966
5967 // === Now do unsigned division (same as I64DivU) ===
5968 // Initialize quotient (R4:R5) = 0
5969 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5970 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5971 // Initialize remainder (R6:R7) = 0
5972 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5973 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5974 // Initialize loop counter R8 = 64
5975 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5976 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5977
5978 let loop_start = bytes.len();
5979
5980 // Shift quotient left
5981 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5982 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5983 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5984 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5985
5986 // Shift remainder left, OR in MSB of dividend
5987 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5988 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5989 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5990 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5991 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5992 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5993
5994 // Shift dividend left
5995 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5996 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5997 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5998 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5999
6000 // Compare and conditionally subtract
6001 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6002 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6003 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6004 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6005 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6006
6007 // Subtract and set quotient bit
6008 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6009 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6010 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6011 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6012 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6013
6014 // Decrement and loop
6015 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6016 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6017
6018 let branch_offset_bytes = bytes.len() - loop_start + 4;
6019 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6020 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6021 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6022
6023 // Move quotient to R0:R1
6024 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6025 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6026
6027 // If result should be negative (R9 MSB set), negate R0:R1
6028 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6029 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6030 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6031
6032 // Negate result R0:R1
6033 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6034 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6035 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6036 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6037 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6038
6039 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6040 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6041 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6042
6043 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6044 Ok(bytes)
6045 }
6046
6047 // I64RemU: 64-bit unsigned remainder using binary long division
6048 // Same algorithm as I64DivU but returns remainder instead of quotient
6049 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6050 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6051 ArmOp::I64RemU {
6052 rdlo,
6053 rdhi,
6054 rnlo,
6055 rnhi,
6056 rmlo,
6057 rmhi,
6058 elide_zero_guard,
6059 } => {
6060 let mut bytes = Vec::new();
6061 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6062 if !elide_zero_guard {
6063 emit_i64_divisor_zero_trap(&mut bytes);
6064 }
6065
6066 // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6067 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6068 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6069
6070 // Initialize quotient (R4:R5) = 0 (computed but not returned)
6071 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6072 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6073 // Initialize remainder (R6:R7) = 0
6074 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6075 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6076 // Initialize loop counter R8 = 64
6077 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6078 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6079
6080 let loop_start = bytes.len();
6081
6082 // Shift quotient left (not needed for result, but keeps algorithm same)
6083 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6084 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6085 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6086 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6087
6088 // Shift remainder left, OR in MSB of dividend
6089 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6090 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6091 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6092 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6093 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6094 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6095
6096 // Shift dividend left
6097 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6098 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6099 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6100 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6101
6102 // Compare and conditionally subtract
6103 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6104 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6105 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6106 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6107 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6108
6109 // Subtract and set quotient bit
6110 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6111 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6112 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6113 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6114 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6115
6116 // Decrement and loop
6117 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6118 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6119
6120 let branch_offset_bytes = bytes.len() - loop_start + 4;
6121 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6122 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6123 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6124
6125 // Move REMAINDER to R0:R1 (difference from I64DivU)
6126 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6127 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6128
6129 // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6130 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6131 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6132
6133 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6134 Ok(bytes)
6135 }
6136
6137 // I64RemS: 64-bit signed remainder
6138 // Remainder sign follows dividend sign (not quotient rule)
6139 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6140 // -> R0:R1 = remainder (signed, same sign as dividend)
6141 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6142 ArmOp::I64RemS {
6143 rdlo,
6144 rdhi,
6145 rnlo,
6146 rnhi,
6147 rmlo,
6148 rmhi,
6149 elide_zero_guard,
6150 } => {
6151 let mut bytes = Vec::new();
6152 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6153 if !elide_zero_guard {
6154 emit_i64_divisor_zero_trap(&mut bytes);
6155 }
6156
6157 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6158 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6159 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6160
6161 // Save dividend sign in R9 (remainder sign = dividend sign)
6162 // MOV R9, R1 (just need the sign bit)
6163 bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6164
6165 // If dividend negative (R1 MSB set), negate it
6166 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6167 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6168
6169 // Negate R0:R1
6170 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6171 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6172 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6173 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6174 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6175
6176 // If divisor negative (R3 MSB set), negate it
6177 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6178 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6179
6180 // Negate R2:R3
6181 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6182 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6183 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6184 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6185 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6186
6187 // === Unsigned division algorithm ===
6188 // Initialize quotient (R4:R5) = 0
6189 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6190 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6191 // Initialize remainder (R6:R7) = 0
6192 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6193 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6194 // Initialize loop counter R8 = 64
6195 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6196 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6197
6198 let loop_start = bytes.len();
6199
6200 // Shift quotient left
6201 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6202 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6203 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6204 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6205
6206 // Shift remainder left, OR in MSB of dividend
6207 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6208 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6209 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6210 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6211 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6212 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6213
6214 // Shift dividend left
6215 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6216 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6217 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6218 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6219
6220 // Compare and conditionally subtract
6221 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6222 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6223 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6224 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6225 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6226
6227 // Subtract and set quotient bit
6228 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6229 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6230 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6231 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6232 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6233
6234 // Decrement and loop
6235 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6236 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6237
6238 let branch_offset_bytes = bytes.len() - loop_start + 4;
6239 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6240 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6241 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6242
6243 // Move remainder to R0:R1
6244 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6245 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6246
6247 // If original dividend was negative (R9 MSB set), negate remainder
6248 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6249 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6250 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6251
6252 // Negate result R0:R1
6253 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6254 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6255 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6256 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6257 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6258
6259 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6260 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6261 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6262
6263 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6264 Ok(bytes)
6265 }
6266
6267 // === F32 VFP single-precision Thumb-2 encodings ===
6268 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6269 ArmOp::F32Add { sd, sn, sm } => {
6270 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6271 }
6272 ArmOp::F32Sub { sd, sn, sm } => {
6273 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6274 }
6275 ArmOp::F32Mul { sd, sn, sm } => {
6276 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6277 }
6278 ArmOp::F32Div { sd, sn, sm } => {
6279 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6280 }
6281 ArmOp::F32Abs { sd, sm } => {
6282 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6283 }
6284 ArmOp::F32Neg { sd, sm } => {
6285 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6286 }
6287 ArmOp::F32Sqrt { sd, sm } => {
6288 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6289 }
6290
6291 // f32 pseudo-ops — multi-instruction sequences
6292 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6293 ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6294 ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6295 ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6296 ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6297 ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6298 ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6299 ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6300
6301 // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6302 ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6303 ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6304 ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6305 ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6306 ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6307 ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6308
6309 ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6310
6311 ArmOp::F32Load { sd, addr } => {
6312 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6313 }
6314 ArmOp::F32Store { sd, addr } => {
6315 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6316 }
6317
6318 ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6319 ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6320 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6321 Err(synth_core::Error::synthesis(
6322 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6323 ))
6324 }
6325 ArmOp::F32ReinterpretI32 { sd, rm } => {
6326 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6327 }
6328 ArmOp::I32ReinterpretF32 { rd, sm } => {
6329 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6330 }
6331 ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6332 ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6333
6334 // === F64 VFP double-precision Thumb-2 encodings ===
6335 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6336 ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6337 0xEE300B00, dd, dn, dm,
6338 )?)),
6339 ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6340 0xEE300B40, dd, dn, dm,
6341 )?)),
6342 ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6343 0xEE200B00, dd, dn, dm,
6344 )?)),
6345 ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6346 0xEE800B00, dd, dn, dm,
6347 )?)),
6348 ArmOp::F64Abs { dd, dm } => {
6349 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6350 }
6351 ArmOp::F64Neg { dd, dm } => {
6352 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6353 }
6354 ArmOp::F64Sqrt { dd, dm } => {
6355 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6356 }
6357
6358 // f64 pseudo-ops
6359 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6360 ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6361 ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6362 ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6363 ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6364 ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6365 ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6366 ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6367
6368 // f64 comparisons
6369 ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6370 ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6371 ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6372 ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6373 ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6374 ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6375
6376 ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6377
6378 ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6379 0xED900B00, dd, addr,
6380 )?)),
6381 ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6382 0xED800B00, dd, addr,
6383 )?)),
6384
6385 ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6386 ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6387 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6388 Err(synth_core::Error::synthesis(
6389 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6390 ))
6391 }
6392 ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6393 ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6394 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6395 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6396 )),
6397 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6398 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6399 )),
6400 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6401 Err(synth_core::Error::synthesis(
6402 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6403 ))
6404 }
6405 ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6406 ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6407
6408 // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6409
6410 // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6411 ArmOp::I64Add {
6412 rdlo,
6413 rdhi,
6414 rnlo,
6415 rnhi,
6416 rmlo,
6417 rmhi,
6418 } => {
6419 let mut bytes = Vec::new();
6420 // ADDS rdlo, rnlo, rmlo (16-bit)
6421 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6422 rd: *rdlo,
6423 rn: *rnlo,
6424 op2: Operand2::Reg(*rmlo),
6425 })?);
6426 // ADC.W rdhi, rnhi, rmhi (32-bit)
6427 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6428 rd: *rdhi,
6429 rn: *rnhi,
6430 op2: Operand2::Reg(*rmhi),
6431 })?);
6432 Ok(bytes)
6433 }
6434
6435 // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6436 ArmOp::I64Sub {
6437 rdlo,
6438 rdhi,
6439 rnlo,
6440 rnhi,
6441 rmlo,
6442 rmhi,
6443 } => {
6444 let mut bytes = Vec::new();
6445 // SUBS rdlo, rnlo, rmlo (16-bit)
6446 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6447 rd: *rdlo,
6448 rn: *rnlo,
6449 op2: Operand2::Reg(*rmlo),
6450 })?);
6451 // SBC.W rdhi, rnhi, rmhi (32-bit)
6452 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6453 rd: *rdhi,
6454 rn: *rnhi,
6455 op2: Operand2::Reg(*rmhi),
6456 })?);
6457 Ok(bytes)
6458 }
6459
6460 // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6461 ArmOp::I64And {
6462 rdlo,
6463 rdhi,
6464 rnlo,
6465 rnhi,
6466 rmlo,
6467 rmhi,
6468 } => {
6469 let mut bytes = Vec::new();
6470 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6471 rd: *rdlo,
6472 rn: *rnlo,
6473 op2: Operand2::Reg(*rmlo),
6474 })?);
6475 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6476 rd: *rdhi,
6477 rn: *rnhi,
6478 op2: Operand2::Reg(*rmhi),
6479 })?);
6480 Ok(bytes)
6481 }
6482
6483 // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6484 ArmOp::I64Or {
6485 rdlo,
6486 rdhi,
6487 rnlo,
6488 rnhi,
6489 rmlo,
6490 rmhi,
6491 } => {
6492 let mut bytes = Vec::new();
6493 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6494 rd: *rdlo,
6495 rn: *rnlo,
6496 op2: Operand2::Reg(*rmlo),
6497 })?);
6498 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6499 rd: *rdhi,
6500 rn: *rnhi,
6501 op2: Operand2::Reg(*rmhi),
6502 })?);
6503 Ok(bytes)
6504 }
6505
6506 // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6507 ArmOp::I64Xor {
6508 rdlo,
6509 rdhi,
6510 rnlo,
6511 rnhi,
6512 rmlo,
6513 rmhi,
6514 } => {
6515 let mut bytes = Vec::new();
6516 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6517 rd: *rdlo,
6518 rn: *rnlo,
6519 op2: Operand2::Reg(*rmlo),
6520 })?);
6521 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6522 rd: *rdhi,
6523 rn: *rnhi,
6524 op2: Operand2::Reg(*rmhi),
6525 })?);
6526 Ok(bytes)
6527 }
6528
6529 // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6530 ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6531 rd: *rd,
6532 rn_lo: *rnlo,
6533 rn_hi: *rnhi,
6534 }),
6535
6536 // I64 comparisons: delegate to I64SetCond
6537 ArmOp::I64Eq {
6538 rd,
6539 rnlo,
6540 rnhi,
6541 rmlo,
6542 rmhi,
6543 } => self.encode_thumb(&ArmOp::I64SetCond {
6544 rd: *rd,
6545 rn_lo: *rnlo,
6546 rn_hi: *rnhi,
6547 rm_lo: *rmlo,
6548 rm_hi: *rmhi,
6549 cond: synth_synthesis::Condition::EQ,
6550 }),
6551
6552 ArmOp::I64Ne {
6553 rd,
6554 rnlo,
6555 rnhi,
6556 rmlo,
6557 rmhi,
6558 } => self.encode_thumb(&ArmOp::I64SetCond {
6559 rd: *rd,
6560 rn_lo: *rnlo,
6561 rn_hi: *rnhi,
6562 rm_lo: *rmlo,
6563 rm_hi: *rmhi,
6564 cond: synth_synthesis::Condition::NE,
6565 }),
6566
6567 ArmOp::I64LtS {
6568 rd,
6569 rnlo,
6570 rnhi,
6571 rmlo,
6572 rmhi,
6573 } => self.encode_thumb(&ArmOp::I64SetCond {
6574 rd: *rd,
6575 rn_lo: *rnlo,
6576 rn_hi: *rnhi,
6577 rm_lo: *rmlo,
6578 rm_hi: *rmhi,
6579 cond: synth_synthesis::Condition::LT,
6580 }),
6581
6582 ArmOp::I64LtU {
6583 rd,
6584 rnlo,
6585 rnhi,
6586 rmlo,
6587 rmhi,
6588 } => self.encode_thumb(&ArmOp::I64SetCond {
6589 rd: *rd,
6590 rn_lo: *rnlo,
6591 rn_hi: *rnhi,
6592 rm_lo: *rmlo,
6593 rm_hi: *rmhi,
6594 cond: synth_synthesis::Condition::LO,
6595 }),
6596
6597 ArmOp::I64LeS {
6598 rd,
6599 rnlo,
6600 rnhi,
6601 rmlo,
6602 rmhi,
6603 } => self.encode_thumb(&ArmOp::I64SetCond {
6604 rd: *rd,
6605 rn_lo: *rnlo,
6606 rn_hi: *rnhi,
6607 rm_lo: *rmlo,
6608 rm_hi: *rmhi,
6609 cond: synth_synthesis::Condition::LE,
6610 }),
6611
6612 ArmOp::I64LeU {
6613 rd,
6614 rnlo,
6615 rnhi,
6616 rmlo,
6617 rmhi,
6618 } => self.encode_thumb(&ArmOp::I64SetCond {
6619 rd: *rd,
6620 rn_lo: *rnlo,
6621 rn_hi: *rnhi,
6622 rm_lo: *rmlo,
6623 rm_hi: *rmhi,
6624 cond: synth_synthesis::Condition::LS,
6625 }),
6626
6627 ArmOp::I64GtS {
6628 rd,
6629 rnlo,
6630 rnhi,
6631 rmlo,
6632 rmhi,
6633 } => self.encode_thumb(&ArmOp::I64SetCond {
6634 rd: *rd,
6635 rn_lo: *rnlo,
6636 rn_hi: *rnhi,
6637 rm_lo: *rmlo,
6638 rm_hi: *rmhi,
6639 cond: synth_synthesis::Condition::GT,
6640 }),
6641
6642 ArmOp::I64GtU {
6643 rd,
6644 rnlo,
6645 rnhi,
6646 rmlo,
6647 rmhi,
6648 } => self.encode_thumb(&ArmOp::I64SetCond {
6649 rd: *rd,
6650 rn_lo: *rnlo,
6651 rn_hi: *rnhi,
6652 rm_lo: *rmlo,
6653 rm_hi: *rmhi,
6654 cond: synth_synthesis::Condition::HI,
6655 }),
6656
6657 ArmOp::I64GeS {
6658 rd,
6659 rnlo,
6660 rnhi,
6661 rmlo,
6662 rmhi,
6663 } => self.encode_thumb(&ArmOp::I64SetCond {
6664 rd: *rd,
6665 rn_lo: *rnlo,
6666 rn_hi: *rnhi,
6667 rm_lo: *rmlo,
6668 rm_hi: *rmhi,
6669 cond: synth_synthesis::Condition::GE,
6670 }),
6671
6672 ArmOp::I64GeU {
6673 rd,
6674 rnlo,
6675 rnhi,
6676 rmlo,
6677 rmhi,
6678 } => self.encode_thumb(&ArmOp::I64SetCond {
6679 rd: *rd,
6680 rn_lo: *rnlo,
6681 rn_hi: *rnhi,
6682 rm_lo: *rmlo,
6683 rm_hi: *rmhi,
6684 cond: synth_synthesis::Condition::HS,
6685 }),
6686
6687 // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6688 ArmOp::I64Const { rdlo, rdhi, value } => {
6689 let lo32 = *value as u32;
6690 let hi32 = (*value >> 32) as u32;
6691 let mut bytes = Vec::new();
6692 // Load low 32 bits into rdlo
6693 bytes.extend_from_slice(
6694 &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6695 );
6696 if lo32 > 0xFFFF {
6697 bytes.extend_from_slice(
6698 &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6699 );
6700 }
6701 // Load high 32 bits into rdhi
6702 bytes.extend_from_slice(
6703 &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6704 );
6705 if hi32 > 0xFFFF {
6706 bytes.extend_from_slice(
6707 &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6708 );
6709 }
6710 Ok(bytes)
6711 }
6712
6713 // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6714 ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6715 let mut bytes = Vec::new();
6716 // #372/#382: a memory `i64.load` carries an index register
6717 // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6718 // immediate `encode_thumb32_ldr` below uses only base+offset and
6719 // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6720 // i64. `i64_effective_base` materializes the effective base into
6721 // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6722 // the function is NOT skipped — #382), returning the residual
6723 // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6724 // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6725 // form unchanged — so existing output is byte-identical.
6726 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6727 bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6728 bytes.extend_from_slice(&self.encode_thumb32_ldr(
6729 rdhi,
6730 &base,
6731 offset.wrapping_add(4),
6732 )?);
6733 Ok(bytes)
6734 }
6735
6736 // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6737 ArmOp::I64Str { rdlo, rdhi, addr } => {
6738 let mut bytes = Vec::new();
6739 // #372/#382: same index-materialization + large-offset fold as
6740 // I64Ldr (see above).
6741 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6742 bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6743 bytes.extend_from_slice(&self.encode_thumb32_str(
6744 rdhi,
6745 &base,
6746 offset.wrapping_add(4),
6747 )?);
6748 Ok(bytes)
6749 }
6750
6751 // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6752 ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6753 let mut bytes = Vec::new();
6754 if rdlo != rn {
6755 // MOV rdlo, rn (16-bit)
6756 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6757 rd: *rdlo,
6758 op2: Operand2::Reg(*rn),
6759 })?);
6760 }
6761 // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6762 bytes.extend_from_slice(
6763 &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6764 );
6765 Ok(bytes)
6766 }
6767
6768 // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6769 ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6770 let mut bytes = Vec::new();
6771 if rdlo != rn {
6772 // MOV rdlo, rn
6773 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6774 rd: *rdlo,
6775 op2: Operand2::Reg(*rn),
6776 })?);
6777 }
6778 // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6779 // site with no branches in the expansion — before the fix this
6780 // emitted the literal two-instruction stream [4608, 2800], half
6781 // of which was `CMP r0,#0` rather than the high-word clear, so
6782 // every i64.extend_i32_u into a high pair leaked stale bits.
6783 emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6784 Ok(bytes)
6785 }
6786
6787 // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6788 ArmOp::I32WrapI64 { rd, rnlo } => {
6789 if rd == rnlo {
6790 // No-op: already in the right register
6791 let instr: u16 = 0xBF00; // NOP
6792 Ok(instr.to_le_bytes().to_vec())
6793 } else {
6794 // MOV rd, rnlo
6795 self.encode_thumb(&ArmOp::Mov {
6796 rd: *rd,
6797 op2: Operand2::Reg(*rnlo),
6798 })
6799 }
6800 }
6801
6802 // ===== Helium MVE operations (Thumb-2 encoding) =====
6803 ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6804 ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6805 ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6806 ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6807 0xEF000150, qd, qn, qm,
6808 ))),
6809 ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6810 0xEF200150, qd, qn, qm,
6811 ))),
6812 ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6813 0xFF000150, qd, qn, qm,
6814 ))),
6815 ArmOp::MveMvn { qd, qm } => {
6816 // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6817 let qd_enc = qreg_to_num(qd);
6818 let qm_enc = qreg_to_num(qm);
6819 let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6820 Ok(vfp_to_thumb_bytes(instr))
6821 }
6822 ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6823 0xEF100150, qd, qn, qm,
6824 ))),
6825 ArmOp::MveAddI { qd, qn, qm, size } => {
6826 let sz = mve_size_bits(size);
6827 let base: u32 = 0xEF000840 | (sz << 20);
6828 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6829 }
6830 ArmOp::MveSubI { qd, qn, qm, size } => {
6831 let sz = mve_size_bits(size);
6832 let base: u32 = 0xFF000840 | (sz << 20);
6833 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6834 }
6835 ArmOp::MveMulI { qd, qn, qm, size } => {
6836 let sz = mve_size_bits(size);
6837 let base: u32 = 0xEF000950 | (sz << 20);
6838 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6839 }
6840 ArmOp::MveNegI { qd, qm, size } => {
6841 let sz = mve_size_bits(size);
6842 // VNEG.Sx Qd, Qm
6843 let qd_enc = qreg_to_num(qd);
6844 let qm_enc = qreg_to_num(qm);
6845 let base: u32 = 0xFFB103C0 | (sz << 18);
6846 let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6847 Ok(vfp_to_thumb_bytes(instr))
6848 }
6849 ArmOp::MveDup { qd, rn, size } => {
6850 let sz = mve_size_bits(size);
6851 let qd_enc = qreg_to_num(qd);
6852 let rn_bits = reg_to_bits(rn);
6853 // VDUP.sz Qd, Rn: EEA0 0B10 variant
6854 // size encoding: 00=32, 01=16, 10=8
6855 let be = match sz {
6856 0 => 0b00u32, // 8-bit
6857 1 => 0b01, // 16-bit
6858 _ => 0b00, // 32-bit (default)
6859 };
6860 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6861 Ok(vfp_to_thumb_bytes(instr))
6862 }
6863 ArmOp::MveExtractLane { rd, qn, lane, size } => {
6864 let qn_enc = qreg_to_num(qn);
6865 let rd_bits = reg_to_bits(rd);
6866 // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6867 // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6868 let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6869 let lane_in_d = (*lane as u32) & 1;
6870 let _sz = mve_size_bits(size);
6871 // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6872 let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6873 Ok(vfp_to_thumb_bytes(instr))
6874 }
6875 ArmOp::MveInsertLane { qd, rn, lane, size } => {
6876 let qd_enc = qreg_to_num(qd);
6877 let rn_bits = reg_to_bits(rn);
6878 let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6879 let lane_in_d = (*lane as u32) & 1;
6880 let _sz = mve_size_bits(size);
6881 // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6882 let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6883 Ok(vfp_to_thumb_bytes(instr))
6884 }
6885
6886 // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6887 ArmOp::MveCmpEqI { qd, qn, qm, size }
6888 | ArmOp::MveCmpNeI { qd, qn, qm, size }
6889 | ArmOp::MveCmpLtS { qd, qn, qm, size }
6890 | ArmOp::MveCmpLtU { qd, qn, qm, size }
6891 | ArmOp::MveCmpGtS { qd, qn, qm, size }
6892 | ArmOp::MveCmpGtU { qd, qn, qm, size }
6893 | ArmOp::MveCmpLeS { qd, qn, qm, size }
6894 | ArmOp::MveCmpLeU { qd, qn, qm, size }
6895 | ArmOp::MveCmpGeS { qd, qn, qm, size }
6896 | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6897 // Encode as VADD (placeholder encoding — real implementation
6898 // would use VCMP + VPSEL pair)
6899 let sz = mve_size_bits(size);
6900 let base: u32 = 0xEF000840 | (sz << 20);
6901 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6902 }
6903
6904 // f32x4 MVE arithmetic
6905 ArmOp::MveAddF32 { qd, qn, qm } => {
6906 // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6907 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6908 }
6909 ArmOp::MveSubF32 { qd, qn, qm } => {
6910 // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6911 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6912 }
6913 ArmOp::MveMulF32 { qd, qn, qm } => {
6914 // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6915 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6916 }
6917 ArmOp::MveNegF32 { qd, qm } => {
6918 let qd_enc = qreg_to_num(qd);
6919 let qm_enc = qreg_to_num(qm);
6920 // VNEG.F32 Qd, Qm: FFB907C0
6921 let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6922 Ok(vfp_to_thumb_bytes(instr))
6923 }
6924 ArmOp::MveAbsF32 { qd, qm } => {
6925 let qd_enc = qreg_to_num(qd);
6926 let qm_enc = qreg_to_num(qm);
6927 // VABS.F32 Qd, Qm: FFB90740
6928 let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6929 Ok(vfp_to_thumb_bytes(instr))
6930 }
6931 ArmOp::MveCmpEqF32 { qd, qn, qm }
6932 | ArmOp::MveCmpNeF32 { qd, qn, qm }
6933 | ArmOp::MveCmpLtF32 { qd, qn, qm }
6934 | ArmOp::MveCmpLeF32 { qd, qn, qm }
6935 | ArmOp::MveCmpGtF32 { qd, qn, qm }
6936 | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6937 // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6938 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6939 }
6940 ArmOp::MveDupF32 { qd, rn } => {
6941 let qd_enc = qreg_to_num(qd);
6942 let rn_bits = reg_to_bits(rn);
6943 // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6944 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6945 Ok(vfp_to_thumb_bytes(instr))
6946 }
6947 ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6948 let qn_enc = qreg_to_num(qn);
6949 let rd_bits = reg_to_bits(rd);
6950 // VMOV Rd, Sn where Sn = Q*4 + lane
6951 let s_num = qn_enc * 4 + (*lane as u32);
6952 let (vn, n) = encode_sreg(s_num);
6953 let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6954 Ok(vfp_to_thumb_bytes(instr))
6955 }
6956 ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6957 let qd_enc = qreg_to_num(qd);
6958 let rn_bits = reg_to_bits(rn);
6959 // VMOV Sn, Rn where Sn = Q*4 + lane
6960 let s_num = qd_enc * 4 + (*lane as u32);
6961 let (vn, n) = encode_sreg(s_num);
6962 let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6963 Ok(vfp_to_thumb_bytes(instr))
6964 }
6965 ArmOp::MveDivF32 { qd, qn, qm } => {
6966 // Lane-wise: extract 4 S-regs, VDIV, insert back
6967 self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6968 }
6969 ArmOp::MveSqrtF32 { qd, qm } => {
6970 // Lane-wise: extract 4 S-regs, VSQRT, insert back
6971 self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6972 }
6973
6974 // Catch-all for any remaining ops
6975 _ => {
6976 let instr: u16 = 0xBF00; // NOP
6977 Ok(instr.to_le_bytes().to_vec())
6978 }
6979 }
6980 }
6981
6982 // === Thumb-2 VFP multi-instruction helpers ===
6983
6984 /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6985 fn encode_thumb_f32_compare(
6986 &self,
6987 rd: &Reg,
6988 sn: &VfpReg,
6989 sm: &VfpReg,
6990 cond_code: u32,
6991 ) -> Result<Vec<u8>> {
6992 let mut bytes = Vec::new();
6993 let rd_bits = reg_to_bits(rd);
6994
6995 // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
6996 // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
6997 // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
6998 // transferred from the VFP compare, so the following `IT<cond>` read
6999 // stale flags and every f32 comparison silently returned 0 (verified:
7000 // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
7001 // because it deliberately skipped compare EXECUTION on a false premise
7002 // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
7003 // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
7004 // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
7005 // untouched — only the byte ORDER differs.
7006
7007 // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
7008 // is immediately overwritten by the VMRS below.
7009 if rd_bits < 8 {
7010 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7011 bytes.extend_from_slice(&movs_zero.to_le_bytes());
7012 } else {
7013 // MOV.W Rd, #0 (32-bit Thumb-2)
7014 let hw1: u16 = 0xF04F;
7015 let hw2: u16 = (rd_bits as u16) << 8;
7016 bytes.extend_from_slice(&hw1.to_le_bytes());
7017 bytes.extend_from_slice(&hw2.to_le_bytes());
7018 }
7019
7020 // VCMP.F32 Sn, Sm
7021 let sn_num = vfp_sreg_to_num(sn)?;
7022 let sm_num = vfp_sreg_to_num(sm)?;
7023 let (vd, d) = encode_sreg(sn_num);
7024 let (vm, m) = encode_sreg(sm_num);
7025 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7026 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7027
7028 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7029 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7030
7031 // IT<cond> — If-Then for conditional MOV
7032 // IT encoding: 1011 1111 cond(4) mask(4)
7033 // mask = 0x8 for single "then" (IT)
7034 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7035 bytes.extend_from_slice(&it.to_le_bytes());
7036
7037 // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7038 if rd_bits < 8 {
7039 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7040 bytes.extend_from_slice(&mov_one.to_le_bytes());
7041 } else {
7042 // MOV.W Rd, #1 (32-bit)
7043 let hw1: u16 = 0xF04F;
7044 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7045 bytes.extend_from_slice(&hw1.to_le_bytes());
7046 bytes.extend_from_slice(&hw2.to_le_bytes());
7047 }
7048
7049 Ok(bytes)
7050 }
7051
7052 /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7053 fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7054 let mut bytes = Vec::new();
7055 let bits = value.to_bits();
7056 let rt: u32 = 12; // R12/IP as temp
7057
7058 // MOVW R12, #lo16
7059 // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7060 let lo16 = bits & 0xFFFF;
7061 let imm4 = (lo16 >> 12) & 0xF;
7062 let i_bit = (lo16 >> 11) & 1;
7063 let imm3 = (lo16 >> 8) & 0x7;
7064 let imm8 = lo16 & 0xFF;
7065 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7066 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7067 bytes.extend_from_slice(&hw1.to_le_bytes());
7068 bytes.extend_from_slice(&hw2.to_le_bytes());
7069
7070 // MOVT R12, #hi16
7071 let hi16 = (bits >> 16) & 0xFFFF;
7072 let imm4 = (hi16 >> 12) & 0xF;
7073 let i_bit = (hi16 >> 11) & 1;
7074 let imm3 = (hi16 >> 8) & 0x7;
7075 let imm8 = hi16 & 0xFF;
7076 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7077 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7078 bytes.extend_from_slice(&hw1.to_le_bytes());
7079 bytes.extend_from_slice(&hw2.to_le_bytes());
7080
7081 // VMOV Sd, R12
7082 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7083 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7084
7085 Ok(bytes)
7086 }
7087
7088 /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7089 fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7090 let mut bytes = Vec::new();
7091
7092 // VMOV Sd, Rm
7093 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7094 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7095
7096 // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7097 // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7098 // (GI-FPU-002: previously swapped — see the ARM32 twin).
7099 let sd_num = vfp_sreg_to_num(sd)?;
7100 let (vd, d) = encode_sreg(sd_num);
7101 let (vm, m) = encode_sreg(sd_num);
7102 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7103 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7104 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7105
7106 Ok(bytes)
7107 }
7108
7109 /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7110 /// Encode F32 rounding as Thumb-2.
7111 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7112 ///
7113 /// For trunc: uses VCVTR.S32.F32 (always truncates).
7114 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7115 /// then restores FPSCR.
7116 fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7117 let mut bytes = Vec::new();
7118 let sm_num = vfp_sreg_to_num(sm)?;
7119 let sd_num = vfp_sreg_to_num(sd)?;
7120 let (vd_s, d_s) = encode_sreg(sd_num);
7121 let (vm_s, m_s) = encode_sreg(sm_num);
7122
7123 if mode == 0b11 {
7124 // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7125 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7126 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7127 } else {
7128 // ceil/floor/nearest: manipulate FPSCR rounding mode
7129 let rt: u32 = 12; // R12/IP as temp
7130
7131 // VMRS R12, FPSCR
7132 let vmrs = 0xEEF10A10 | (rt << 12);
7133 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7134
7135 // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7136 // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7137 // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7138 // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7139 let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7140 let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7141 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7142 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7143
7144 // ORR.W R12, R12, #(mode << 22)
7145 if mode != 0 {
7146 let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7147 let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7148 bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7149 bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7150 }
7151
7152 // VMSR FPSCR, R12
7153 let vmsr = 0xEEE10A10 | (rt << 12);
7154 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7155
7156 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7157 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7158 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7159
7160 // Restore FPSCR: clear rmode bits back to nearest (default)
7161 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7162 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7163 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7164 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7165 }
7166
7167 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7168 let (vd2, d2) = encode_sreg(sd_num);
7169 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7170 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7171
7172 Ok(bytes)
7173 }
7174
7175 /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7176 fn encode_thumb_f32_minmax(
7177 &self,
7178 sd: &VfpReg,
7179 sn: &VfpReg,
7180 sm: &VfpReg,
7181 is_min: bool,
7182 ) -> Result<Vec<u8>> {
7183 let mut bytes = Vec::new();
7184 let sn_num = vfp_sreg_to_num(sn)?;
7185 let sm_num = vfp_sreg_to_num(sm)?;
7186 let sd_num = vfp_sreg_to_num(sd)?;
7187
7188 // VMOV.F32 Sd, Sn
7189 let (vd, d) = encode_sreg(sd_num);
7190 let (vn, n) = encode_sreg(sn_num);
7191 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7192 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7193
7194 // VCMP.F32 Sn, Sm
7195 let (vm, m) = encode_sreg(sm_num);
7196 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7197 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7198
7199 // VMRS APSR_nzcv, FPSCR
7200 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7201
7202 // IT GT (for min) or IT MI (for max)
7203 let cond: u16 = if is_min { 0xC } else { 0x4 };
7204 let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7205 bytes.extend_from_slice(&it.to_le_bytes());
7206
7207 // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7208 let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7209 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7210
7211 Ok(bytes)
7212 }
7213
7214 /// Encode F32 copysign as Thumb-2
7215 /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7216 /// encoder scratch, #212), the flags, and Sd:
7217 ///
7218 /// VMOV R12, Sm ; CMP R12, #0 (N flag = the sign bit)
7219 /// VABS.F32 Sd, Sn (magnitude, sign cleared)
7220 /// IT MI ; VNEG.F32(MI) Sd, Sd
7221 ///
7222 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7223 /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7224 /// is safe. (The previous sequence staged the magnitude through R0 —
7225 /// clobbering a live allocator-owned value, the #615 class; caught while
7226 /// composing the F64 twin for #369.)
7227 fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7228 let mut bytes = Vec::new();
7229
7230 // VMOV R12, Sm (sign source bits)
7231 bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7232 false,
7233 sm,
7234 &Reg::R12,
7235 )?));
7236 // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7237 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7238 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7239 // VABS.F32 Sd, Sn
7240 let sd_num = vfp_sreg_to_num(sd)?;
7241 let sn_num = vfp_sreg_to_num(sn)?;
7242 let (vd, d) = encode_sreg(sd_num);
7243 let (vn, n) = encode_sreg(sn_num);
7244 let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7245 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7246 // IT MI ; VNEG.F32(MI) Sd, Sd
7247 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7248 let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7249 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7250
7251 Ok(bytes)
7252 }
7253
7254 /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7255 fn encode_thumb_f64_compare(
7256 &self,
7257 rd: &Reg,
7258 dn: &VfpReg,
7259 dm: &VfpReg,
7260 cond_code: u32,
7261 ) -> Result<Vec<u8>> {
7262 let mut bytes = Vec::new();
7263 let rd_bits = reg_to_bits(rd);
7264
7265 // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7266 // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7267 // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7268 // transferred, so the following `IT<cond>` read stale flags and every
7269 // f64 comparison silently returned 0 — the exact bug the f32 compare
7270 // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7271 // FIRST (its flag side effect is overwritten by the VMRS), then
7272 // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7273 // unchanged.
7274
7275 // MOVS Rd, #0
7276 if rd_bits < 8 {
7277 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7278 bytes.extend_from_slice(&movs_zero.to_le_bytes());
7279 } else {
7280 let hw1: u16 = 0xF04F;
7281 let hw2: u16 = (rd_bits as u16) << 8;
7282 bytes.extend_from_slice(&hw1.to_le_bytes());
7283 bytes.extend_from_slice(&hw2.to_le_bytes());
7284 }
7285
7286 // VCMP.F64 Dn, Dm
7287 let dn_num = vfp_dreg_to_num(dn)?;
7288 let dm_num = vfp_dreg_to_num(dm)?;
7289 let (vd, d) = encode_dreg(dn_num);
7290 let (vm, m) = encode_dreg(dm_num);
7291 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7292 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7293
7294 // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7295 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7296
7297 // IT<cond>
7298 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7299 bytes.extend_from_slice(&it.to_le_bytes());
7300
7301 // MOV Rd, #1
7302 if rd_bits < 8 {
7303 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7304 bytes.extend_from_slice(&mov_one.to_le_bytes());
7305 } else {
7306 let hw1: u16 = 0xF04F;
7307 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7308 bytes.extend_from_slice(&hw1.to_le_bytes());
7309 bytes.extend_from_slice(&hw2.to_le_bytes());
7310 }
7311
7312 Ok(bytes)
7313 }
7314
7315 /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7316 fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7317 let mut bytes = Vec::new();
7318 let bits = value.to_bits();
7319 let lo32 = bits as u32;
7320 let hi32 = (bits >> 32) as u32;
7321
7322 // MOVW R0, #lo16(lo32)
7323 let lo16 = lo32 & 0xFFFF;
7324 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7325
7326 // MOVT R0, #hi16(lo32)
7327 let hi16 = (lo32 >> 16) & 0xFFFF;
7328 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7329
7330 // MOVW R12, #lo16(hi32)
7331 let lo16 = hi32 & 0xFFFF;
7332 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7333
7334 // MOVT R12, #hi16(hi32)
7335 let hi16 = (hi32 >> 16) & 0xFFFF;
7336 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7337
7338 // VMOV Dd, R0, R12
7339 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7340 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7341
7342 Ok(bytes)
7343 }
7344
7345 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7346 /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7347 /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7348 /// definition — never S0 (which may hold a live value; the previous
7349 /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7350 /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7351 /// twin had; latent here because f64.convert_i32_* was decode-dropped
7352 /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7353 /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7354 fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7355 let dd_num = vfp_dreg_to_num(dd)?;
7356 if dd_num > 7 {
7357 return Err(synth_core::Error::synthesis(format!(
7358 "F64ConvertI32: destination {dd:?} has no S-register alias \
7359 (D8..D15) — the selector allocates only D0..D7"
7360 )));
7361 }
7362 let mut bytes = Vec::new();
7363
7364 // VMOV S(2d), Rm — stage the integer in the destination's low word.
7365 let (vn_s, n_s) = encode_sreg(2 * dd_num);
7366 let rt = reg_to_bits(rm);
7367 let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7368 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7369
7370 // VCVT.F64.S32/U32 Dd, S(2d)
7371 let (vd, d) = encode_dreg(dd_num);
7372 let (vm, m) = encode_sreg(2 * dd_num);
7373 let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7374 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7375 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7376
7377 Ok(bytes)
7378 }
7379
7380 /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7381 fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7382 let dd_num = vfp_dreg_to_num(dd)?;
7383 let sm_num = vfp_sreg_to_num(sm)?;
7384 let (vd, d) = encode_dreg(dd_num);
7385 let (vm, m) = encode_sreg(sm_num);
7386
7387 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7388 Ok(vfp_to_thumb_bytes(vcvt))
7389 }
7390
7391 /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7392 /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7393 /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7394 fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7395 let sd_num = vfp_sreg_to_num(sd)?;
7396 let dm_num = vfp_dreg_to_num(dm)?;
7397 let (vd, d) = encode_sreg(sd_num);
7398 let (vm, m) = encode_dreg(dm_num);
7399
7400 let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7401 Ok(vfp_to_thumb_bytes(vcvt))
7402 }
7403
7404 /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7405 ///
7406 /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7407 /// clobbering half of an operand the selector has already popped. The
7408 /// overlapping write is well-defined: VCVT reads its source operand before
7409 /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7410 ///
7411 /// # The one precondition, and who actually provides it
7412 ///
7413 /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7414 /// whole safety argument, and it is worth naming the guarantor precisely
7415 /// (#946): **`select_with_stack`** provides it, by copying a home into a
7416 /// fresh D-temp first. Visible in the shipped output for
7417 /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7418 ///
7419 /// ```text
7420 /// vmov r1, r2, d0 ; read the param out of its AAPCS-VFP home D0
7421 /// vmov d1, r1, r2 ; ...into a fresh D-temp
7422 /// vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7423 /// ```
7424 ///
7425 /// `InstructionSelector::select` / `select_default` do NOT provide it —
7426 /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7427 /// or home test. That path is not reachable from `synth compile`
7428 /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7429 /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7430 /// not a live miscompile — but a caller reaching that `pub` API directly
7431 /// gets no such guarantee.
7432 ///
7433 /// # What this deliberately does NOT claim
7434 ///
7435 /// An earlier version of this comment said the staging register is "never
7436 /// S0, which may hold an unrelated live value (the #615 class)". **That is
7437 /// false**, and measurably so: for
7438 /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7439 /// compiler emits `vcvt.s32.f64 s0, d0`.
7440 ///
7441 /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7442 /// here it is always the low half of `dm` itself, which the precondition
7443 /// above already makes dead. Naming a guard the code does not have — and
7444 /// does not need — invites a future reader to lean on it. The dead-temp
7445 /// precondition is the only thing holding this up.
7446 fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7447 let dm_num = vfp_dreg_to_num(dm)?;
7448 if dm_num > 7 {
7449 return Err(synth_core::Error::synthesis(format!(
7450 "I32TruncF64: source {dm:?} has no S-register alias \
7451 (D8..D15) — the selector allocates only D0..D7"
7452 )));
7453 }
7454 let mut bytes = Vec::new();
7455
7456 // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7457 // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7458 let (vm, m) = encode_dreg(dm_num);
7459 let (vd_s, d_s) = encode_sreg(2 * dm_num);
7460 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7461 let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7462 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7463
7464 // VMOV Rd, S(2m)
7465 let rt = reg_to_bits(rd);
7466 let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7467 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7468
7469 Ok(bytes)
7470 }
7471
7472 /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7473 /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7474 /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7475 /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7476 /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7477 /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7478 /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7479 /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7480 /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7481 /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7482 /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7483 /// IT-conditional; none of these sequences emits them inside an IT block).
7484 fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7485 let dd_num = vfp_dreg_to_num(dd)?;
7486 let dm_num = vfp_dreg_to_num(dm)?;
7487 let (vd, d) = encode_dreg(dd_num);
7488 let (vm, m) = encode_dreg(dm_num);
7489 // clang-verified bases (thumbv7em, fpv5-d16):
7490 // vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7491 // vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7492 let base: u32 = match mode {
7493 0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7494 0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7495 0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7496 _ => 0xEEB60BC0, // VRINTZ.F64 (round toward zero)
7497 };
7498 Ok(vfp_to_thumb_bytes(
7499 base | (d << 22) | (vd << 12) | (m << 5) | vm,
7500 ))
7501 }
7502
7503 /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7504 ///
7505 /// VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7506 /// VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm (FPv5; -0.0 < +0.0 ordered)
7507 /// IT VS ; VADD.F64(VS) Dd, Dn, Dm (unordered ⇒ NaN-propagating)
7508 ///
7509 /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7510 /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7511 /// VADD overwrites the result with a quiet NaN whenever the compare was
7512 /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7513 /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7514 /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7515 /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7516 ///
7517 /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7518 /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7519 /// destination while both sources are still marked live).
7520 fn encode_thumb_f64_minmax(
7521 &self,
7522 dd: &VfpReg,
7523 dn: &VfpReg,
7524 dm: &VfpReg,
7525 is_min: bool,
7526 ) -> Result<Vec<u8>> {
7527 if dd == dn || dd == dm {
7528 return Err(synth_core::Error::synthesis(format!(
7529 "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7530 the unordered NaN fix-up would read a clobbered operand \
7531 (compiler bug: the selector must allocate a fresh D-temp)",
7532 if is_min { "Min" } else { "Max" },
7533 )));
7534 }
7535 let mut bytes = Vec::new();
7536 let dd_num = vfp_dreg_to_num(dd)?;
7537 let dn_num = vfp_dreg_to_num(dn)?;
7538 let dm_num = vfp_dreg_to_num(dm)?;
7539 let (vd, d) = encode_dreg(dd_num);
7540 let (vn, n) = encode_dreg(dn_num);
7541 let (vm, m) = encode_dreg(dm_num);
7542
7543 // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7544 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7545 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7546 // VMRS APSR_nzcv, FPSCR
7547 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7548 // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7549 // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7550 let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7551 let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7552 bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7553 // IT VS (unordered ⇒ at least one NaN operand)
7554 bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7555 // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7556 let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7557 bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7558
7559 Ok(bytes)
7560 }
7561
7562 /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7563 /// encoder scratch, #212), the flags, and Dd:
7564 ///
7565 /// VMOV R12, S(2m+1) (high word of the SIGN source Dm)
7566 /// CMP R12, #0 (N flag = the sign bit)
7567 /// VABS.F64 Dd, Dn (magnitude, sign cleared)
7568 /// IT MI ; VNEG.F64(MI) Dd, Dd
7569 ///
7570 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7571 /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7572 /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7573 /// allocator's back — the #615 class.)
7574 fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7575 let dm_num = vfp_dreg_to_num(dm)?;
7576 if dm_num > 7 {
7577 return Err(synth_core::Error::synthesis(format!(
7578 "F64Copysign: sign source {dm:?} has no S-register alias \
7579 (D8..D15) — the selector allocates only D0..D7"
7580 )));
7581 }
7582 let mut bytes = Vec::new();
7583 // VMOV R12, S(2m+1) — the sign source's high word.
7584 let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7585 let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7586 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7587 // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7588 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7589 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7590 // VABS.F64 Dd, Dn
7591 let dd_num = vfp_dreg_to_num(dd)?;
7592 let dn_num = vfp_dreg_to_num(dn)?;
7593 let (vd, d) = encode_dreg(dd_num);
7594 let (vn, n) = encode_dreg(dn_num);
7595 let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7596 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7597 // IT MI ; VNEG.F64(MI) Dd, Dd
7598 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7599 let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7600 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7601
7602 Ok(bytes)
7603 }
7604
7605 /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7606 fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7607 let mut bytes = Vec::new();
7608
7609 let sm_num = vfp_sreg_to_num(sm)?;
7610 let (vd, d) = encode_sreg(sm_num);
7611 let (vm, m) = encode_sreg(sm_num);
7612 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7613 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7614 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7615
7616 // VMOV Rd, Sm
7617 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7618 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7619
7620 Ok(bytes)
7621 }
7622
7623 // === Thumb-2 32-bit encoding helpers ===
7624
7625 /// Encode Thumb-2 32-bit ADD with immediate
7626 fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7627 let rd_bits = reg_to_bits(rd);
7628 let rn_bits = reg_to_bits(rn);
7629
7630 // The `i:imm3:imm8` field is split the same way for both forms.
7631 let i_bit = (imm >> 11) & 1;
7632 let imm3 = (imm >> 8) & 0x7;
7633 let imm8 = imm & 0xFF;
7634
7635 let hw1_base = if imm <= 0xFF {
7636 // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7637 // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7638 // correct — keep this form so existing encodings stay bit-identical.
7639 0xF100
7640 } else if imm <= 0xFFF {
7641 // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7642 // This is what makes `add sp, sp, #frame` correct for frame sizes
7643 // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7644 0xF200
7645 } else {
7646 return Err(synth_core::Error::synthesis(
7647 "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7648 ));
7649 };
7650
7651 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7652 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7653
7654 let mut bytes = hw1.to_le_bytes().to_vec();
7655 bytes.extend_from_slice(&hw2.to_le_bytes());
7656 Ok(bytes)
7657 }
7658
7659 /// Encode Thumb-2 32-bit SUB with immediate
7660 fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7661 let rd_bits = reg_to_bits(rd);
7662 let rn_bits = reg_to_bits(rn);
7663
7664 let i_bit = (imm >> 11) & 1;
7665 let imm3 = (imm >> 8) & 0x7;
7666 let imm8 = imm & 0xFF;
7667
7668 let hw1_base = if imm <= 0xFF {
7669 // SUB.W (T3) modified immediate — correct for the zero-extended byte
7670 // (imm <= 0xFF). Kept bit-identical for existing encodings.
7671 0xF1A0
7672 } else if imm <= 0xFFF {
7673 // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7674 // `sub sp, sp, #frame` correct for frame sizes >= 256.
7675 0xF2A0
7676 } else {
7677 return Err(synth_core::Error::synthesis(
7678 "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7679 ));
7680 };
7681
7682 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7683 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7684
7685 let mut bytes = hw1.to_le_bytes().to_vec();
7686 bytes.extend_from_slice(&hw2.to_le_bytes());
7687 Ok(bytes)
7688 }
7689
7690 /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7691 fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7692 let rd_bits = reg_to_bits(rd);
7693 let rn_bits = reg_to_bits(rn);
7694
7695 // ADDS.W (flag-setting) has only the modified-immediate form — error on
7696 // an un-encodable value rather than silently add the wrong constant.
7697 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7698 synth_core::Error::synthesis(
7699 "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7700 )
7701 })?;
7702 let i_bit = (field >> 11) & 1;
7703 let imm3 = (field >> 8) & 0x7;
7704 let imm8 = field & 0xFF;
7705
7706 // ADDS.W Rd, Rn, #imm (with S=1)
7707 // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7708 let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7709 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7710
7711 let mut bytes = hw1.to_le_bytes().to_vec();
7712 bytes.extend_from_slice(&hw2.to_le_bytes());
7713 Ok(bytes)
7714 }
7715
7716 /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7717 fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7718 let rd_bits = reg_to_bits(rd);
7719 let rn_bits = reg_to_bits(rn);
7720
7721 // SUBS.W (flag-setting) has only the modified-immediate form — error on
7722 // an un-encodable value rather than silently subtract the wrong constant.
7723 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7724 synth_core::Error::synthesis(
7725 "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7726 )
7727 })?;
7728 let i_bit = (field >> 11) & 1;
7729 let imm3 = (field >> 8) & 0x7;
7730 let imm8 = field & 0xFF;
7731
7732 // SUBS.W Rd, Rn, #imm (with S=1)
7733 // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7734 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7735 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7736
7737 let mut bytes = hw1.to_le_bytes().to_vec();
7738 bytes.extend_from_slice(&hw2.to_le_bytes());
7739 Ok(bytes)
7740 }
7741
7742 /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7743 ///
7744 /// # Contract (Verus-style)
7745 /// ```text
7746 /// requires rd <= R14
7747 /// ensures result.len() == 4
7748 /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7749 /// ```
7750 fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7751 let rd_bits = reg_to_bits(rd);
7752 reg_bits_checked(rd_bits)?;
7753 let imm16 = imm & 0xFFFF;
7754
7755 // MOVW Rd, #imm16
7756 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7757 let imm4 = (imm16 >> 12) & 0xF;
7758 let i_bit = (imm16 >> 11) & 1;
7759 let imm3 = (imm16 >> 8) & 0x7;
7760 let imm8 = imm16 & 0xFF;
7761
7762 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7763 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7764
7765 let mut bytes = hw1.to_le_bytes().to_vec();
7766 bytes.extend_from_slice(&hw2.to_le_bytes());
7767 encoding_contracts::verify_thumb32(&bytes);
7768 Ok(bytes)
7769 }
7770
7771 /// Encode Thumb-2 32-bit shift with immediate
7772 ///
7773 /// # Contract (Verus-style)
7774 /// ```text
7775 /// requires rd <= R14, rm <= R14
7776 /// ensures result.len() == 4
7777 /// ```
7778 fn encode_thumb32_shift(
7779 &self,
7780 rd: &Reg,
7781 rm: &Reg,
7782 shift: u32,
7783 shift_type: u8,
7784 ) -> Result<Vec<u8>> {
7785 let rd_bits = reg_to_bits(rd);
7786 let rm_bits = reg_to_bits(rm);
7787 reg_bits_checked(rd_bits)?;
7788 reg_bits_checked(rm_bits)?;
7789 let imm5 = shift & 0x1F;
7790 let imm2 = imm5 & 0x3;
7791 let imm3 = (imm5 >> 2) & 0x7;
7792
7793 // MOV.W Rd, Rm, <shift> #imm
7794 // EA4F 0 imm3 Rd imm2 type Rm
7795 let hw1: u16 = 0xEA4F;
7796 let hw2: u16 =
7797 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7798 as u16;
7799
7800 let mut bytes = hw1.to_le_bytes().to_vec();
7801 bytes.extend_from_slice(&hw2.to_le_bytes());
7802 Ok(bytes)
7803 }
7804
7805 /// Encode Thumb-2 32-bit shift by register
7806 /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7807 /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7808 fn encode_thumb32_shift_reg(
7809 &self,
7810 rd: &Reg,
7811 rn: &Reg,
7812 rm: &Reg,
7813 shift_type: u8,
7814 ) -> Result<Vec<u8>> {
7815 let rd_bits = reg_to_bits(rd);
7816 let rn_bits = reg_to_bits(rn);
7817 let rm_bits = reg_to_bits(rm);
7818
7819 // hw1: 1111 1010 0xx0 Rn
7820 let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7821 // hw2: 1111 Rd 0000 Rm
7822 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7823
7824 let mut bytes = hw1.to_le_bytes().to_vec();
7825 bytes.extend_from_slice(&hw2.to_le_bytes());
7826 Ok(bytes)
7827 }
7828
7829 /// Encode Thumb-2 32-bit CMP with immediate
7830 fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7831 let rn_bits = reg_to_bits(rn);
7832
7833 // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7834 // so an un-encodable immediate MUST be materialized into a register by
7835 // the selector. Error rather than silently compare the wrong constant.
7836 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7837 synth_core::Error::synthesis(
7838 "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7839 )
7840 })?;
7841 let i_bit = (field >> 11) & 1;
7842 let imm3 = (field >> 8) & 0x7;
7843 let imm8 = field & 0xFF;
7844
7845 // CMP.W Rn, #imm
7846 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7847 let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7848
7849 let mut bytes = hw1.to_le_bytes().to_vec();
7850 bytes.extend_from_slice(&hw2.to_le_bytes());
7851 Ok(bytes)
7852 }
7853
7854 /// #372/#382: resolve the base register AND residual immediate offset for an
7855 /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7856 /// `(base, low_offset)`; the caller accesses the halves at `[base,
7857 /// #low_offset]` and `[base, #low_offset + 4]`.
7858 ///
7859 /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7860 /// returns `(addr.base, off)` and emits NOTHING — byte-identical.
7861 /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7862 /// with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7863 /// `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7864 /// Byte-identical to the pre-#382 (#372) behavior.
7865 /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7866 /// high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7867 /// rightly refused it and the WHOLE function was skipped (#382). Instead
7868 /// MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7869 /// the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7870 /// `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7871 /// `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7872 ///
7873 /// The effective address is fully materialized into `ip` BEFORE the halves
7874 /// are accessed, so an `rdlo` aliasing the index register is safe.
7875 ///
7876 /// RQ-63-ARMI64OFF (#1165): the wasm memarg is unsigned and reaches the
7877 /// encoder through the selector's `as i32` cast, so a memarg `>= 2^31`
7878 /// arrives NEGATIVE. It is read back as `u32` (lossless) and the
7879 /// materialization carries the full 32 bits — `base + index + offset (mod
7880 /// 2^32)`, the arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`)
7881 /// and the software bounds guard already use for the same memarg. This
7882 /// used to CLAMP a negative offset to 0, so `i64.load offset=0xfffffff8`
7883 /// silently read `[R11 + addr]` — a wrong address, and a divergence from
7884 /// the i32 form of the same memarg. A FRAME access (no index) with an
7885 /// out-of-range or negative offset is refused by `check_ldst_imm12` in the
7886 /// halves — loud, never clamped.
7887 fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7888 let offset = addr.offset as u32;
7889 match addr.offset_reg {
7890 Some(idx) => {
7891 let ip = Reg::R12;
7892 // The high half sits at +4, which must itself fit imm12.
7893 if offset > 0xFFB {
7894 // Large static offset (#382): fold it (and R11) into ip so the
7895 // imm12 halves stay in range instead of skipping the function.
7896 // ADD ip, index, #offset (index != ip → no add_imm alias trap)
7897 bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7898 // ADD.W ip, ip, base (+ R11)
7899 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7900 reg_to_bits(&ip),
7901 reg_to_bits(&ip),
7902 reg_to_bits(&addr.base),
7903 )?);
7904 Ok((ip, 0))
7905 } else {
7906 // ADD.W ip, addr.base, idx (Thumb-2, byte-verified vs as)
7907 let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7908 let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7909 bytes.extend_from_slice(&hw1.to_le_bytes());
7910 bytes.extend_from_slice(&hw2.to_le_bytes());
7911 Ok((ip, offset))
7912 }
7913 }
7914 None => Ok((addr.base, offset)),
7915 }
7916 }
7917
7918 /// Encode Thumb-2 32-bit LDR
7919 fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7920 let rd_bits = reg_to_bits(rd);
7921 let base_bits = reg_to_bits(base);
7922
7923 // LDR.W Rd, [Rn, #imm12]
7924 check_ldst_imm12(offset)?;
7925 let hw1: u16 = (0xF8D0 | base_bits) as u16;
7926 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7927
7928 let mut bytes = hw1.to_le_bytes().to_vec();
7929 bytes.extend_from_slice(&hw2.to_le_bytes());
7930 Ok(bytes)
7931 }
7932
7933 /// Encode Thumb-2 32-bit STR
7934 fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7935 let rd_bits = reg_to_bits(rd);
7936 let base_bits = reg_to_bits(base);
7937
7938 // STR.W Rd, [Rn, #imm12]
7939 check_ldst_imm12(offset)?;
7940 let hw1: u16 = (0xF8C0 | base_bits) as u16;
7941 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7942
7943 let mut bytes = hw1.to_le_bytes().to_vec();
7944 bytes.extend_from_slice(&hw2.to_le_bytes());
7945 Ok(bytes)
7946 }
7947
7948 /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7949 fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7950 let rd_bits = reg_to_bits(rd);
7951 let base_bits = reg_to_bits(base);
7952 let rm_bits = reg_to_bits(offset_reg);
7953
7954 // LDR.W Rd, [Rn, Rm, LSL #0]
7955 // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7956 // imm2 = 00 for no shift (LSL #0)
7957 let hw1: u16 = (0xF850 | base_bits) as u16;
7958 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7959
7960 let mut bytes = hw1.to_le_bytes().to_vec();
7961 bytes.extend_from_slice(&hw2.to_le_bytes());
7962 Ok(bytes)
7963 }
7964
7965 /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7966 fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7967 let rd_bits = reg_to_bits(rd);
7968 let base_bits = reg_to_bits(base);
7969 let rm_bits = reg_to_bits(offset_reg);
7970
7971 // STR.W Rd, [Rn, Rm, LSL #0]
7972 // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7973 // imm2 = 00 for no shift (LSL #0)
7974 let hw1: u16 = (0xF840 | base_bits) as u16;
7975 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7976
7977 let mut bytes = hw1.to_le_bytes().to_vec();
7978 bytes.extend_from_slice(&hw2.to_le_bytes());
7979 Ok(bytes)
7980 }
7981
7982 // === Sub-word load/store Thumb-2 encoding helpers ===
7983
7984 /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7985 fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7986 let rd_bits = reg_to_bits(rd);
7987 let base_bits = reg_to_bits(base);
7988 // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7989 check_ldst_imm12(offset)?;
7990 let hw1: u16 = (0xF890 | base_bits) as u16;
7991 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7992 let mut bytes = hw1.to_le_bytes().to_vec();
7993 bytes.extend_from_slice(&hw2.to_le_bytes());
7994 Ok(bytes)
7995 }
7996
7997 /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7998 fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7999 let rd_bits = reg_to_bits(rd);
8000 let base_bits = reg_to_bits(base);
8001 let rm_bits = reg_to_bits(offset_reg);
8002 // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
8003 let hw1: u16 = (0xF810 | base_bits) as u16;
8004 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8005 let mut bytes = hw1.to_le_bytes().to_vec();
8006 bytes.extend_from_slice(&hw2.to_le_bytes());
8007 Ok(bytes)
8008 }
8009
8010 /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
8011 fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8012 let rd_bits = reg_to_bits(rd);
8013 let base_bits = reg_to_bits(base);
8014 // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
8015 check_ldst_imm12(offset)?;
8016 let hw1: u16 = (0xF990 | base_bits) as u16;
8017 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8018 let mut bytes = hw1.to_le_bytes().to_vec();
8019 bytes.extend_from_slice(&hw2.to_le_bytes());
8020 Ok(bytes)
8021 }
8022
8023 /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8024 fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8025 let rd_bits = reg_to_bits(rd);
8026 let base_bits = reg_to_bits(base);
8027 let rm_bits = reg_to_bits(offset_reg);
8028 // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8029 let hw1: u16 = (0xF910 | base_bits) as u16;
8030 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8031 let mut bytes = hw1.to_le_bytes().to_vec();
8032 bytes.extend_from_slice(&hw2.to_le_bytes());
8033 Ok(bytes)
8034 }
8035
8036 /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8037 fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8038 let rd_bits = reg_to_bits(rd);
8039 let base_bits = reg_to_bits(base);
8040 // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8041 check_ldst_imm12(offset)?;
8042 let hw1: u16 = (0xF8B0 | base_bits) as u16;
8043 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8044 let mut bytes = hw1.to_le_bytes().to_vec();
8045 bytes.extend_from_slice(&hw2.to_le_bytes());
8046 Ok(bytes)
8047 }
8048
8049 /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8050 fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8051 let rd_bits = reg_to_bits(rd);
8052 let base_bits = reg_to_bits(base);
8053 let rm_bits = reg_to_bits(offset_reg);
8054 // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8055 let hw1: u16 = (0xF830 | base_bits) as u16;
8056 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8057 let mut bytes = hw1.to_le_bytes().to_vec();
8058 bytes.extend_from_slice(&hw2.to_le_bytes());
8059 Ok(bytes)
8060 }
8061
8062 /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8063 fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8064 let rd_bits = reg_to_bits(rd);
8065 let base_bits = reg_to_bits(base);
8066 // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8067 check_ldst_imm12(offset)?;
8068 let hw1: u16 = (0xF9B0 | base_bits) as u16;
8069 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8070 let mut bytes = hw1.to_le_bytes().to_vec();
8071 bytes.extend_from_slice(&hw2.to_le_bytes());
8072 Ok(bytes)
8073 }
8074
8075 /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8076 fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8077 let rd_bits = reg_to_bits(rd);
8078 let base_bits = reg_to_bits(base);
8079 let rm_bits = reg_to_bits(offset_reg);
8080 // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8081 let hw1: u16 = (0xF930 | base_bits) as u16;
8082 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8083 let mut bytes = hw1.to_le_bytes().to_vec();
8084 bytes.extend_from_slice(&hw2.to_le_bytes());
8085 Ok(bytes)
8086 }
8087
8088 /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8089 fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8090 let rd_bits = reg_to_bits(rd);
8091 let base_bits = reg_to_bits(base);
8092 // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8093 check_ldst_imm12(offset)?;
8094 let hw1: u16 = (0xF880 | base_bits) as u16;
8095 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8096 let mut bytes = hw1.to_le_bytes().to_vec();
8097 bytes.extend_from_slice(&hw2.to_le_bytes());
8098 Ok(bytes)
8099 }
8100
8101 /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8102 fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8103 let rd_bits = reg_to_bits(rd);
8104 let base_bits = reg_to_bits(base);
8105 let rm_bits = reg_to_bits(offset_reg);
8106 // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8107 let hw1: u16 = (0xF800 | base_bits) as u16;
8108 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8109 let mut bytes = hw1.to_le_bytes().to_vec();
8110 bytes.extend_from_slice(&hw2.to_le_bytes());
8111 Ok(bytes)
8112 }
8113
8114 /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8115 fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8116 let rd_bits = reg_to_bits(rd);
8117 let base_bits = reg_to_bits(base);
8118 // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8119 check_ldst_imm12(offset)?;
8120 let hw1: u16 = (0xF8A0 | base_bits) as u16;
8121 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8122 let mut bytes = hw1.to_le_bytes().to_vec();
8123 bytes.extend_from_slice(&hw2.to_le_bytes());
8124 Ok(bytes)
8125 }
8126
8127 /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8128 fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8129 let rd_bits = reg_to_bits(rd);
8130 let base_bits = reg_to_bits(base);
8131 let rm_bits = reg_to_bits(offset_reg);
8132 // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8133 let hw1: u16 = (0xF820 | base_bits) as u16;
8134 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8135 let mut bytes = hw1.to_le_bytes().to_vec();
8136 bytes.extend_from_slice(&hw2.to_le_bytes());
8137 Ok(bytes)
8138 }
8139
8140 /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8141 fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8142 let rd_bits = reg_to_bits(rd);
8143 let rn_bits = reg_to_bits(rn);
8144
8145 // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8146 // which picks the correct form per value:
8147 // - imm <= 0xFF -> ADD.W (T3). Its `i:imm3:imm8` field is a
8148 // ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8149 // - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8150 //
8151 // #681: this function used to pack the raw value into the T3 field for
8152 // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8153 // = 0x8000_0000, so every dynamic-address load/store with a static
8154 // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8155 // --safety-bounds software the guard checked the intended address while
8156 // the access used the mis-encoded one (bounds bypass). Same
8157 // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8158 if imm <= 0xFFF {
8159 self.encode_thumb32_add(rd, rn, imm)
8160 } else {
8161 // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8162 // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8163 // "encoder must produce a legal sequence, not assert" class — see #350.
8164 //
8165 // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8166 // the ADD reads it):
8167 // - rd != rn => use rd itself (rn is untouched, since rd != rn).
8168 // - rd == rn => use R12/IP (the reserved encoder scratch). rd/rn are
8169 // never R12 (R12 is non-allocatable), so it can't alias.
8170 //
8171 // The materialized value is the same whether or not MOVT is emitted, so
8172 // the byte length depends only on `imm` (and rd==rn) — the size probe and
8173 // the final emit therefore agree (mandatory: the function is encoded twice).
8174 let scratch: u32 = if rd_bits == rn_bits {
8175 12 // R12/IP — in-place add, can't use rd because rd == rn
8176 } else {
8177 rd_bits // rn is preserved because rd != rn
8178 };
8179 // Invariant: the scratch must never alias Rn (would clobber it before
8180 // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8181 // which is reserved encoder scratch), but the encoder is also driven by
8182 // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8183 // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8184 // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8185 // instead of asserting. #350 follow-up.
8186 if scratch == rn_bits {
8187 return Err(synth_core::Error::synthesis(format!(
8188 "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8189 register (R12 is the reserved encoder scratch and aliases Rn here)"
8190 )));
8191 }
8192
8193 let lo16 = imm & 0xFFFF;
8194 let hi16 = (imm >> 16) & 0xFFFF;
8195
8196 let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8197 if hi16 != 0 {
8198 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8199 }
8200 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8201 Ok(bytes)
8202 }
8203 }
8204
8205 // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8206
8207 /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8208 ///
8209 /// # Contract (Verus-style)
8210 /// ```text
8211 /// requires rd <= 14, imm16 <= 0xFFFF
8212 /// ensures result.len() == 4
8213 /// ```
8214 fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8215 reg_bits_checked(rd)?;
8216 encoding_contracts::verify_imm16(imm16);
8217 // MOVW Rd, #imm16
8218 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8219 let imm16 = imm16 & 0xFFFF;
8220 let imm4 = (imm16 >> 12) & 0xF;
8221 let i_bit = (imm16 >> 11) & 1;
8222 let imm3 = (imm16 >> 8) & 0x7;
8223 let imm8 = imm16 & 0xFF;
8224
8225 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8226 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8227
8228 let mut bytes = hw1.to_le_bytes().to_vec();
8229 bytes.extend_from_slice(&hw2.to_le_bytes());
8230 encoding_contracts::verify_thumb32(&bytes);
8231 Ok(bytes)
8232 }
8233
8234 /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8235 ///
8236 /// # Contract (Verus-style)
8237 /// ```text
8238 /// requires rd <= 14, imm16 <= 0xFFFF
8239 /// ensures result.len() == 4
8240 /// ```
8241 fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8242 reg_bits_checked(rd)?;
8243 encoding_contracts::verify_imm16(imm16);
8244 // MOVT Rd, #imm16
8245 // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8246 let imm16 = imm16 & 0xFFFF;
8247 let imm4 = (imm16 >> 12) & 0xF;
8248 let i_bit = (imm16 >> 11) & 1;
8249 let imm3 = (imm16 >> 8) & 0x7;
8250 let imm8 = imm16 & 0xFF;
8251
8252 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8253 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8254
8255 let mut bytes = hw1.to_le_bytes().to_vec();
8256 bytes.extend_from_slice(&hw2.to_le_bytes());
8257 encoding_contracts::verify_thumb32(&bytes);
8258 Ok(bytes)
8259 }
8260
8261 /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8262 fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8263 // MOV.W Rd, Rm, LSR #imm
8264 // EA4F 0 imm3 Rd imm2 01 Rm
8265 let imm5 = shift & 0x1F;
8266 let imm2 = imm5 & 0x3;
8267 let imm3 = (imm5 >> 2) & 0x7;
8268
8269 let hw1: u16 = 0xEA4F;
8270 let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8271
8272 let mut bytes = hw1.to_le_bytes().to_vec();
8273 bytes.extend_from_slice(&hw2.to_le_bytes());
8274 Ok(bytes)
8275 }
8276
8277 /// Encode Thumb-2 32-bit AND with immediate - raw version
8278 fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8279 // AND.W Rd, Rn, #<modified_immediate>
8280 // F0 00 Rn | 0 imm3 Rd imm8
8281 //
8282 // #681 class audit: the field is a ThumbExpandImm modified immediate,
8283 // not a raw value. The only current caller (POPCNT final mask) passes
8284 // 0x3F, which expands to itself — the gate is byte-identical today and
8285 // closes the raw-packing landmine for any future caller.
8286 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8287 synth_core::Error::synthesis(
8288 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8289 )
8290 })?;
8291 let i_bit = (field >> 11) & 1;
8292 let imm3 = (field >> 8) & 0x7;
8293 let imm8 = field & 0xFF;
8294
8295 let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8296 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8297
8298 let mut bytes = hw1.to_le_bytes().to_vec();
8299 bytes.extend_from_slice(&hw2.to_le_bytes());
8300 Ok(bytes)
8301 }
8302
8303 /// Encode Thumb-2 32-bit SUB (register) - raw version
8304 fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8305 // SUB.W Rd, Rn, Rm
8306 // EBA0 Rn | 0 Rd 00 00 Rm
8307 let hw1: u16 = (0xEBA0 | rn) as u16;
8308 let hw2: u16 = ((rd << 8) | rm) as u16;
8309
8310 let mut bytes = hw1.to_le_bytes().to_vec();
8311 bytes.extend_from_slice(&hw2.to_le_bytes());
8312 Ok(bytes)
8313 }
8314
8315 /// Encode Thumb-2 32-bit ADD (register) - raw version
8316 fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8317 // ADD.W Rd, Rn, Rm
8318 // EB00 Rn | 0 Rd 00 00 Rm
8319 let hw1: u16 = (0xEB00 | rn) as u16;
8320 let hw2: u16 = ((rd << 8) | rm) as u16;
8321
8322 let mut bytes = hw1.to_le_bytes().to_vec();
8323 bytes.extend_from_slice(&hw2.to_le_bytes());
8324 Ok(bytes)
8325 }
8326
8327 /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8328 /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8329 /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8330 fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8331 // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8332 let hw1: u16 = (0xEB10 | rn) as u16;
8333 let hw2: u16 = ((rd << 8) | rm) as u16;
8334 let mut bytes = hw1.to_le_bytes().to_vec();
8335 bytes.extend_from_slice(&hw2.to_le_bytes());
8336 Ok(bytes)
8337 }
8338
8339 /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8340 /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8341 fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8342 // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8343 let hw1: u16 = (0xEBB0 | rn) as u16;
8344 let hw2: u16 = ((rd << 8) | rm) as u16;
8345 let mut bytes = hw1.to_le_bytes().to_vec();
8346 bytes.extend_from_slice(&hw2.to_le_bytes());
8347 Ok(bytes)
8348 }
8349
8350 /// Encode a sequence of ARM instructions
8351 pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8352 let mut code = Vec::new();
8353
8354 for op in ops {
8355 let encoded = self.encode(op)?;
8356 code.extend_from_slice(&encoded);
8357 }
8358
8359 Ok(code)
8360 }
8361}
8362
8363/// Convert register to bit encoding (0-15)
8364/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8365/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8366/// `None` (the caller must materialize the value into a register). This is the
8367/// shared correct path for the data-processing immediate encoders — without it
8368/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8369/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8370fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8371 // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8372 if value <= 0xFF {
8373 return Some(value);
8374 }
8375 let b0 = value & 0xFF; // byte 0
8376 let b1 = (value >> 8) & 0xFF; // byte 1
8377 // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8378 if value == (b0 << 16) | b0 {
8379 return Some(0x100 | b0);
8380 }
8381 // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8382 if value == (b1 << 24) | (b1 << 8) {
8383 return Some(0x200 | b1);
8384 }
8385 // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8386 if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8387 return Some(0x300 | b0);
8388 }
8389 // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8390 // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8391 // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8392 for rot in 8..=31u32 {
8393 let unrot = value.rotate_left(rot);
8394 if (0x80..=0xFF).contains(&unrot) {
8395 return Some((rot << 7) | (unrot & 0x7F));
8396 }
8397 }
8398 None
8399}
8400
8401/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8402/// `0..=4095`; a larger offset must be materialized into a register by the
8403/// selector (register-offset addressing). Returning `Err` rather than silently
8404/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8405/// the load/store sibling of #253/#255).
8406fn check_ldst_imm12(offset: u32) -> Result<()> {
8407 if offset > 0xFFF {
8408 Err(synth_core::Error::synthesis(
8409 "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8410 ))
8411 } else {
8412 Ok(())
8413 }
8414}
8415
8416/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8417///
8418/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8419/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8420/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8421/// not a move at all. The destination is never written (it keeps stale data)
8422/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8423/// defect #311 fixed for `I64SetCond`.
8424///
8425/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8426/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8427/// does not set flags, which is what these zero-fill sites want anyway.
8428///
8429/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8430/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8431/// PAST this instruction moves when it widens and its displacement has to be
8432/// derived rather than hard-coded. (A branch targeting this instruction's own
8433/// address is unaffected — an instruction cannot move itself.)
8434fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8435 if rd_bits < 8 {
8436 let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8437 bytes.extend_from_slice(&movs.to_le_bytes());
8438 } else {
8439 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8440 bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8441 }
8442}
8443
8444/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8445/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8446/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8447/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8448/// `estimator_encoder_agreement` oracle).
8449fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8450 if rd_bits < 8 { 1 } else { 2 }
8451}
8452
8453fn reg_to_bits(reg: &Reg) -> u32 {
8454 match reg {
8455 Reg::R0 => 0,
8456 Reg::R1 => 1,
8457 Reg::R2 => 2,
8458 Reg::R3 => 3,
8459 Reg::R4 => 4,
8460 Reg::R5 => 5,
8461 Reg::R6 => 6,
8462 Reg::R7 => 7,
8463 Reg::R8 => 8,
8464 Reg::R9 => 9,
8465 Reg::R10 => 10,
8466 Reg::R11 => 11,
8467 Reg::R12 => 12,
8468 Reg::SP => 13,
8469 Reg::LR => 14,
8470 Reg::PC => 15,
8471 }
8472}
8473
8474// ======================================================================
8475// #610 — i64 fixed-ABI expansion wrappers.
8476//
8477// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8478// shift-subtract loops) compute in FIXED low registers. Before #610 the
8479// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8480// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8481// collided with selector-assigned registers — then restored the saved
8482// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8483// returned the caller's stale register: 0 for every input under qemu.
8484//
8485// These wrappers make each core honor its register parameters:
8486// 1. save R0-R3,
8487// 2. marshal the operand registers into the core's fixed input regs via
8488// the stack (permutation-safe: every source is read before any fixed
8489// register is written),
8490// 3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8491// scratch and never allocatable, #212),
8492// 4. MOV the result pair from R0:R1 into the selector's rd pair,
8493// 5. restore R0-R3, skipping any register the result now occupies.
8494//
8495// All emitted lengths are register-independent so the optimized path's
8496// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8497// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8498// ======================================================================
8499
8500/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8501/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8502/// before any destination register is written, so arbitrary source/target
8503/// permutations (including operands living in R0-R3) are safe.
8504fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8505 debug_assert!(srcs.len() <= 4);
8506 // PUSH {R0-R3} — save the caller-visible low registers.
8507 bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8508 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8509 for src in srcs.iter().rev() {
8510 let rt = reg_to_bits(src) as u16;
8511 bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8512 bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8513 }
8514 // POP {Ri} — Ri := srcs[i].
8515 for i in 0..srcs.len() as u16 {
8516 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8517 }
8518}
8519
8520/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8521/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8522/// register the result now lives in (its saved caller word is discarded).
8523fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8524 let lo = reg_to_bits(rdlo);
8525 let hi = reg_to_bits(rdhi);
8526 if lo == 1 && hi == 0 {
8527 // A fully swapped pair would clobber one half in either MOV order.
8528 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8529 return Err(synth_core::Error::synthesis(
8530 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8531 ));
8532 }
8533 let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8534 let d = ((rd >> 3) & 1) as u16;
8535 bytes.extend_from_slice(
8536 &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8537 );
8538 };
8539 if hi == 0 {
8540 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8541 mov16(bytes, lo, 0);
8542 mov16(bytes, hi, 1);
8543 } else {
8544 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8545 mov16(bytes, hi, 1);
8546 mov16(bytes, lo, 0);
8547 }
8548 for i in 0..4u32 {
8549 if i == lo || i == hi {
8550 // The result lives here — drop the saved caller word.
8551 bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8552 } else {
8553 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8554 }
8555 }
8556 Ok(())
8557}
8558
8559/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8560/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8561/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8562fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8563 bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8564 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8565 bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8566 bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8567}
8568
8569/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8570/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8571/// guard — #633: without it the core negated INT64_MIN onto itself and
8572/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8573/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8574///
8575/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8576/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8577/// register-independent (estimator contract, #498/#511).
8578fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8579 // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8580 bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8581 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8582 // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8583 bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8584 bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8585 // BNE .no_trap
8586 bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8587 // CMP R0, #0 — dividend lo word of INT64_MIN
8588 bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8589 // BNE .no_trap
8590 bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8591 // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8592 bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8593 bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8594 // BNE .no_trap
8595 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8596 // UDF #0 — signed-division overflow
8597 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8598 // .no_trap:
8599}
8600
8601// ======================================================================
8602// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8603// Identical register contract, A32 encodings: the multi-instruction i64
8604// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8605// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8606// the selector-assigned operand registers in and the result out, saving and
8607// restoring the caller-visible R0-R3 around the core.
8608// ======================================================================
8609
8610/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8611/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8612/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8613/// written, so arbitrary source/target permutations are safe.
8614fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8615 debug_assert!(srcs.len() <= 4);
8616 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8617 // PUSH {R0-R3} — save the caller-visible low registers.
8618 w(bytes, 0xE92D_000F);
8619 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8620 for src in srcs.iter().rev() {
8621 w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8622 }
8623 // LDR Ri, [SP], #4 — Ri := srcs[i].
8624 for i in 0..srcs.len() as u32 {
8625 w(bytes, 0xE49D_0004 | (i << 12));
8626 }
8627}
8628
8629/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8630/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8631/// any register the result now lives in (its saved caller word is discarded).
8632fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8633 let lo = reg_to_bits(rdlo);
8634 let hi = reg_to_bits(rdhi);
8635 if lo == 1 && hi == 0 {
8636 // A fully swapped pair would clobber one half in either MOV order.
8637 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8638 return Err(synth_core::Error::synthesis(
8639 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8640 ));
8641 }
8642 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8643 let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8644 if hi == 0 {
8645 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8646 mov(bytes, lo, 0);
8647 mov(bytes, hi, 1);
8648 } else {
8649 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8650 mov(bytes, hi, 1);
8651 mov(bytes, lo, 0);
8652 }
8653 for i in 0..4u32 {
8654 if i == lo || i == hi {
8655 // The result lives here — drop the saved caller word.
8656 w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8657 } else {
8658 w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8659 }
8660 }
8661 Ok(())
8662}
8663
8664/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8665/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8666/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8667fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8668 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8669 w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8670 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8671 w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8672}
8673
8674/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8675/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8676/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8677/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8678fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8679 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8680 w(bytes, 0xE002_C003); // AND R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8681 w(bytes, 0xE37C_0001); // CMN R12, #1 (EQ iff divisor == -1)
8682 w(bytes, 0x0350_0000); // CMPEQ R0, #0 (EQ iff also dividend lo == 0)
8683 w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8684 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8685 w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8686}
8687
8688/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8689/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8690/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8691/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8692/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8693/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8694/// Returns a typed Err instead. See #185.
8695fn reg_bits_checked(bits: u32) -> Result<()> {
8696 if bits > 14 {
8697 return Err(synth_core::Error::synthesis(format!(
8698 "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8699 )));
8700 }
8701 Ok(())
8702}
8703
8704/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8705/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8706fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8707 if val == 0 {
8708 return Some((0, 1));
8709 }
8710 for rot in 0..16u32 {
8711 let shift = rot * 2;
8712 // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8713 let unrotated = val.rotate_left(shift);
8714 if unrotated <= 0xFF {
8715 // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8716 return Some(((rot << 8) | unrotated, 1));
8717 }
8718 }
8719 None
8720}
8721
8722/// Encode operand2 field and return (bits, immediate_flag).
8723/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8724/// Panics if an immediate value cannot be represented. Callers that need large
8725/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8726fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8727 match op2 {
8728 Operand2::Imm(val) => {
8729 let uval = *val as u32;
8730 // Attempt rotated-immediate encoding (ARM32 Operand2)
8731 if let Some(encoded) = try_encode_rotated_imm(uval) {
8732 Ok(encoded)
8733 } else {
8734 // #378-class honesty: an immediate that can't be expressed as an
8735 // ARM32 rotated immediate is an INTERNAL selector bug — large
8736 // constants must be materialized via MOVW/MOVT, not passed here.
8737 // FAIL HONESTLY with an Err rather than silently masking to
8738 // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8739 // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8740 // this is an Err and not a panic (the `encoder_no_panic` fuzz
8741 // contract — malformed/oversized input must degrade, not crash).
8742 Err(synth_core::Error::synthesis(format!(
8743 "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8744 rotated immediate — the selector must materialize large \
8745 constants via MOVW/MOVT"
8746 )))
8747 }
8748 }
8749
8750 Operand2::Reg(reg) => {
8751 let reg_bits = reg_to_bits(reg);
8752 Ok((reg_bits, 0)) // I=0 for register
8753 }
8754
8755 Operand2::RegShift {
8756 rm,
8757 shift: _,
8758 amount,
8759 } => {
8760 // Simplified encoding with shift
8761 let rm_bits = reg_to_bits(rm);
8762 let shift_bits = (*amount & 0x1F) << 7;
8763 Ok((shift_bits | rm_bits, 0))
8764 }
8765 }
8766}
8767
8768/// Largest immediate an A32 `LDR`/`STR`/`LDRB`/`STRB` `[Rn, #imm12]` form holds.
8769const A32_LDST_IMM12_MAX: u32 = 0xFFF;
8770/// Largest immediate an A32 `LDRH`/`STRH`/`LDRSB`/`LDRSH` `[Rn, #imm8]`
8771/// (`imm4H:imm4L`) form holds.
8772const A32_LDST_IMM8_MAX: u32 = 0xFF;
8773/// Largest LOW-half immediate an A32 `I64Ldr`/`I64Str` pair can fold: the high
8774/// half sits at `+4`, which must itself fit imm12 (`0xFFB + 4 = 0xFFF`).
8775const A32_I64_PAIR_IMM12_MAX: u32 = 0xFFB;
8776
8777/// RQ-63-ARMI64OFF (#1165): resolve the A32 base register and residual
8778/// immediate for a load/store whose static offset may exceed the form's
8779/// immediate field — the A32 counterpart of the Thumb-2 `i64_effective_base`
8780/// (#372/#382). Returns `(base, residual)`; the caller encodes `[base,
8781/// #residual]` (and `#residual + 4` for the i64 high half).
8782///
8783/// The offset is the wasm memarg round-tripped through the selector's `as i32`
8784/// cast (a memarg `>= 2^31` arrives negative), so it is read back as `u32` and
8785/// the effective address is `base + index + offset (mod 2^32)` — the same
8786/// arithmetic the Thumb-2 word path (`encode_thumb32_add_imm`) and the
8787/// `--safety-bounds software` guard (`software_bounds_guard`, which round-trips
8788/// the same cast) already use. A clamp or a mask here is a WRONG ADDRESS, i.e.
8789/// the silent memory miscompile this artifact exists to refuse: before this
8790/// helper the A32 immediate arms masked `& 0xFFF` (word/byte) and `& 0xFF`
8791/// (halfword/signed), so `i32.load offset=5000` on the `-b arm` default target
8792/// silently read `[ip, #904]`, and the i64 pair arm declined outright (46 of
8793/// the 110 core-module declines in v0.62's ARM census).
8794///
8795/// - `offset <= imm_max`, no index: emits nothing, returns `(addr.base, offset)`
8796/// — byte-identical to the pre-#1165 immediate form.
8797/// - `offset <= imm_max`, index `rm`: emits `ADD ip, base, rm`, returns
8798/// `(ip, offset)` — byte-identical to the #206 register-offset form.
8799/// - `offset > imm_max`: MATERIALIZES the whole effective base into `ip`:
8800/// `MOVW ip, #lo16 ; [MOVT ip, #hi16 — only when non-zero] ; ADD ip, ip, base
8801/// ; [ADD ip, ip, rm]`, returns `(ip, 0)`. `ip` is fully computed BEFORE the
8802/// access, so a destination register aliasing `base` or `rm` is safe.
8803///
8804/// `ip` (R12) is the reserved encoder scratch — never allocated (pool R0–R8;
8805/// R9/R10/R11 are the globals/size/base contract registers). If `base` or `rm`
8806/// IS R12 the materialization would clobber its own input, so that case is a
8807/// typed `Err` — a loud decline, never a wrong address (the alias trap
8808/// `encode_thumb32_add_imm` carries for `rd == rn == R12`).
8809///
8810/// Bytes verified against `arm-none-eabi-as` in the `test_1165_a32_*` tests
8811/// and executed by `scripts/repro/a32_ldst_offset_1165_differential.py`.
8812fn a32_effective_base(bytes: &mut Vec<u8>, addr: &MemAddr, imm_max: u32) -> Result<(Reg, u32)> {
8813 let ip = Reg::R12;
8814 let ip_bits = reg_to_bits(&ip);
8815 let offset = addr.offset as u32;
8816 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8817 // ADD rd, rn, rm (cond=AL, opcode=ADD, S=0, register operand2).
8818 let add_reg = |rd: u32, rn: u32, rm: u32| 0xE080_0000 | (rn << 16) | (rd << 12) | rm;
8819 if offset <= imm_max {
8820 return Ok(match addr.offset_reg {
8821 Some(rm) => {
8822 w(
8823 bytes,
8824 add_reg(ip_bits, reg_to_bits(&addr.base), reg_to_bits(&rm)),
8825 );
8826 (ip, offset)
8827 }
8828 None => (addr.base, offset),
8829 });
8830 }
8831 if addr.base == ip || addr.offset_reg == Some(ip) {
8832 return Err(synth_core::Error::synthesis(format!(
8833 "A32 load/store offset {offset:#x} exceeds the immediate field (max {imm_max:#x}) \
8834 and the address already uses the R12 scratch — no free register to \
8835 materialize it (RQ-63-ARMI64OFF)"
8836 )));
8837 }
8838 // MOVW ip, #lo16 ; MOVT ip, #hi16 (elided when zero, mirroring I64Const).
8839 let lo16 = offset & 0xFFFF;
8840 let hi16 = offset >> 16;
8841 w(
8842 bytes,
8843 0xE300_0000 | ((lo16 >> 12) << 16) | (ip_bits << 12) | (lo16 & 0xFFF),
8844 );
8845 if hi16 != 0 {
8846 w(
8847 bytes,
8848 0xE340_0000 | ((hi16 >> 12) << 16) | (ip_bits << 12) | (hi16 & 0xFFF),
8849 );
8850 }
8851 w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&addr.base)));
8852 if let Some(rm) = addr.offset_reg {
8853 w(bytes, add_reg(ip_bits, ip_bits, reg_to_bits(&rm)));
8854 }
8855 Ok((ip, 0))
8856}
8857
8858/// Encode an A32 immediate-form memory address to `(base_reg, imm12)`.
8859///
8860/// RQ-63-ARMI64OFF (#1165): this used to mask `& 0xFFF`, silently re-targeting
8861/// any offset past the field (the #259 class, closed on Thumb-2 by
8862/// `check_ldst_imm12` but never here). The immediate arms are only reached with
8863/// an in-range, index-free address — `encode_arm_reg_offset_mem` materializes
8864/// everything else into IP first — so this is a TRIPWIRE: a typed error, never
8865/// a wrong address, should a future arm bypass that pre-pass.
8866fn encode_mem_addr(addr: &MemAddr) -> Result<(u32, u32)> {
8867 check_a32_imm_addr(addr, A32_LDST_IMM12_MAX)
8868}
8869
8870/// `encode_mem_addr` for the `imm4H:imm4L` (8-bit) forms — `LDRH`/`STRH`/
8871/// `LDRSB`/`LDRSH`. Same tripwire contract; the former `& 0xFF` mask is gone.
8872fn encode_mem_addr_imm8(addr: &MemAddr) -> Result<(u32, u32)> {
8873 check_a32_imm_addr(addr, A32_LDST_IMM8_MAX)
8874}
8875
8876fn check_a32_imm_addr(addr: &MemAddr, imm_max: u32) -> Result<(u32, u32)> {
8877 if addr.offset_reg.is_some() {
8878 return Err(synth_core::Error::synthesis(
8879 "internal: A32 immediate-form load/store reached with a register offset — \
8880 encode_arm_reg_offset_mem must materialize it first (#206)",
8881 ));
8882 }
8883 let offset = addr.offset as u32;
8884 if offset > imm_max {
8885 return Err(synth_core::Error::synthesis(format!(
8886 "internal: A32 load/store immediate offset {offset:#x} exceeds the {imm_max:#x} \
8887 field — encode_arm_reg_offset_mem must materialize it first (RQ-63-ARMI64OFF)"
8888 )));
8889 }
8890 Ok((reg_to_bits(&addr.base), offset))
8891}
8892
8893/// S-register number: S0=0, S1=1, ..., S31=31
8894fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8895 match reg {
8896 VfpReg::S0 => Ok(0),
8897 VfpReg::S1 => Ok(1),
8898 VfpReg::S2 => Ok(2),
8899 VfpReg::S3 => Ok(3),
8900 VfpReg::S4 => Ok(4),
8901 VfpReg::S5 => Ok(5),
8902 VfpReg::S6 => Ok(6),
8903 VfpReg::S7 => Ok(7),
8904 VfpReg::S8 => Ok(8),
8905 VfpReg::S9 => Ok(9),
8906 VfpReg::S10 => Ok(10),
8907 VfpReg::S11 => Ok(11),
8908 VfpReg::S12 => Ok(12),
8909 VfpReg::S13 => Ok(13),
8910 VfpReg::S14 => Ok(14),
8911 VfpReg::S15 => Ok(15),
8912 VfpReg::S16 => Ok(16),
8913 VfpReg::S17 => Ok(17),
8914 VfpReg::S18 => Ok(18),
8915 VfpReg::S19 => Ok(19),
8916 VfpReg::S20 => Ok(20),
8917 VfpReg::S21 => Ok(21),
8918 VfpReg::S22 => Ok(22),
8919 VfpReg::S23 => Ok(23),
8920 VfpReg::S24 => Ok(24),
8921 VfpReg::S25 => Ok(25),
8922 VfpReg::S26 => Ok(26),
8923 VfpReg::S27 => Ok(27),
8924 VfpReg::S28 => Ok(28),
8925 VfpReg::S29 => Ok(29),
8926 VfpReg::S30 => Ok(30),
8927 VfpReg::S31 => Ok(31),
8928 // D-registers are not used in F32 single-precision encodings
8929 _ => Err(synth_core::Error::SynthesisError(
8930 "D-register not supported in single-precision VFP encoding".to_string(),
8931 )),
8932 }
8933}
8934
8935/// D-register number: D0=0, D1=1, ..., D15=15
8936fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8937 match reg {
8938 VfpReg::D0 => Ok(0),
8939 VfpReg::D1 => Ok(1),
8940 VfpReg::D2 => Ok(2),
8941 VfpReg::D3 => Ok(3),
8942 VfpReg::D4 => Ok(4),
8943 VfpReg::D5 => Ok(5),
8944 VfpReg::D6 => Ok(6),
8945 VfpReg::D7 => Ok(7),
8946 VfpReg::D8 => Ok(8),
8947 VfpReg::D9 => Ok(9),
8948 VfpReg::D10 => Ok(10),
8949 VfpReg::D11 => Ok(11),
8950 VfpReg::D12 => Ok(12),
8951 VfpReg::D13 => Ok(13),
8952 VfpReg::D14 => Ok(14),
8953 VfpReg::D15 => Ok(15),
8954 // S-registers are not used in F64 double-precision encodings
8955 _ => Err(synth_core::Error::SynthesisError(
8956 "S-register not supported in double-precision VFP encoding".to_string(),
8957 )),
8958 }
8959}
8960
8961/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8962/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8963/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8964fn encode_sreg(s: u32) -> (u32, u32) {
8965 (s >> 1, s & 1)
8966}
8967
8968/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8969/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8970/// For D0-D15, qualifier is always 0.
8971fn encode_dreg(d: u32) -> (u32, u32) {
8972 (d & 0xF, (d >> 4) & 1)
8973}
8974
8975/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8976/// Returns the full 32-bit instruction word.
8977///
8978/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8979/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8980fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8981 let sd_num = vfp_sreg_to_num(sd)?;
8982 let sn_num = vfp_sreg_to_num(sn)?;
8983 let sm_num = vfp_sreg_to_num(sm)?;
8984 let (vd, d) = encode_sreg(sd_num);
8985 let (vn, n) = encode_sreg(sn_num);
8986 let (vm, m) = encode_sreg(sm_num);
8987
8988 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8989}
8990
8991/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8992/// Returns the full 32-bit instruction word.
8993fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8994 let sd_num = vfp_sreg_to_num(sd)?;
8995 let sm_num = vfp_sreg_to_num(sm)?;
8996 let (vd, d) = encode_sreg(sd_num);
8997 let (vm, m) = encode_sreg(sm_num);
8998
8999 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9000}
9001
9002/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
9003/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9004/// U bit (bit 23) controls add/subtract offset.
9005fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9006 let sd_num = vfp_sreg_to_num(sd)?;
9007 let (vd, d) = encode_sreg(sd_num);
9008 let rn = reg_to_bits(&addr.base);
9009
9010 let offset = addr.offset;
9011 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9012 let abs_offset = offset.unsigned_abs();
9013 let imm8 = (abs_offset / 4) & 0xFF;
9014
9015 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9016}
9017
9018/// Encode VMOV between core register and S-register.
9019/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9020/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
9021fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
9022 let s_num = vfp_sreg_to_num(sreg)?;
9023 let (vn, n) = encode_sreg(s_num);
9024 let rt = reg_to_bits(core);
9025
9026 let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
9027 Ok(base | (vn << 16) | (rt << 12) | (n << 7))
9028}
9029
9030/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
9031/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
9032/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
9033fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
9034 let dd_num = vfp_dreg_to_num(dd)?;
9035 let dn_num = vfp_dreg_to_num(dn)?;
9036 let dm_num = vfp_dreg_to_num(dm)?;
9037 let (vd, d) = encode_dreg(dd_num);
9038 let (vn, n) = encode_dreg(dn_num);
9039 let (vm, m) = encode_dreg(dm_num);
9040
9041 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
9042}
9043
9044/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
9045fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
9046 let dd_num = vfp_dreg_to_num(dd)?;
9047 let dm_num = vfp_dreg_to_num(dm)?;
9048 let (vd, d) = encode_dreg(dd_num);
9049 let (vm, m) = encode_dreg(dm_num);
9050
9051 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
9052}
9053
9054/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
9055/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
9056fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
9057 let dd_num = vfp_dreg_to_num(dd)?;
9058 let (vd, d) = encode_dreg(dd_num);
9059 let rn = reg_to_bits(&addr.base);
9060
9061 let offset = addr.offset;
9062 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9063 let abs_offset = offset.unsigned_abs();
9064 let imm8 = (abs_offset / 4) & 0xFF;
9065
9066 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
9067}
9068
9069/// Encode VMOV between two core registers and a D-register.
9070/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9071/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
9072fn encode_vmov_core_dreg(
9073 to_dreg: bool,
9074 dreg: &VfpReg,
9075 core_lo: &Reg,
9076 core_hi: &Reg,
9077) -> Result<u32> {
9078 let d_num = vfp_dreg_to_num(dreg)?;
9079 let (vm, m) = encode_dreg(d_num);
9080 let rt = reg_to_bits(core_lo);
9081 let rt2 = reg_to_bits(core_hi);
9082
9083 let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
9084 Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
9085}
9086
9087/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
9088fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
9089 let hw1 = ((instr >> 16) & 0xFFFF) as u16;
9090 let hw2 = (instr & 0xFFFF) as u16;
9091 let mut bytes = hw1.to_le_bytes().to_vec();
9092 bytes.extend_from_slice(&hw2.to_le_bytes());
9093 bytes
9094}
9095
9096// ============================================================================
9097// Helium MVE encoding helpers
9098// ============================================================================
9099
9100/// Q-register number: Q0=0, Q1=1, ..., Q7=7
9101fn qreg_to_num(reg: &QReg) -> u32 {
9102 match reg {
9103 QReg::Q0 => 0,
9104 QReg::Q1 => 1,
9105 QReg::Q2 => 2,
9106 QReg::Q3 => 3,
9107 QReg::Q4 => 4,
9108 QReg::Q5 => 5,
9109 QReg::Q6 => 6,
9110 QReg::Q7 => 7,
9111 }
9112}
9113
9114/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
9115fn mve_size_bits(size: &MveSize) -> u32 {
9116 match size {
9117 MveSize::S8 => 0b00,
9118 MveSize::S16 => 0b01,
9119 MveSize::S32 => 0b10,
9120 }
9121}
9122
9123/// Encode MVE 3-register instruction.
9124/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
9125/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
9126fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9127 let d = qreg_to_num(qd) * 2;
9128 let n = qreg_to_num(qn) * 2;
9129 let m = qreg_to_num(qm) * 2;
9130
9131 // Standard NEON/MVE 3-register encoding:
9132 // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
9133 // N bit (bit 7) = Vn[4], Vn[3:0] = bits [19:16]
9134 // M bit (bit 5) = Vm[4], Vm[3:0] = bits [3:0]
9135 let vd = d & 0xF;
9136 let d_bit = (d >> 4) & 1;
9137 let vn = n & 0xF;
9138 let n_bit = (n >> 4) & 1;
9139 let vm = m & 0xF;
9140 let m_bit = (m >> 4) & 1;
9141
9142 base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9143}
9144
9145/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9146fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9147 encode_mve_3reg(base, qd, qn, qm)
9148}
9149
9150/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9151/// Format: EC9x xxxx - contiguous load, word-sized elements
9152fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9153 let qd_enc = qreg_to_num(qd) * 2;
9154 let rn = reg_to_bits(&addr.base);
9155 let offset = addr.offset;
9156 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9157 let abs_offset = offset.unsigned_abs();
9158 let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9159
9160 // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9161 0xED100E80
9162 | (u_bit << 23)
9163 | ((qd_enc >> 4) << 22)
9164 | (rn << 16)
9165 | ((qd_enc & 0xF) << 12)
9166 | (imm7 & 0x7F)
9167}
9168
9169/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9170fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9171 let qd_enc = qreg_to_num(qd) * 2;
9172 let rn = reg_to_bits(&addr.base);
9173 let offset = addr.offset;
9174 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9175 let abs_offset = offset.unsigned_abs();
9176 let imm7 = (abs_offset / 4) & 0x7F;
9177
9178 0xED000E80
9179 | (u_bit << 23)
9180 | ((qd_enc >> 4) << 22)
9181 | (rn << 16)
9182 | ((qd_enc & 0xF) << 12)
9183 | (imm7 & 0x7F)
9184}
9185
9186impl ArmEncoder {
9187 /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9188 fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9189 let mut result = Vec::new();
9190 let qd_num = qreg_to_num(qd);
9191
9192 // Load each 32-bit word into R12 (temp) then VMOV into S-register
9193 for i in 0..4 {
9194 let word = u32::from_le_bytes([
9195 bytes[i * 4],
9196 bytes[i * 4 + 1],
9197 bytes[i * 4 + 2],
9198 bytes[i * 4 + 3],
9199 ]);
9200 let lo16 = word & 0xFFFF;
9201 let hi16 = (word >> 16) & 0xFFFF;
9202
9203 // MOVW R12, #lo16
9204 result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9205 // MOVT R12, #hi16
9206 if hi16 != 0 {
9207 result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9208 }
9209
9210 // VMOV Sn, R12 where Sn = Qd*4 + i
9211 let s_num = qd_num * 4 + i as u32;
9212 let (vn, n) = encode_sreg(s_num);
9213 let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9214 result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9215 }
9216
9217 Ok(result)
9218 }
9219
9220 /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9221 fn encode_thumb_mve_lane_wise_f32_binop(
9222 &self,
9223 qd: &QReg,
9224 qn: &QReg,
9225 qm: &QReg,
9226 vfp_base: u32,
9227 ) -> Result<Vec<u8>> {
9228 let mut result = Vec::new();
9229 let qd_num = qreg_to_num(qd);
9230 let qn_num = qreg_to_num(qn);
9231 let qm_num = qreg_to_num(qm);
9232
9233 // For each lane 0..3: use S-registers directly (Q aliasing)
9234 for i in 0..4u32 {
9235 let sd = qd_num * 4 + i;
9236 let sn = qn_num * 4 + i;
9237 let sm = qm_num * 4 + i;
9238
9239 let (vd, d) = encode_sreg(sd);
9240 let (vn, n) = encode_sreg(sn);
9241 let (vm, m) = encode_sreg(sm);
9242
9243 let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9244 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9245 }
9246
9247 Ok(result)
9248 }
9249
9250 /// Encode lane-wise f32 VSQRT via S-register extraction
9251 fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9252 let mut result = Vec::new();
9253 let qd_num = qreg_to_num(qd);
9254 let qm_num = qreg_to_num(qm);
9255
9256 // VSQRT.F32 base: 0xEEB10AC0
9257 for i in 0..4u32 {
9258 let sd = qd_num * 4 + i;
9259 let sm = qm_num * 4 + i;
9260
9261 let (vd, d) = encode_sreg(sd);
9262 let (vm, m) = encode_sreg(sm);
9263
9264 let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9265 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9266 }
9267
9268 Ok(result)
9269 }
9270}
9271
9272/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9273/// expansion: the registers, beyond the op's declared RESULT registers and the
9274/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9275/// modified when it completes. Transient-but-restored traffic (push/pop through
9276/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9277///
9278/// This is the SINGLE declaration site for that contract — the one place a
9279/// future expansion that must borrow a register says so (the repo rule: one
9280/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9281/// from the expansion's observed behavior: a contract read off the bytes would
9282/// rubber-stamp any clobber. Intent is declared here; the canary gate
9283/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9284/// bytes of every variant the shipped rule table emits, on both backends, with
9285/// every non-contract register holding a distinctive canary, and fails on any
9286/// undeclared write — and on any declared register the expansion never
9287/// actually writes, so an over-broad declaration cannot hollow the gate.
9288///
9289/// The default is the STRICTEST reading — result registers only — which is
9290/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9291/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9292/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9293/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9294/// the i64 shifts and bit-counts off their own operand registers, so every
9295/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9296/// A32. Backend-independent for the same reason; if a backend's expansion ever
9297/// diverges, this signature grows a backend parameter in the same PR.
9298pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9299 // No variant currently borrows any register beyond R12. A new declaration
9300 // is added as a `match op { .. }` arm here — nowhere else.
9301 let _ = op;
9302 &[]
9303}
9304
9305#[cfg(test)]
9306mod tests {
9307 use super::*;
9308
9309 #[test]
9310 fn test_encoder_creation() {
9311 let encoder_arm = ArmEncoder::new_arm32();
9312 assert!(!encoder_arm.thumb_mode);
9313
9314 let encoder_thumb = ArmEncoder::new_thumb2();
9315 assert!(encoder_thumb.thumb_mode);
9316 }
9317
9318 /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9319 /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9320 /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9321 /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9322 /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9323 /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9324 /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9325 /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9326 /// #204 hardening. With rd=R8 the boolean died in the flags
9327 /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9328 /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9329 #[test]
9330 fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9331 use synth_synthesis::{ArmOp, Condition, Reg};
9332 let enc = ArmEncoder::new_thumb2();
9333 let bytes = enc
9334 .encode(&ArmOp::I64SetCond {
9335 rd: Reg::R8,
9336 rn_lo: Reg::R2,
9337 rn_hi: Reg::R3,
9338 rm_lo: Reg::R6,
9339 rm_hi: Reg::R7,
9340 cond: Condition::EQ,
9341 })
9342 .unwrap();
9343 // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9344 // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9345 let halfwords: Vec<u16> = bytes
9346 .chunks(2)
9347 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9348 .collect();
9349 assert!(
9350 halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9351 "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9352 );
9353 assert!(
9354 !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9355 "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9356 );
9357
9358 let bytes_z = enc
9359 .encode(&ArmOp::I64SetCondZ {
9360 rd: Reg::R8,
9361 rn_lo: Reg::R2,
9362 rn_hi: Reg::R3,
9363 })
9364 .unwrap();
9365 let hw_z: Vec<u16> = bytes_z
9366 .chunks(2)
9367 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9368 .collect();
9369 assert!(
9370 hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9371 "SetCondZ high rd MOV.W: {hw_z:04x?}"
9372 );
9373 // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9374 assert!(
9375 hw_z.contains(&(0xF1B0 | 8)),
9376 "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9377 );
9378 }
9379
9380 #[test]
9381 fn test_encode_setcond_high_reg_uses_mov_w_204() {
9382 use synth_synthesis::{ArmOp, Condition, Reg};
9383 let enc = ArmEncoder::new_thumb2();
9384 // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9385 let hi = enc
9386 .encode(&ArmOp::SetCond {
9387 rd: Reg::R12,
9388 cond: Condition::NE,
9389 })
9390 .unwrap();
9391 assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9392 // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9393 assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9394 assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9395 assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9396 assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9397 // Low Rd keeps the compact 16-bit MOVS form.
9398 let lo = enc
9399 .encode(&ArmOp::SetCond {
9400 rd: Reg::R0,
9401 cond: Condition::NE,
9402 })
9403 .unwrap();
9404 assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9405 assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9406 assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9407 }
9408
9409 /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9410 /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9411 /// A32: cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9412 #[test]
9413 fn test_encode_umull_209b() {
9414 use synth_synthesis::{ArmOp, Reg};
9415 let op = ArmOp::Umull {
9416 rdlo: Reg::R4,
9417 rdhi: Reg::R5,
9418 rn: Reg::R0,
9419 rm: Reg::R3,
9420 };
9421 // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9422 let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9423 assert_eq!(
9424 t,
9425 vec![0xA0, 0xFB, 0x03, 0x45],
9426 "umull r4,r5,r0,r3 (T2): {t:02x?}"
9427 );
9428 // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9429 let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9430 assert_eq!(
9431 a,
9432 0xE085_4390u32.to_le_bytes().to_vec(),
9433 "umull (A32): {a:02x?}"
9434 );
9435 }
9436
9437 /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9438 /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9439 /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9440 /// the access to the wrong runtime address (silent miscompile on the default
9441 /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9442 /// load from `[ip, #off]`. Verify the bytes.
9443 #[test]
9444 fn test_encode_arm32_indexed_load_keeps_index_206() {
9445 use synth_synthesis::{ArmOp, MemAddr, Reg};
9446 let enc = ArmEncoder::new_arm32();
9447 // ldr r0, [r11, r1, #8] must NOT collapse to a single immediate ldr.
9448 let bytes = enc
9449 .encode(&ArmOp::Ldr {
9450 rd: Reg::R0,
9451 addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9452 })
9453 .unwrap();
9454 assert_eq!(
9455 bytes.len(),
9456 8,
9457 "expected ADD ip + LDR (2 words): {bytes:02x?}"
9458 );
9459 let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9460 let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9461 // ADD ip, r11, r1 = 0xE08BC001
9462 assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9463 // LDR r0, [ip, #8] = 0xE59C0008
9464 assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9465 // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9466 assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9467 }
9468
9469 /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9470 /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9471 /// the function silently returned the leftover table-index value. The A32
9472 /// encoder must emit a real dispatch expansion, since #642 guarded by an
9473 /// inline bounds check:
9474 /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9475 /// MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9476 #[test]
9477 fn test_encode_arm32_call_indirect_is_real_call_594() {
9478 use synth_synthesis::{ArmOp, Reg};
9479 let enc = ArmEncoder::new_arm32();
9480 let bytes = enc
9481 .encode(&ArmOp::CallIndirect {
9482 rd: Reg::R0,
9483 type_idx: 0,
9484 table_index_reg: Reg::R0,
9485 table_size: 4,
9486 table_byte_offset: 0,
9487 null_check: false,
9488 type_check: None,
9489 })
9490 .unwrap();
9491 assert_eq!(
9492 bytes.len(),
9493 28,
9494 "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9495 );
9496 let words: Vec<u32> = bytes
9497 .as_chunks::<4>()
9498 .0
9499 .iter()
9500 .map(|&w| u32::from_le_bytes(w))
9501 .collect();
9502 // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9503 assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9504 assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9505 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9506 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9507 // MOV r12, r0, LSL #2 = 0xE1A0C100
9508 assert_eq!(
9509 words[4], 0xE1A0_C100,
9510 "MOV r12,r0,LSL#2: {:#010x}",
9511 words[4]
9512 );
9513 // LDR r12, [r11, r12] = 0xE79BC00C
9514 assert_eq!(
9515 words[5], 0xE79B_C00C,
9516 "LDR r12,[r11,r12]: {:#010x}",
9517 words[5]
9518 );
9519 // BLX r12 = 0xE12FFF3C
9520 assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9521 // The bug: a single NOP word. Must never come back.
9522 assert!(
9523 !bytes
9524 .as_chunks::<4>()
9525 .0
9526 .iter()
9527 .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9528 "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9529 );
9530
9531 // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9532 let bytes = enc
9533 .encode(&ArmOp::CallIndirect {
9534 rd: Reg::R0,
9535 type_idx: 0,
9536 table_index_reg: Reg::R4,
9537 table_size: 4,
9538 table_byte_offset: 0,
9539 null_check: false,
9540 type_check: None,
9541 })
9542 .unwrap();
9543 let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9544 assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9545 let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9546 assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9547 }
9548
9549 /// #642: a table size above 16 bits must not be silently truncated by the
9550 /// MOVW — the A32 guard adds a MOVT for the high half.
9551 #[test]
9552 fn test_encode_arm32_call_indirect_wide_table_size_642() {
9553 use synth_synthesis::{ArmOp, Reg};
9554 let enc = ArmEncoder::new_arm32();
9555 let bytes = enc
9556 .encode(&ArmOp::CallIndirect {
9557 rd: Reg::R0,
9558 type_idx: 0,
9559 table_index_reg: Reg::R0,
9560 table_size: 0x0002_0003,
9561 table_byte_offset: 0,
9562 null_check: false,
9563 type_check: None,
9564 })
9565 .unwrap();
9566 assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9567 let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9568 let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9569 assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9570 assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9571 }
9572
9573 /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9574 /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9575 /// [r11, ip]; blx ip`.
9576 ///
9577 /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9578 /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9579 /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9580 /// 7:6), so the index was destroyed and every call_indirect dispatched
9581 /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9582 /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9583 /// execution-validated under unicorn against the wasmtime oracle on a
9584 /// multi-entry table (indexes 0, 1, 3 —
9585 /// scripts/repro/call_indirect_597_differential.py) before this pin was
9586 /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9587 /// never come back).
9588 #[test]
9589 fn test_encode_thumb_call_indirect_lsl2_597() {
9590 use synth_synthesis::{ArmOp, Reg};
9591 let enc = ArmEncoder::new_thumb2();
9592 let bytes = enc
9593 .encode(&ArmOp::CallIndirect {
9594 rd: Reg::R0,
9595 type_idx: 0,
9596 table_index_reg: Reg::R0,
9597 table_size: 4,
9598 table_byte_offset: 0,
9599 null_check: false,
9600 type_check: None,
9601 })
9602 .unwrap();
9603 assert_eq!(
9604 bytes,
9605 vec![
9606 // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9607 0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9608 0x60, 0x45, // cmp r0, ip
9609 0x00, 0xD3, // blo .+4 (skip the udf)
9610 0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9611 // #597-pinned dispatch
9612 0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9613 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9614 0xE0, 0x47, // blx ip
9615 ],
9616 "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9617 );
9618 // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9619 assert!(
9620 !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9621 "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9622 );
9623
9624 // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9625 // and the cmp's Rn field.
9626 let bytes = enc
9627 .encode(&ArmOp::CallIndirect {
9628 rd: Reg::R0,
9629 type_idx: 0,
9630 table_index_reg: Reg::R4,
9631 table_size: 4,
9632 table_byte_offset: 0,
9633 null_check: false,
9634 type_check: None,
9635 })
9636 .unwrap();
9637 assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9638 assert_eq!(
9639 &bytes[10..14],
9640 &[0x4F, 0xEA, 0x84, 0x0C],
9641 "mov.w ip, r4, LSL #2: {bytes:02x?}"
9642 );
9643 }
9644
9645 /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9646 /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9647 /// the N bit set; a table size above 16 bits adds a MOVT.
9648 #[test]
9649 fn test_encode_thumb_call_indirect_guard_shapes_642() {
9650 use synth_synthesis::{ArmOp, Reg};
9651 let enc = ArmEncoder::new_thumb2();
9652 let bytes = enc
9653 .encode(&ArmOp::CallIndirect {
9654 rd: Reg::R0,
9655 type_idx: 0,
9656 table_index_reg: Reg::R8,
9657 table_size: 3,
9658 table_byte_offset: 0,
9659 null_check: false,
9660 type_check: None,
9661 })
9662 .unwrap();
9663 // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9664 assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9665
9666 let bytes = enc
9667 .encode(&ArmOp::CallIndirect {
9668 rd: Reg::R0,
9669 type_idx: 0,
9670 table_index_reg: Reg::R0,
9671 table_size: 0x0002_0003,
9672 table_byte_offset: 0,
9673 null_check: false,
9674 type_check: None,
9675 })
9676 .unwrap();
9677 // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9678 assert_eq!(
9679 &bytes[0..8],
9680 &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9681 "movw ip,#3; movt ip,#2: {bytes:02x?}"
9682 );
9683 }
9684
9685 /// #650: a non-zero table base offset (table N of the contiguous R11
9686 /// region) routes the Thumb-2 pointer load through
9687 /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9688 /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9689 #[test]
9690 fn test_encode_thumb_call_indirect_table_offset_650() {
9691 use synth_synthesis::{ArmOp, Reg};
9692 let enc = ArmEncoder::new_thumb2();
9693 // falcon's fused-component shape: table 0 has 7 entries, so table 1
9694 // sits at byte offset 28.
9695 let bytes = enc
9696 .encode(&ArmOp::CallIndirect {
9697 rd: Reg::R0,
9698 type_idx: 0,
9699 table_index_reg: Reg::R1,
9700 table_size: 41,
9701 table_byte_offset: 28,
9702 null_check: false,
9703 type_check: None,
9704 })
9705 .unwrap();
9706 assert_eq!(
9707 bytes,
9708 vec![
9709 // #642 bounds guard against TABLE 1's OWN size (41)
9710 0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9711 0x61, 0x45, // cmp r1, ip
9712 0x00, 0xD3, // blo .+4 (skip the udf)
9713 0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9714 // dispatch through table 1's base (R11 + 28)
9715 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9716 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9717 0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9718 0xE0, 0x47, // blx ip
9719 ],
9720 "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9721 );
9722
9723 // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9724 // imm-form ldr) — single-table byte identity by construction.
9725 let zero = enc
9726 .encode(&ArmOp::CallIndirect {
9727 rd: Reg::R0,
9728 type_idx: 0,
9729 table_index_reg: Reg::R1,
9730 table_size: 41,
9731 table_byte_offset: 0,
9732 null_check: false,
9733 type_check: None,
9734 })
9735 .unwrap();
9736 assert_eq!(
9737 &zero[10..],
9738 &[
9739 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9740 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9741 0xE0, 0x47, // blx ip
9742 ],
9743 "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9744 );
9745 }
9746
9747 /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9748 /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9749 #[test]
9750 fn test_encode_arm32_call_indirect_table_offset_650() {
9751 use synth_synthesis::{ArmOp, Reg};
9752 let enc = ArmEncoder::new_arm32();
9753 let bytes = enc
9754 .encode(&ArmOp::CallIndirect {
9755 rd: Reg::R0,
9756 type_idx: 0,
9757 table_index_reg: Reg::R1,
9758 table_size: 41,
9759 table_byte_offset: 28,
9760 null_check: false,
9761 type_check: None,
9762 })
9763 .unwrap();
9764 let words: Vec<u32> = bytes
9765 .as_chunks::<4>()
9766 .0
9767 .iter()
9768 .map(|&w| u32::from_le_bytes(w))
9769 .collect();
9770 assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9771 assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9772 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9773 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9774 assert_eq!(
9775 words[4], 0xE1A0_C101,
9776 "MOV r12,r1,LSL#2: {:#010x}",
9777 words[4]
9778 );
9779 assert_eq!(
9780 words[5], 0xE08B_C00C,
9781 "ADD r12,r11,r12 (#650): {:#010x}",
9782 words[5]
9783 );
9784 assert_eq!(
9785 words[6], 0xE59C_C01C,
9786 "LDR r12,[r12,#28] (#650): {:#010x}",
9787 words[6]
9788 );
9789 assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9790 }
9791
9792 /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9793 /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9794 /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9795 /// branch to address 0. `null_check: false` keeps the expansion
9796 /// byte-identical to the pre-#664 form (by-construction pin).
9797 #[test]
9798 fn test_encode_thumb_call_indirect_null_check_664() {
9799 use synth_synthesis::{ArmOp, Reg};
9800 let enc = ArmEncoder::new_thumb2();
9801 let op = |null_check| ArmOp::CallIndirect {
9802 rd: Reg::R0,
9803 type_idx: 0,
9804 table_index_reg: Reg::R1,
9805 table_size: 4,
9806 table_byte_offset: 0,
9807 null_check,
9808 type_check: None,
9809 };
9810 let with = enc.encode(&op(true)).unwrap();
9811 let without = enc.encode(&op(false)).unwrap();
9812 // The checked form = the unchecked form with EXACTLY the three-insn
9813 // null check spliced in before the final BLX (byte identity of the
9814 // shared prefix/suffix — nothing else may move).
9815 assert_eq!(
9816 with.len(),
9817 without.len() + 8,
9818 "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9819 );
9820 let blx_at = without.len() - 2;
9821 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9822 assert_eq!(
9823 &with[blx_at..],
9824 &[
9825 0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9826 0x00, 0xD1, // bne .+4 (skip the udf)
9827 0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9828 0xE0, 0x47, // blx ip
9829 ],
9830 "null check precedes the BLX: {with:02x?}"
9831 );
9832 assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9833 }
9834
9835 /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9836 /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9837 #[test]
9838 fn test_encode_arm32_call_indirect_null_check_664() {
9839 use synth_synthesis::{ArmOp, Reg};
9840 let enc = ArmEncoder::new_arm32();
9841 let op = |null_check| ArmOp::CallIndirect {
9842 rd: Reg::R0,
9843 type_idx: 0,
9844 table_index_reg: Reg::R1,
9845 table_size: 4,
9846 table_byte_offset: 0,
9847 null_check,
9848 type_check: None,
9849 };
9850 let with = enc.encode(&op(true)).unwrap();
9851 let without = enc.encode(&op(false)).unwrap();
9852 assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9853 let blx_at = without.len() - 4;
9854 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9855 let words: Vec<u32> = with[blx_at..]
9856 .as_chunks::<4>()
9857 .0
9858 .iter()
9859 .map(|&w| u32::from_le_bytes(w))
9860 .collect();
9861 assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9862 assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9863 assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9864 assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9865 }
9866
9867 /// #676: `type_check` splices the runtime type check — scale the index,
9868 /// load the slot's structural class id from the type-id sidecar
9869 /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9870 /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9871 /// the dispatch tail. `type_check: None` keeps the expansion
9872 /// byte-identical to the pre-#676 form (by-construction pin, the same
9873 /// trick as #650 offset-0 / #664 `null_check: false`).
9874 #[test]
9875 fn test_encode_thumb_call_indirect_type_check_676() {
9876 use synth_synthesis::{ArmOp, Reg};
9877 let enc = ArmEncoder::new_thumb2();
9878 let op = |type_check| ArmOp::CallIndirect {
9879 rd: Reg::R0,
9880 type_idx: 1,
9881 table_index_reg: Reg::R1,
9882 table_size: 5,
9883 table_byte_offset: 0,
9884 null_check: false,
9885 type_check,
9886 };
9887 let with = enc.encode(&op(Some((2, 20)))).unwrap();
9888 let without = enc.encode(&op(None)).unwrap();
9889 // The checked form = the unchecked form with EXACTLY the six-insn
9890 // type check spliced in after the bounds guard (byte identity of
9891 // the shared prefix/suffix — nothing else may move).
9892 assert_eq!(
9893 with.len(),
9894 without.len() + 20,
9895 "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9896 );
9897 // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9898 let guard_end = 10;
9899 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9900 assert_eq!(
9901 &with[guard_end..guard_end + 20],
9902 &[
9903 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9904 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9905 0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9906 0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9907 0x00, 0xD0, // beq .+4 (skip the udf on a match)
9908 0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9909 ],
9910 "type check follows the bounds guard: {with:02x?}"
9911 );
9912 assert_eq!(
9913 &with[guard_end + 20..],
9914 &without[guard_end..],
9915 "dispatch tail unchanged (idx*4 recomputed)"
9916 );
9917 }
9918
9919 /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9920 /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9921 /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9922 /// bytes identical.
9923 #[test]
9924 fn test_encode_arm32_call_indirect_type_check_676() {
9925 use synth_synthesis::{ArmOp, Reg};
9926 let enc = ArmEncoder::new_arm32();
9927 let op = |type_check| ArmOp::CallIndirect {
9928 rd: Reg::R0,
9929 type_idx: 1,
9930 table_index_reg: Reg::R1,
9931 table_size: 5,
9932 table_byte_offset: 0,
9933 null_check: false,
9934 type_check,
9935 };
9936 let with = enc.encode(&op(Some((2, 20)))).unwrap();
9937 let without = enc.encode(&op(None)).unwrap();
9938 assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
9939 // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
9940 let guard_end = 16;
9941 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9942 let words: Vec<u32> = with[guard_end..guard_end + 24]
9943 .as_chunks::<4>()
9944 .0
9945 .iter()
9946 .map(|&w| u32::from_le_bytes(w))
9947 .collect();
9948 assert_eq!(
9949 words[0], 0xE1A0_C101,
9950 "MOV r12,r1,LSL#2: {:#010x}",
9951 words[0]
9952 );
9953 assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
9954 assert_eq!(
9955 words[2], 0xE59C_C014,
9956 "LDR r12,[r12,#20] (sidecar): {:#010x}",
9957 words[2]
9958 );
9959 assert_eq!(
9960 words[3], 0xE35C_0002,
9961 "CMP r12,#2 (expected class id): {:#010x}",
9962 words[3]
9963 );
9964 assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
9965 assert_eq!(
9966 words[5], 0xE7F0_00F0,
9967 "UDF (type-mismatch trap): {:#010x}",
9968 words[5]
9969 );
9970 assert_eq!(
9971 &with[guard_end + 24..],
9972 &without[guard_end..],
9973 "dispatch tail unchanged"
9974 );
9975 }
9976
9977 /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9978 /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9979 /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9980 /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9981 /// dropping the address operand and miscompiling every optimized memory
9982 /// access. High registers must use the 32-bit `.W` forms.
9983 #[test]
9984 fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9985 let encoder = ArmEncoder::new_thumb2();
9986
9987 // add ip, ip, r0 — the exact MemLoad/MemStore base+addr op.
9988 let code = encoder
9989 .encode(&ArmOp::Add {
9990 rd: Reg::R12,
9991 rn: Reg::R12,
9992 op2: Operand2::Reg(Reg::R0),
9993 })
9994 .unwrap();
9995 // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9996 assert_eq!(
9997 code,
9998 vec![0x0C, 0xEB, 0x00, 0x0C],
9999 "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
10000 );
10001 // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
10002 assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
10003
10004 // Low-register add stays 16-bit (no regression for the common case).
10005 let lo = encoder
10006 .encode(&ArmOp::Add {
10007 rd: Reg::R1,
10008 rn: Reg::R2,
10009 op2: Operand2::Reg(Reg::R3),
10010 })
10011 .unwrap();
10012 assert_eq!(
10013 lo.len(),
10014 2,
10015 "low-reg ADD should remain 16-bit, got {lo:02X?}"
10016 );
10017 }
10018
10019 /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
10020 /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
10021 #[test]
10022 fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
10023 let encoder = ArmEncoder::new_thumb2();
10024
10025 // adds r10, r10, r8 → ADDS.W = EB1A 0A08
10026 let adds = encoder
10027 .encode(&ArmOp::Adds {
10028 rd: Reg::R10,
10029 rn: Reg::R10,
10030 op2: Operand2::Reg(Reg::R8),
10031 })
10032 .unwrap();
10033 assert_eq!(
10034 adds,
10035 vec![0x1A, 0xEB, 0x08, 0x0A],
10036 "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
10037 );
10038
10039 // subs r10, r10, r8 → SUBS.W = EBBA 0A08
10040 let subs = encoder
10041 .encode(&ArmOp::Subs {
10042 rd: Reg::R10,
10043 rn: Reg::R10,
10044 op2: Operand2::Reg(Reg::R8),
10045 })
10046 .unwrap();
10047 assert_eq!(
10048 subs,
10049 vec![0xBA, 0xEB, 0x08, 0x0A],
10050 "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
10051 );
10052 }
10053
10054 /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
10055 /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
10056 #[test]
10057 fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
10058 let encoder = ArmEncoder::new_thumb2();
10059
10060 // cmn r10, r8 → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
10061 let cmn = encoder
10062 .encode(&ArmOp::Cmn {
10063 rn: Reg::R10,
10064 op2: Operand2::Reg(Reg::R8),
10065 })
10066 .unwrap();
10067 assert_eq!(
10068 cmn,
10069 vec![0x1A, 0xEB, 0x08, 0x0F],
10070 "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
10071 );
10072
10073 // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
10074 let lo = encoder
10075 .encode(&ArmOp::Cmn {
10076 rn: Reg::R1,
10077 op2: Operand2::Reg(Reg::R2),
10078 })
10079 .unwrap();
10080 assert_eq!(
10081 lo.len(),
10082 2,
10083 "low-reg CMN should remain 16-bit, got {lo:02X?}"
10084 );
10085 assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
10086 }
10087
10088 /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
10089 /// guards its registers must return Err, not panic under debug-assertions.
10090 /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
10091 #[test]
10092 fn test_encode_pc_operand_returns_err_not_panic_185() {
10093 let encoder = ArmEncoder::new_thumb2();
10094 for op in [
10095 ArmOp::Sdiv {
10096 rd: Reg::PC,
10097 rn: Reg::R0,
10098 rm: Reg::R1,
10099 },
10100 ArmOp::Udiv {
10101 rd: Reg::R0,
10102 rn: Reg::PC,
10103 rm: Reg::R1,
10104 },
10105 ArmOp::Sdiv {
10106 rd: Reg::R0,
10107 rn: Reg::R1,
10108 rm: Reg::PC,
10109 },
10110 ] {
10111 let r = encoder.encode(&op);
10112 assert!(
10113 r.is_err(),
10114 "encode({op:?}) must return Err for a PC operand, got {r:?}"
10115 );
10116 }
10117 // Valid registers still encode fine (no false rejection).
10118 assert!(
10119 encoder
10120 .encode(&ArmOp::Sdiv {
10121 rd: Reg::R0,
10122 rn: Reg::R1,
10123 rm: Reg::R2
10124 })
10125 .is_ok()
10126 );
10127 }
10128
10129 #[test]
10130 fn test_encode_nop_arm32() {
10131 let encoder = ArmEncoder::new_arm32();
10132 let code = encoder.encode(&ArmOp::Nop).unwrap();
10133
10134 assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
10135 assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
10136 }
10137
10138 #[test]
10139 fn test_encode_nop_thumb() {
10140 let encoder = ArmEncoder::new_thumb2();
10141 let code = encoder.encode(&ArmOp::Nop).unwrap();
10142
10143 assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10144 assert_eq!(code, vec![0x00, 0xBF]); // NOP
10145 }
10146
10147 #[test]
10148 fn test_encode_mov_immediate_arm32() {
10149 let encoder = ArmEncoder::new_arm32();
10150 let op = ArmOp::Mov {
10151 rd: Reg::R0,
10152 op2: Operand2::Imm(42),
10153 };
10154
10155 let code = encoder.encode(&op).unwrap();
10156 assert_eq!(code.len(), 4);
10157
10158 // Verify it's a MOV instruction (bits should have immediate flag set)
10159 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10160 assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10161 }
10162
10163 #[test]
10164 fn test_encode_add_registers_arm32() {
10165 let encoder = ArmEncoder::new_arm32();
10166 let op = ArmOp::Add {
10167 rd: Reg::R0,
10168 rn: Reg::R1,
10169 op2: Operand2::Reg(Reg::R2),
10170 };
10171
10172 let code = encoder.encode(&op).unwrap();
10173 assert_eq!(code.len(), 4);
10174
10175 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10176 // Verify it's an ADD instruction with correct opcode
10177 assert_eq!(instr & 0x0FE00000, 0x00800000);
10178 }
10179
10180 /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10181 /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10182 /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10183 #[test]
10184 fn test_encode_add_imm_large_350() {
10185 let enc = ArmEncoder::new_thumb2();
10186
10187 // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10188 // VALUE must be right (#681: this test used to assert only the length,
10189 // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10190 // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10191 // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10192 let small = enc
10193 .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10194 .unwrap();
10195 assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10196
10197 // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10198 fn movx_imm16(b: &[u8]) -> u32 {
10199 let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10200 let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10201 let imm4 = hw1 & 0xF;
10202 let i = (hw1 >> 10) & 1;
10203 let imm3 = (hw2 >> 12) & 0x7;
10204 let imm8 = hw2 & 0xFF;
10205 (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10206 }
10207 fn movx_rd(b: &[u8]) -> u32 {
10208 (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10209 }
10210
10211 // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10212 // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10213 let seq = enc
10214 .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10215 .unwrap();
10216 assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10217 // MOVW r12, #0x1170
10218 assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10219 assert_eq!(movx_rd(&seq[0..4]), 12);
10220 assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10221 // MOVT r12, #0x0001
10222 assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10223 assert_eq!(movx_rd(&seq[4..8]), 12);
10224 assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10225 // ADD.W r12, r0, r12 (EB00 | rn=0 ; rd=12, rm=12)
10226 let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10227 let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10228 assert_eq!(add1 & 0xFFF0, 0xEB00);
10229 assert_eq!(add1 & 0xF, 0); // rn = r0
10230 assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10231 assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10232 // The materialized scratch must reconstruct exactly 70000.
10233 assert_eq!(
10234 (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10235 70000
10236 );
10237
10238 // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10239 let seq16 = enc
10240 .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10241 .unwrap();
10242 assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10243 assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10244 assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10245
10246 // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10247 // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10248 let inplace = enc
10249 .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10250 .unwrap();
10251 assert_eq!(inplace.len(), 12);
10252 assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10253 assert_eq!(
10254 (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10255 0x12345
10256 );
10257 // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10258 let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10259 assert_eq!(ip_add2 & 0xF, 12);
10260 assert_eq!((ip_add2 >> 8) & 0xF, 5);
10261 }
10262
10263 /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10264 /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10265 /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10266 /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10267 /// computed a wrong address (and bypassed --safety-bounds software: the
10268 /// guard checked the intended address, the access used the mis-encoded
10269 /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10270 /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10271 /// `encode_thumb32_add` already uses per #253.
10272 ///
10273 /// Every expected byte sequence below is pinned against clang
10274 /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10275 #[test]
10276 fn test_encode_add_imm_thumb_expand_681() {
10277 let enc = ArmEncoder::new_thumb2();
10278 let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10279
10280 // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10281 // clang: add.w r12, r0, #0xff = f100 0cff
10282 assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10283
10284 // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10285 // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10286 // clang: addw r12, r0, #0x100 = f200 1c00
10287 assert_eq!(
10288 add(&Reg::R12, &Reg::R0, 0x100),
10289 vec![0x00, 0xF2, 0x00, 0x1C]
10290 );
10291 // clang: addw r12, r0, #0x104 = f200 1c04
10292 assert_eq!(
10293 add(&Reg::R12, &Reg::R0, 0x104),
10294 vec![0x00, 0xF2, 0x04, 0x1C]
10295 );
10296 // clang: addw r12, r0, #0x200 = f200 2c00
10297 assert_eq!(
10298 add(&Reg::R12, &Reg::R0, 0x200),
10299 vec![0x00, 0xF2, 0x00, 0x2C]
10300 );
10301 // clang: addw r12, r0, #0x3fc = f200 3cfc
10302 assert_eq!(
10303 add(&Reg::R12, &Reg::R0, 0x3FC),
10304 vec![0x00, 0xF2, 0xFC, 0x3C]
10305 );
10306 // clang: addw r12, r0, #0x400 = f200 4c00
10307 assert_eq!(
10308 add(&Reg::R12, &Reg::R0, 0x400),
10309 vec![0x00, 0xF2, 0x00, 0x4C]
10310 );
10311 // clang: addw r12, r0, #0xfff = f600 7cff
10312 assert_eq!(
10313 add(&Reg::R12, &Reg::R0, 0xFFF),
10314 vec![0x00, 0xF6, 0xFF, 0x7C]
10315 );
10316 // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10317 assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10318 }
10319
10320 /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10321 /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10322 /// (T4-style) form, so a non-representable immediate must Err loudly
10323 /// (#253/#255/#378 class: never silently encode a different constant).
10324 /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10325 /// pinned here bit-for-bit against clang.
10326 #[test]
10327 fn test_rsb_and_imm_thumb_expand_gate_681() {
10328 let enc = ArmEncoder::new_thumb2();
10329
10330 // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10331 let rsb = enc
10332 .encode(&ArmOp::Rsb {
10333 rd: Reg::R3,
10334 rn: Reg::R2,
10335 imm: 32,
10336 })
10337 .unwrap();
10338 assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10339
10340 // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10341 assert!(
10342 enc.encode(&ArmOp::Rsb {
10343 rd: Reg::R3,
10344 rn: Reg::R2,
10345 imm: 0x101,
10346 })
10347 .is_err(),
10348 "non-ThumbExpandImm RSB immediate must Err"
10349 );
10350
10351 // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10352 let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10353 assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10354 assert!(
10355 enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10356 "non-ThumbExpandImm AND immediate must Err"
10357 );
10358
10359 // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10360 // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10361 let a32 = ArmEncoder::new_arm32();
10362 assert!(
10363 a32.encode(&ArmOp::Rsb {
10364 rd: Reg::R3,
10365 rn: Reg::R2,
10366 imm: 0x120,
10367 })
10368 .is_err(),
10369 "A32 RSB immediate > 0xFF must Err, not mask"
10370 );
10371 // imm 32 (the only value real codegen emits) still encodes.
10372 assert!(
10373 a32.encode(&ArmOp::Rsb {
10374 rd: Reg::R3,
10375 rn: Reg::R2,
10376 imm: 32,
10377 })
10378 .is_ok()
10379 );
10380 }
10381
10382 /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10383 /// with ARBITRARY registers, including the one case the in-place lowering
10384 /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10385 /// register) would alias Rn and clobber it before the ADD reads it. The
10386 /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10387 /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10388 /// is non-allocatable; this guards only the fuzz/adversarial path.)
10389 #[test]
10390 fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10391 let enc = ArmEncoder::new_thumb2();
10392 // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10393 let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10394 assert!(
10395 r.is_err(),
10396 "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10397 );
10398 // Small imm with rd==rn==R12 still takes the single-instruction fast path
10399 // (no scratch needed) and must succeed — the guard is scoped to the
10400 // out-of-range lowering only.
10401 let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10402 assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10403 }
10404
10405 /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10406 /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10407 /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10408 /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10409 /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10410 /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10411 /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10412 /// harness — which drives arbitrary operands — still passes.
10413 #[test]
10414 fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10415 let enc = ArmEncoder::new_arm32();
10416 let bad = enc.encode(&ArmOp::Add {
10417 rd: Reg::R0,
10418 rn: Reg::R1,
10419 op2: Operand2::Imm(0x1FF),
10420 });
10421 assert!(
10422 bad.is_err(),
10423 "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10424 to 0xFF), got {bad:?}"
10425 );
10426 // A representable rotated immediate still encodes fine (regression guard).
10427 let ok = enc.encode(&ArmOp::Add {
10428 rd: Reg::R0,
10429 rn: Reg::R1,
10430 op2: Operand2::Imm(0xFF),
10431 });
10432 assert!(
10433 ok.is_ok(),
10434 "0xFF is a valid rotated immediate, must stay Ok"
10435 );
10436 }
10437
10438 #[test]
10439 fn test_encode_ldr_arm32() {
10440 let encoder = ArmEncoder::new_arm32();
10441 let op = ArmOp::Ldr {
10442 rd: Reg::R0,
10443 addr: MemAddr::imm(Reg::R1, 4),
10444 };
10445
10446 let code = encoder.encode(&op).unwrap();
10447 assert_eq!(code.len(), 4);
10448
10449 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10450 // Verify load bit is set
10451 assert_eq!(instr & 0x00100000, 0x00100000);
10452 }
10453
10454 #[test]
10455 fn test_encode_str_arm32() {
10456 let encoder = ArmEncoder::new_arm32();
10457 let op = ArmOp::Str {
10458 rd: Reg::R0,
10459 addr: MemAddr::imm(Reg::SP, 0),
10460 };
10461
10462 let code = encoder.encode(&op).unwrap();
10463 assert_eq!(code.len(), 4);
10464 }
10465
10466 #[test]
10467 fn test_encode_branch_arm32() {
10468 let encoder = ArmEncoder::new_arm32();
10469 let op = ArmOp::Bl {
10470 label: "main".to_string(),
10471 };
10472
10473 let code = encoder.encode(&op).unwrap();
10474 assert_eq!(code.len(), 4);
10475
10476 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10477 // Verify BL opcode
10478 assert_eq!(instr & 0x0F000000, 0x0B000000);
10479 }
10480
10481 /// #1040: the A32 BL relocatable placeholder must carry an embedded addend
10482 /// of -8 so an R_ARM_CALL nets to exactly the symbol S — the A32 twin of
10483 /// the Thumb #167/#174 test above. A32 `BL` computes
10484 /// `target = P + 8 + (imm24 << 2)`, so:
10485 /// - `eb000000` (imm24 = 0, addend 0) lands at S+8, two instructions
10486 /// past the callee entry. This is what synth emitted before #1040, and
10487 /// combined with the mislabelled R_ARM_THM_CALL a real linker
10488 /// (`arm-none-eabi-ld`) corrupted the word to `eaca0000` — opcode
10489 /// `eb` (BL) flipped to `ea` (B), so LR was never set at all.
10490 /// - `ebfffffe` (imm24 = -2, offset -8) is `bl <self>` and nets to S.
10491 /// This is exactly what `arm-none-eabi-as -march=armv7-r` emits for
10492 /// `bl <extern>`, verified directly rather than derived from the ABI.
10493 #[test]
10494 fn test_encode_arm32_bl_placeholder_addend_1040() {
10495 let encoder = ArmEncoder::new_arm32();
10496 let code = encoder
10497 .encode(&ArmOp::Bl {
10498 label: "callee".to_string(),
10499 })
10500 .unwrap();
10501 assert_eq!(code.len(), 4, "A32 BL is one 32-bit word");
10502 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10503 assert_eq!(
10504 instr, 0xEBFF_FFFE,
10505 "A32 BL placeholder must be `ebfffffe` (gas's `bl <extern>`), not `eb000000` — a 0 addend resolves two instructions past the callee entry (#1040)"
10506 );
10507 // Spell the addend out so a future edit cannot satisfy the literal by
10508 // accident: sign-extended imm24, word-scaled, plus the +8 pipeline
10509 // bias, must be exactly 0 (branch-to-self).
10510 let imm24 = instr & 0x00FF_FFFF;
10511 let signed = ((imm24 as i32) << 8) >> 8;
10512 assert_eq!(
10513 8 + (signed << 2),
10514 0,
10515 "placeholder must branch to itself so the REL addend is -8"
10516 );
10517 }
10518
10519 /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10520 /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10521 /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10522 /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10523 /// - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10524 /// to fit (#167).
10525 /// - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10526 /// entry (#174).
10527 /// - 0xFFFE (addend -4) → lands at S. Correct.
10528 #[test]
10529 fn test_encode_thumb_bl_placeholder_addend_167_174() {
10530 let encoder = ArmEncoder::new_thumb2();
10531 let op = ArmOp::Bl {
10532 label: "callee".to_string(),
10533 };
10534
10535 let code = encoder.encode(&op).unwrap();
10536 assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10537
10538 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10539 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10540 assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10541 assert_eq!(
10542 hw2, 0xFFFE,
10543 "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10544 );
10545 assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10546 assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10547 }
10548
10549 /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10550 /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10551 /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10552 /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10553 /// displacement: gust_poll's loop-head `br_if` to an outer block end
10554 /// landed mid-shape (a spurious state write + spurious calls on the
10555 /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10556 /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10557 /// disassembler (`bne.w #0x224` = f040 8112).
10558 #[test]
10559 fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10560 use synth_synthesis::Condition;
10561 let encoder = ArmEncoder::new_thumb2();
10562
10563 // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10564 let code = encoder
10565 .encode(&ArmOp::BCondOffset {
10566 cond: Condition::NE,
10567 offset: 0x112,
10568 })
10569 .unwrap();
10570 assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10571 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10572 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10573 assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10574 assert_eq!(
10575 hw2, 0x8112,
10576 "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10577 0x8089 (offset>>1) is the halved #740 miscompile"
10578 );
10579
10580 // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10581 // imm6=0b111111, imm11=0x700 → f43f af00.
10582 let code = encoder
10583 .encode(&ArmOp::BCondOffset {
10584 cond: Condition::EQ,
10585 offset: -0x100,
10586 })
10587 .unwrap();
10588 assert_eq!(code.len(), 4);
10589 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10590 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10591 assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10592 assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10593
10594 // Narrow encoding stays byte-identical (in-range offsets untouched).
10595 let code = encoder
10596 .encode(&ArmOp::BCondOffset {
10597 cond: Condition::EQ,
10598 offset: 5,
10599 })
10600 .unwrap();
10601 assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10602
10603 // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10604 assert!(
10605 encoder
10606 .encode(&ArmOp::BCondOffset {
10607 cond: Condition::NE,
10608 offset: 1 << 19,
10609 })
10610 .is_err(),
10611 "out-of-range T3 offset must be a loud decline"
10612 );
10613 }
10614
10615 #[test]
10616 fn test_encode_sequence() {
10617 let encoder = ArmEncoder::new_arm32();
10618 let ops = vec![
10619 ArmOp::Mov {
10620 rd: Reg::R0,
10621 op2: Operand2::Imm(42),
10622 },
10623 ArmOp::Mov {
10624 rd: Reg::R1,
10625 op2: Operand2::Imm(10),
10626 },
10627 ArmOp::Add {
10628 rd: Reg::R2,
10629 rn: Reg::R0,
10630 op2: Operand2::Reg(Reg::R1),
10631 },
10632 ];
10633
10634 let code = encoder.encode_sequence(&ops).unwrap();
10635 assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10636 }
10637
10638 #[test]
10639 fn test_reg_to_bits() {
10640 assert_eq!(reg_to_bits(&Reg::R0), 0);
10641 assert_eq!(reg_to_bits(&Reg::R7), 7);
10642 assert_eq!(reg_to_bits(&Reg::SP), 13);
10643 assert_eq!(reg_to_bits(&Reg::LR), 14);
10644 assert_eq!(reg_to_bits(&Reg::PC), 15);
10645 }
10646
10647 #[test]
10648 fn test_encode_bitwise_operations() {
10649 let encoder = ArmEncoder::new_arm32();
10650
10651 let and_op = ArmOp::And {
10652 rd: Reg::R0,
10653 rn: Reg::R1,
10654 op2: Operand2::Reg(Reg::R2),
10655 };
10656 let and_code = encoder.encode(&and_op).unwrap();
10657 assert_eq!(and_code.len(), 4);
10658
10659 let orr_op = ArmOp::Orr {
10660 rd: Reg::R0,
10661 rn: Reg::R1,
10662 op2: Operand2::Reg(Reg::R2),
10663 };
10664 let orr_code = encoder.encode(&orr_op).unwrap();
10665 assert_eq!(orr_code.len(), 4);
10666
10667 let eor_op = ArmOp::Eor {
10668 rd: Reg::R0,
10669 rn: Reg::R1,
10670 op2: Operand2::Reg(Reg::R2),
10671 };
10672 let eor_code = encoder.encode(&eor_op).unwrap();
10673 assert_eq!(eor_code.len(), 4);
10674 }
10675
10676 // === Thumb-2 32-bit encoding tests ===
10677
10678 #[test]
10679 fn test_encode_sdiv_thumb2() {
10680 let encoder = ArmEncoder::new_thumb2();
10681 let op = ArmOp::Sdiv {
10682 rd: Reg::R0,
10683 rn: Reg::R1,
10684 rm: Reg::R2,
10685 };
10686
10687 let code = encoder.encode(&op).unwrap();
10688 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10689
10690 // SDIV R0, R1, R2: 0xFB91 0xF0F2
10691 // First halfword: 0xFB90 | Rn(1) = 0xFB91
10692 // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10693 // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10694 assert_eq!(code[0], 0x91);
10695 assert_eq!(code[1], 0xFB);
10696 assert_eq!(code[2], 0xF2);
10697 assert_eq!(code[3], 0xF0);
10698 }
10699
10700 #[test]
10701 fn test_encode_udiv_thumb2() {
10702 let encoder = ArmEncoder::new_thumb2();
10703 let op = ArmOp::Udiv {
10704 rd: Reg::R0,
10705 rn: Reg::R1,
10706 rm: Reg::R2,
10707 };
10708
10709 let code = encoder.encode(&op).unwrap();
10710 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10711
10712 // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10713 // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10714 assert_eq!(code[0], 0xB1);
10715 assert_eq!(code[1], 0xFB);
10716 assert_eq!(code[2], 0xF2);
10717 assert_eq!(code[3], 0xF0);
10718 }
10719
10720 #[test]
10721 fn test_encode_mul_thumb2() {
10722 let encoder = ArmEncoder::new_thumb2();
10723 let op = ArmOp::Mul {
10724 rd: Reg::R0,
10725 rn: Reg::R1,
10726 rm: Reg::R2,
10727 };
10728
10729 let code = encoder.encode(&op).unwrap();
10730 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10731 }
10732
10733 #[test]
10734 fn test_encode_and_thumb2() {
10735 let encoder = ArmEncoder::new_thumb2();
10736 let op = ArmOp::And {
10737 rd: Reg::R0,
10738 rn: Reg::R1,
10739 op2: Operand2::Reg(Reg::R2),
10740 };
10741
10742 let code = encoder.encode(&op).unwrap();
10743 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10744 }
10745
10746 #[test]
10747 fn test_encode_lsl_thumb2_low_regs() {
10748 let encoder = ArmEncoder::new_thumb2();
10749 let op = ArmOp::Lsl {
10750 rd: Reg::R0,
10751 rn: Reg::R1,
10752 shift: 5,
10753 };
10754
10755 let code = encoder.encode(&op).unwrap();
10756 assert_eq!(code.len(), 2); // 16-bit for low registers
10757 }
10758
10759 #[test]
10760 fn test_encode_clz_thumb2() {
10761 let encoder = ArmEncoder::new_thumb2();
10762 let op = ArmOp::Clz {
10763 rd: Reg::R0,
10764 rm: Reg::R1,
10765 };
10766
10767 let code = encoder.encode(&op).unwrap();
10768 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10769 }
10770
10771 #[test]
10772 fn test_encode_bx_thumb2() {
10773 let encoder = ArmEncoder::new_thumb2();
10774 let op = ArmOp::Bx { rm: Reg::LR };
10775
10776 let code = encoder.encode(&op).unwrap();
10777 assert_eq!(code.len(), 2); // 16-bit instruction
10778
10779 // BX LR: 0x4770
10780 assert_eq!(code, vec![0x70, 0x47]);
10781 }
10782
10783 // ========================================================================
10784 // f32 pseudo-op encoding tests
10785 // ========================================================================
10786
10787 #[test]
10788 fn test_encode_f32_abs_arm32() {
10789 let encoder = ArmEncoder::new_arm32();
10790 let op = ArmOp::F32Abs {
10791 sd: VfpReg::S0,
10792 sm: VfpReg::S2,
10793 };
10794 let code = encoder.encode(&op).unwrap();
10795 assert_eq!(code.len(), 4); // Single VFP instruction
10796 }
10797
10798 #[test]
10799 fn test_encode_f32_neg_arm32() {
10800 let encoder = ArmEncoder::new_arm32();
10801 let op = ArmOp::F32Neg {
10802 sd: VfpReg::S0,
10803 sm: VfpReg::S2,
10804 };
10805 let code = encoder.encode(&op).unwrap();
10806 assert_eq!(code.len(), 4);
10807 }
10808
10809 #[test]
10810 fn test_encode_f32_sqrt_arm32() {
10811 let encoder = ArmEncoder::new_arm32();
10812 let op = ArmOp::F32Sqrt {
10813 sd: VfpReg::S0,
10814 sm: VfpReg::S2,
10815 };
10816 let code = encoder.encode(&op).unwrap();
10817 assert_eq!(code.len(), 4);
10818 }
10819
10820 #[test]
10821 fn test_encode_f32_ceil_arm32() {
10822 let encoder = ArmEncoder::new_arm32();
10823 let op = ArmOp::F32Ceil {
10824 sd: VfpReg::S0,
10825 sm: VfpReg::S2,
10826 };
10827 let code = encoder.encode(&op).unwrap();
10828 // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10829 assert_eq!(code.len(), 36);
10830 }
10831
10832 #[test]
10833 fn test_encode_f32_floor_thumb2() {
10834 let encoder = ArmEncoder::new_thumb2();
10835 let op = ArmOp::F32Floor {
10836 sd: VfpReg::S0,
10837 sm: VfpReg::S2,
10838 };
10839 let code = encoder.encode(&op).unwrap();
10840 // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10841 assert_eq!(code.len(), 36);
10842 }
10843
10844 #[test]
10845 fn test_encode_f32_min_arm32() {
10846 let encoder = ArmEncoder::new_arm32();
10847 let op = ArmOp::F32Min {
10848 sd: VfpReg::S0,
10849 sn: VfpReg::S2,
10850 sm: VfpReg::S4,
10851 };
10852 let code = encoder.encode(&op).unwrap();
10853 assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10854 }
10855
10856 #[test]
10857 fn test_encode_f32_max_thumb2() {
10858 let encoder = ArmEncoder::new_thumb2();
10859 let op = ArmOp::F32Max {
10860 sd: VfpReg::S0,
10861 sn: VfpReg::S2,
10862 sm: VfpReg::S4,
10863 };
10864 let code = encoder.encode(&op).unwrap();
10865 // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10866 assert_eq!(code.len(), 18);
10867 }
10868
10869 #[test]
10870 fn test_encode_f32_copysign_arm32() {
10871 let encoder = ArmEncoder::new_arm32();
10872 let op = ArmOp::F32Copysign {
10873 sd: VfpReg::S0,
10874 sn: VfpReg::S2,
10875 sm: VfpReg::S4,
10876 };
10877 let code = encoder.encode(&op).unwrap();
10878 // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10879 assert_eq!(code.len(), 24);
10880 }
10881
10882 // ========================================================================
10883 // f64 encoding tests
10884 // ========================================================================
10885
10886 #[test]
10887 fn test_encode_f64_add_arm32() {
10888 let encoder = ArmEncoder::new_arm32();
10889 let op = ArmOp::F64Add {
10890 dd: VfpReg::D0,
10891 dn: VfpReg::D1,
10892 dm: VfpReg::D2,
10893 };
10894 let code = encoder.encode(&op).unwrap();
10895 assert_eq!(code.len(), 4);
10896 // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10897 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10898 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10899 }
10900
10901 #[test]
10902 fn test_encode_f64_sub_thumb2() {
10903 let encoder = ArmEncoder::new_thumb2();
10904 let op = ArmOp::F64Sub {
10905 dd: VfpReg::D0,
10906 dn: VfpReg::D1,
10907 dm: VfpReg::D2,
10908 };
10909 let code = encoder.encode(&op).unwrap();
10910 assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10911 }
10912
10913 #[test]
10914 fn test_encode_f64_mul_arm32() {
10915 let encoder = ArmEncoder::new_arm32();
10916 let op = ArmOp::F64Mul {
10917 dd: VfpReg::D0,
10918 dn: VfpReg::D1,
10919 dm: VfpReg::D2,
10920 };
10921 let code = encoder.encode(&op).unwrap();
10922 assert_eq!(code.len(), 4);
10923 }
10924
10925 #[test]
10926 fn test_encode_f64_div_arm32() {
10927 let encoder = ArmEncoder::new_arm32();
10928 let op = ArmOp::F64Div {
10929 dd: VfpReg::D0,
10930 dn: VfpReg::D1,
10931 dm: VfpReg::D2,
10932 };
10933 let code = encoder.encode(&op).unwrap();
10934 assert_eq!(code.len(), 4);
10935 }
10936
10937 #[test]
10938 fn test_encode_f64_abs_arm32() {
10939 let encoder = ArmEncoder::new_arm32();
10940 let op = ArmOp::F64Abs {
10941 dd: VfpReg::D0,
10942 dm: VfpReg::D2,
10943 };
10944 let code = encoder.encode(&op).unwrap();
10945 assert_eq!(code.len(), 4);
10946 }
10947
10948 #[test]
10949 fn test_encode_f64_neg_arm32() {
10950 let encoder = ArmEncoder::new_arm32();
10951 let op = ArmOp::F64Neg {
10952 dd: VfpReg::D0,
10953 dm: VfpReg::D2,
10954 };
10955 let code = encoder.encode(&op).unwrap();
10956 assert_eq!(code.len(), 4);
10957 }
10958
10959 #[test]
10960 fn test_encode_f64_sqrt_arm32() {
10961 let encoder = ArmEncoder::new_arm32();
10962 let op = ArmOp::F64Sqrt {
10963 dd: VfpReg::D0,
10964 dm: VfpReg::D2,
10965 };
10966 let code = encoder.encode(&op).unwrap();
10967 assert_eq!(code.len(), 4);
10968 }
10969
10970 #[test]
10971 fn test_encode_f64_load_arm32() {
10972 let encoder = ArmEncoder::new_arm32();
10973 let op = ArmOp::F64Load {
10974 dd: VfpReg::D0,
10975 addr: MemAddr::imm(Reg::R0, 8),
10976 };
10977 let code = encoder.encode(&op).unwrap();
10978 assert_eq!(code.len(), 4);
10979 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10980 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
10981 assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
10982 }
10983
10984 #[test]
10985 fn test_encode_f64_store_thumb2() {
10986 let encoder = ArmEncoder::new_thumb2();
10987 let op = ArmOp::F64Store {
10988 dd: VfpReg::D0,
10989 addr: MemAddr::imm(Reg::SP, 0),
10990 };
10991 let code = encoder.encode(&op).unwrap();
10992 assert_eq!(code.len(), 4);
10993 }
10994
10995 #[test]
10996 fn test_encode_f64_compare_arm32() {
10997 let encoder = ArmEncoder::new_arm32();
10998 let op = ArmOp::F64Eq {
10999 rd: Reg::R0,
11000 dn: VfpReg::D0,
11001 dm: VfpReg::D1,
11002 };
11003 let code = encoder.encode(&op).unwrap();
11004 assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
11005 }
11006
11007 #[test]
11008 fn test_encode_f64_compare_thumb2() {
11009 let encoder = ArmEncoder::new_thumb2();
11010 let op = ArmOp::F64Lt {
11011 rd: Reg::R0,
11012 dn: VfpReg::D0,
11013 dm: VfpReg::D1,
11014 };
11015 let code = encoder.encode(&op).unwrap();
11016 // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
11017 assert_eq!(code.len(), 14);
11018 }
11019
11020 #[test]
11021 fn test_encode_f64_const_arm32() {
11022 let encoder = ArmEncoder::new_arm32();
11023 let op = ArmOp::F64Const {
11024 dd: VfpReg::D0,
11025 value: 3.125,
11026 };
11027 let code = encoder.encode(&op).unwrap();
11028 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11029 assert_eq!(code.len(), 20);
11030 }
11031
11032 #[test]
11033 fn test_encode_f64_const_thumb2() {
11034 let encoder = ArmEncoder::new_thumb2();
11035 let op = ArmOp::F64Const {
11036 dd: VfpReg::D0,
11037 value: 2.5,
11038 };
11039 let code = encoder.encode(&op).unwrap();
11040 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
11041 assert_eq!(code.len(), 20);
11042 }
11043
11044 #[test]
11045 fn test_encode_f64_convert_i32s_arm32() {
11046 let encoder = ArmEncoder::new_arm32();
11047 let op = ArmOp::F64ConvertI32S {
11048 dd: VfpReg::D0,
11049 rm: Reg::R0,
11050 };
11051 let code = encoder.encode(&op).unwrap();
11052 // VMOV(4) + VCVT(4) = 8
11053 assert_eq!(code.len(), 8);
11054 }
11055
11056 #[test]
11057 fn test_encode_f64_promote_f32_arm32() {
11058 let encoder = ArmEncoder::new_arm32();
11059 let op = ArmOp::F64PromoteF32 {
11060 dd: VfpReg::D0,
11061 sm: VfpReg::S0,
11062 };
11063 let code = encoder.encode(&op).unwrap();
11064 assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
11065 }
11066
11067 #[test]
11068 fn test_encode_f64_promote_f32_thumb2() {
11069 let encoder = ArmEncoder::new_thumb2();
11070 let op = ArmOp::F64PromoteF32 {
11071 dd: VfpReg::D0,
11072 sm: VfpReg::S0,
11073 };
11074 let code = encoder.encode(&op).unwrap();
11075 assert_eq!(code.len(), 4);
11076 }
11077
11078 #[test]
11079 fn test_encode_i32_trunc_f64s_arm32() {
11080 let encoder = ArmEncoder::new_arm32();
11081 let op = ArmOp::I32TruncF64S {
11082 rd: Reg::R0,
11083 dm: VfpReg::D0,
11084 };
11085 let code = encoder.encode(&op).unwrap();
11086 // VCVT(4) + VMOV(4) = 8
11087 assert_eq!(code.len(), 8);
11088 }
11089
11090 #[test]
11091 fn test_encode_f64_reinterpret_i64_arm32() {
11092 let encoder = ArmEncoder::new_arm32();
11093 let op = ArmOp::F64ReinterpretI64 {
11094 dd: VfpReg::D0,
11095 rmlo: Reg::R0,
11096 rmhi: Reg::R1,
11097 };
11098 let code = encoder.encode(&op).unwrap();
11099 assert_eq!(code.len(), 4); // Single VMOV instruction
11100 }
11101
11102 #[test]
11103 fn test_encode_i64_reinterpret_f64_thumb2() {
11104 let encoder = ArmEncoder::new_thumb2();
11105 let op = ArmOp::I64ReinterpretF64 {
11106 rdlo: Reg::R0,
11107 rdhi: Reg::R1,
11108 dm: VfpReg::D0,
11109 };
11110 let code = encoder.encode(&op).unwrap();
11111 assert_eq!(code.len(), 4);
11112 }
11113
11114 #[test]
11115 fn test_encode_f64_trunc_thumb2() {
11116 let encoder = ArmEncoder::new_thumb2();
11117 let op = ArmOp::F64Trunc {
11118 dd: VfpReg::D0,
11119 dm: VfpReg::D1,
11120 };
11121 let code = encoder.encode(&op).unwrap();
11122 // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
11123 // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
11124 assert_eq!(code.len(), 4);
11125 assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
11126 }
11127
11128 /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
11129 /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
11130 /// clobbers ONLY its destination (+R12/flags where noted) — the previous
11131 /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
11132 /// min/max/rounding semantics were wrong (ordered IT select returned the
11133 /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
11134 #[test]
11135 fn test_369_f64_tail_thumb2_encodings_match_clang() {
11136 let enc = ArmEncoder::new_thumb2();
11137 // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
11138 for (op, want) in [
11139 (
11140 ArmOp::F64Nearest {
11141 dd: VfpReg::D1,
11142 dm: VfpReg::D2,
11143 },
11144 vec![0xb9, 0xfe, 0x42, 0x1b],
11145 ),
11146 (
11147 ArmOp::F64Ceil {
11148 dd: VfpReg::D1,
11149 dm: VfpReg::D2,
11150 },
11151 vec![0xba, 0xfe, 0x42, 0x1b],
11152 ),
11153 (
11154 ArmOp::F64Floor {
11155 dd: VfpReg::D1,
11156 dm: VfpReg::D2,
11157 },
11158 vec![0xbb, 0xfe, 0x42, 0x1b],
11159 ),
11160 ] {
11161 assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
11162 }
11163 // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
11164 let min = enc
11165 .encode(&ArmOp::F64Min {
11166 dd: VfpReg::D0,
11167 dn: VfpReg::D1,
11168 dm: VfpReg::D2,
11169 })
11170 .unwrap();
11171 assert_eq!(
11172 min,
11173 vec![
11174 0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
11175 0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
11176 0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
11177 0x68, 0xbf, // it vs
11178 0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11179 ]
11180 );
11181 // vmaxnm variant flips only bit6 of the VMINNM word.
11182 let max = enc
11183 .encode(&ArmOp::F64Max {
11184 dd: VfpReg::D0,
11185 dn: VfpReg::D1,
11186 dm: VfpReg::D2,
11187 })
11188 .unwrap();
11189 assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11190 // Destination aliasing a source must ERR (the NaN fix-up would read
11191 // a clobbered operand), never encode.
11192 assert!(
11193 enc.encode(&ArmOp::F64Min {
11194 dd: VfpReg::D1,
11195 dn: VfpReg::D1,
11196 dm: VfpReg::D2,
11197 })
11198 .is_err()
11199 );
11200 // copysign d0,(mag)d1,(sign)d2:
11201 // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11202 let cs = enc
11203 .encode(&ArmOp::F64Copysign {
11204 dd: VfpReg::D0,
11205 dn: VfpReg::D1,
11206 dm: VfpReg::D2,
11207 })
11208 .unwrap();
11209 assert_eq!(
11210 cs,
11211 vec![
11212 0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11213 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11214 0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11215 0x48, 0xbf, // it mi
11216 0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11217 ]
11218 );
11219 // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11220 // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11221 let cs32 = enc
11222 .encode(&ArmOp::F32Copysign {
11223 sd: VfpReg::S0,
11224 sn: VfpReg::S1,
11225 sm: VfpReg::S2,
11226 })
11227 .unwrap();
11228 assert_eq!(
11229 cs32,
11230 vec![
11231 0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11232 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11233 0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11234 0x48, 0xbf, // it mi
11235 0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11236 ]
11237 );
11238 // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11239 // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11240 // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11241 let conv_s = enc
11242 .encode(&ArmOp::F64ConvertI32S {
11243 dd: VfpReg::D0,
11244 rm: Reg::R3,
11245 })
11246 .unwrap();
11247 assert_eq!(
11248 conv_s,
11249 vec![
11250 0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11251 0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11252 ]
11253 );
11254 let conv_u = enc
11255 .encode(&ArmOp::F64ConvertI32U {
11256 dd: VfpReg::D0,
11257 rm: Reg::R3,
11258 })
11259 .unwrap();
11260 assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11261 // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11262 // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11263 let trunc_s = enc
11264 .encode(&ArmOp::I32TruncF64S {
11265 rd: Reg::R3,
11266 dm: VfpReg::D1,
11267 })
11268 .unwrap();
11269 assert_eq!(
11270 trunc_s,
11271 vec![
11272 0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11273 0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11274 ]
11275 );
11276 let trunc_u = enc
11277 .encode(&ArmOp::I32TruncF64U {
11278 rd: Reg::R3,
11279 dm: VfpReg::D1,
11280 })
11281 .unwrap();
11282 assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11283 // f32.demote_f64: vcvt.f32.f64 s1, d2
11284 let demote = enc
11285 .encode(&ArmOp::F32DemoteF64 {
11286 sd: VfpReg::S1,
11287 dm: VfpReg::D2,
11288 })
11289 .unwrap();
11290 assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11291 }
11292
11293 #[test]
11294 fn test_encode_f64_min_arm32() {
11295 let encoder = ArmEncoder::new_arm32();
11296 let op = ArmOp::F64Min {
11297 dd: VfpReg::D0,
11298 dn: VfpReg::D1,
11299 dm: VfpReg::D2,
11300 };
11301 let code = encoder.encode(&op).unwrap();
11302 // VMOV + VCMP + VMRS + conditional VMOV = 16
11303 assert_eq!(code.len(), 16);
11304 }
11305
11306 #[test]
11307 fn test_f64_cp11_encoding() {
11308 // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11309 let encoder = ArmEncoder::new_arm32();
11310
11311 // F64Add
11312 let code = encoder
11313 .encode(&ArmOp::F64Add {
11314 dd: VfpReg::D0,
11315 dn: VfpReg::D0,
11316 dm: VfpReg::D0,
11317 })
11318 .unwrap();
11319 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11320 assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11321
11322 // F32Add for comparison
11323 let code = encoder
11324 .encode(&ArmOp::F32Add {
11325 sd: VfpReg::S0,
11326 sn: VfpReg::S0,
11327 sm: VfpReg::S0,
11328 })
11329 .unwrap();
11330 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11331 assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11332 }
11333
11334 #[test]
11335 fn test_dreg_encoding_higher_registers() {
11336 let encoder = ArmEncoder::new_arm32();
11337
11338 // Test with D15 (highest register)
11339 let op = ArmOp::F64Add {
11340 dd: VfpReg::D15,
11341 dn: VfpReg::D14,
11342 dm: VfpReg::D13,
11343 };
11344 let code = encoder.encode(&op).unwrap();
11345 assert_eq!(code.len(), 4);
11346
11347 // Verify the register encoding worked (instruction is valid)
11348 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11349 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11350 }
11351
11352 // ========================================================================
11353 // Control flow encoding tests
11354 // ========================================================================
11355
11356 #[test]
11357 fn test_encode_label_emits_no_bytes() {
11358 let encoder = ArmEncoder::new_thumb2();
11359 let op = ArmOp::Label {
11360 name: ".Lblock_end_0".to_string(),
11361 };
11362 let code = encoder.encode(&op).unwrap();
11363 assert!(code.is_empty(), "Label should emit zero bytes");
11364
11365 let encoder32 = ArmEncoder::new_arm32();
11366 let code32 = encoder32.encode(&op).unwrap();
11367 assert!(
11368 code32.is_empty(),
11369 "Label should emit zero bytes in ARM32 too"
11370 );
11371 }
11372
11373 #[test]
11374 fn test_encode_bcc_eq_thumb2() {
11375 use synth_synthesis::Condition;
11376 let encoder = ArmEncoder::new_thumb2();
11377 let op = ArmOp::Bcc {
11378 cond: Condition::EQ,
11379 label: "target".to_string(),
11380 };
11381 let code = encoder.encode(&op).unwrap();
11382 assert_eq!(code.len(), 2); // 16-bit conditional branch
11383
11384 // BEQ with offset 0: 0xD000 in little-endian
11385 assert_eq!(code, vec![0x00, 0xD0]);
11386 }
11387
11388 #[test]
11389 fn test_encode_bcc_ne_thumb2() {
11390 use synth_synthesis::Condition;
11391 let encoder = ArmEncoder::new_thumb2();
11392 let op = ArmOp::Bcc {
11393 cond: Condition::NE,
11394 label: "target".to_string(),
11395 };
11396 let code = encoder.encode(&op).unwrap();
11397 assert_eq!(code.len(), 2);
11398
11399 // BNE with offset 0: 0xD100 in little-endian
11400 assert_eq!(code, vec![0x00, 0xD1]);
11401 }
11402
11403 #[test]
11404 fn test_encode_bcc_arm32() {
11405 use synth_synthesis::Condition;
11406 let encoder = ArmEncoder::new_arm32();
11407 let op = ArmOp::Bcc {
11408 cond: Condition::EQ,
11409 label: "target".to_string(),
11410 };
11411 let code = encoder.encode(&op).unwrap();
11412 assert_eq!(code.len(), 4); // 32-bit ARM instruction
11413
11414 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11415 // BEQ: cond=0x0, opcode=0xA, offset=0
11416 assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11417 assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11418 }
11419
11420 #[test]
11421 fn test_encode_udf_thumb2() {
11422 let encoder = ArmEncoder::new_thumb2();
11423 let op = ArmOp::Udf { imm: 0 };
11424 let code = encoder.encode(&op).unwrap();
11425 assert_eq!(code.len(), 2); // 16-bit
11426
11427 // UDF #0: 0xDE00 in little-endian
11428 assert_eq!(code, vec![0x00, 0xDE]);
11429 }
11430
11431 /// #610: the i64 rot/div/rem expansions must land the result in the
11432 /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11433 /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11434 /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11435 /// the div/rem expansions ignored their register fields outright.
11436 #[test]
11437 fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11438 let encoder = ArmEncoder::new_thumb2();
11439 for op in [
11440 ArmOp::I64Rotl {
11441 rdlo: Reg::R4,
11442 rdhi: Reg::R5,
11443 rnlo: Reg::R0,
11444 rnhi: Reg::R1,
11445 shift: Reg::R2,
11446 },
11447 ArmOp::I64Rotr {
11448 rdlo: Reg::R4,
11449 rdhi: Reg::R5,
11450 rnlo: Reg::R0,
11451 rnhi: Reg::R1,
11452 shift: Reg::R2,
11453 },
11454 ] {
11455 let code = encoder.encode(&op).unwrap();
11456 assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11457 // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11458 // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11459 let tail: Vec<u16> = code[code.len() - 12..]
11460 .chunks(2)
11461 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11462 .collect();
11463 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11464 }
11465 }
11466
11467 /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11468 /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11469 #[test]
11470 fn test_610_i64_div_rem_expansion_guard_and_rd() {
11471 let encoder = ArmEncoder::new_thumb2();
11472 let mk = |which: u8| {
11473 let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11474 (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11475 match which {
11476 0 => ArmOp::I64DivU {
11477 rdlo,
11478 rdhi,
11479 rnlo,
11480 rnhi,
11481 rmlo,
11482 rmhi,
11483 elide_zero_guard: false,
11484 },
11485 1 => ArmOp::I64RemU {
11486 rdlo,
11487 rdhi,
11488 rnlo,
11489 rnhi,
11490 rmlo,
11491 rmhi,
11492 elide_zero_guard: false,
11493 },
11494 2 => ArmOp::I64DivS {
11495 rdlo,
11496 rdhi,
11497 rnlo,
11498 rnhi,
11499 rmlo,
11500 rmhi,
11501 elide_zero_guard: false,
11502 elide_overflow_guard: false,
11503 },
11504 _ => ArmOp::I64RemS {
11505 rdlo,
11506 rdhi,
11507 rnlo,
11508 rnhi,
11509 rmlo,
11510 rmhi,
11511 elide_zero_guard: false,
11512 },
11513 }
11514 };
11515 for which in 0..4u8 {
11516 let code = encoder.encode(&mk(which)).unwrap();
11517 // Zero-divisor trap guard right after the 26-byte marshal prologue.
11518 let guard: Vec<u16> = code[26..34]
11519 .chunks(2)
11520 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11521 .collect();
11522 assert_eq!(
11523 guard,
11524 vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11525 "ORRS R12,R2,R3; BNE +0; UDF #0"
11526 );
11527 // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11528 let tail: Vec<u16> = code[code.len() - 12..]
11529 .chunks(2)
11530 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11531 .collect();
11532 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11533 }
11534 }
11535
11536 /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11537 /// registers (drop the saved caller word) instead of popping over them.
11538 #[test]
11539 fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11540 let encoder = ArmEncoder::new_thumb2();
11541 let code = encoder
11542 .encode(&ArmOp::I64DivU {
11543 rdlo: Reg::R0,
11544 rdhi: Reg::R1,
11545 rnlo: Reg::R0,
11546 rnhi: Reg::R1,
11547 rmlo: Reg::R2,
11548 rmhi: Reg::R3,
11549 elide_zero_guard: false,
11550 })
11551 .unwrap();
11552 let tail: Vec<u16> = code[code.len() - 12..]
11553 .chunks(2)
11554 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11555 .collect();
11556 // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11557 // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11558 assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11559 }
11560
11561 /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11562 /// materialized by two MOVs in either order — must be a loud Err, never
11563 /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11564 #[test]
11565 fn test_610_i64_swapped_rd_pair_rejected() {
11566 let encoder = ArmEncoder::new_thumb2();
11567 let result = encoder.encode(&ArmOp::I64RemU {
11568 rdlo: Reg::R1,
11569 rdhi: Reg::R0,
11570 rnlo: Reg::R2,
11571 rnhi: Reg::R3,
11572 rmlo: Reg::R4,
11573 rmhi: Reg::R5,
11574 elide_zero_guard: false,
11575 });
11576 assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11577 }
11578
11579 /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11580 /// must not clobber the result. Pre-fix the total was materialized with
11581 /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11582 /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11583 /// carried across the restore in R12 (never allocatable, never restored)
11584 /// and moved into rd only after the pop — structurally rd-independent.
11585 #[test]
11586 fn test_632_i64_popcnt_result_survives_scratch_restore() {
11587 let encoder = ArmEncoder::new_thumb2();
11588 // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11589 for rd in [
11590 Reg::R0,
11591 Reg::R2,
11592 Reg::R3,
11593 Reg::R4,
11594 Reg::R5,
11595 Reg::R6,
11596 Reg::R8,
11597 ] {
11598 let code = encoder
11599 .encode(&ArmOp::I64Popcnt {
11600 rd,
11601 rnlo: Reg::R6,
11602 rnhi: Reg::R7,
11603 })
11604 .unwrap();
11605 assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11606 let hw: Vec<u16> = code
11607 .chunks(2)
11608 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11609 .collect();
11610 let pop = hw
11611 .iter()
11612 .position(|&h| h == 0xBC38)
11613 .expect("POP {R3,R4,R5} present");
11614 // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11615 // in R12, which the POP cannot touch).
11616 assert_eq!(
11617 &hw[pop - 2..pop],
11618 &[0xEB04, 0x0C05],
11619 "total must be carried in R12 across the restore"
11620 );
11621 // Immediately after the POP: MOV rd, R12.
11622 let rd_bits = match rd {
11623 Reg::R8 => 8u16,
11624 Reg::R6 => 6,
11625 Reg::R5 => 5,
11626 Reg::R4 => 4,
11627 Reg::R3 => 3,
11628 Reg::R2 => 2,
11629 _ => 0,
11630 };
11631 let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11632 assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11633 // No write into rd between the PUSH and the POP (the old
11634 // pre-restore ADDS is gone).
11635 assert!(
11636 !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11637 "no ADDS rd, R4, R5 before the restore pop"
11638 );
11639 }
11640 }
11641
11642 /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11643 /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11644 /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11645 #[test]
11646 fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11647 let encoder = ArmEncoder::new_thumb2();
11648 let code = encoder
11649 .encode(&ArmOp::I64Popcnt {
11650 rd: Reg::R0,
11651 rnlo: Reg::R3,
11652 rnhi: Reg::R4,
11653 })
11654 .unwrap();
11655 let hw: Vec<u16> = code
11656 .chunks(2)
11657 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11658 .collect();
11659 // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11660 // write to R4); MOV R4, R12.
11661 assert_eq!(hw[0], 0xB438);
11662 assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11663 assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11664 assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11665 }
11666
11667 /// #632: A32 twin — same structural fix on the ARM-mode path
11668 /// (`--target cortex-r5`): total carried in R12 across the restore.
11669 #[test]
11670 fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11671 let encoder = ArmEncoder::new_arm32();
11672 for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11673 let code = encoder
11674 .encode(&ArmOp::I64Popcnt {
11675 rd,
11676 rnlo: Reg::R6,
11677 rnhi: Reg::R7,
11678 })
11679 .unwrap();
11680 let words: Vec<u32> = code
11681 .chunks(4)
11682 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11683 .collect();
11684 let pop = words
11685 .iter()
11686 .position(|&w| w == 0xE8BD_0038)
11687 .expect("POP {R3,R4,R5} present");
11688 assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11689 let rd_bits = match rd {
11690 Reg::R8 => 8u32,
11691 Reg::R5 => 5,
11692 Reg::R4 => 4,
11693 Reg::R3 => 3,
11694 _ => 0,
11695 };
11696 assert_eq!(
11697 words[pop + 1],
11698 0xE1A0_0000 | (rd_bits << 12) | 12,
11699 "MOV rd, R12 after the restore"
11700 );
11701 }
11702 }
11703
11704 /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11705 /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11706 /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11707 #[test]
11708 fn test_633_i64_divs_overflow_guard_emitted() {
11709 let encoder = ArmEncoder::new_thumb2();
11710 let code = encoder
11711 .encode(&ArmOp::I64DivS {
11712 rdlo: Reg::R4,
11713 rdhi: Reg::R5,
11714 rnlo: Reg::R0,
11715 rnhi: Reg::R1,
11716 rmlo: Reg::R2,
11717 rmhi: Reg::R3,
11718 elide_zero_guard: false,
11719 elide_overflow_guard: false,
11720 })
11721 .unwrap();
11722 // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11723 let guard: Vec<u16> = code[34..56]
11724 .chunks(2)
11725 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11726 .collect();
11727 assert_eq!(
11728 guard,
11729 vec![
11730 0xEA02, 0x0C03, // AND.W R12, R2, R3
11731 0xF11C, 0x0F01, // CMN.W R12, #1
11732 0xD105, // BNE .no_trap
11733 0x2800, // CMP R0, #0
11734 0xD103, // BNE .no_trap
11735 0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11736 0xD100, // BNE .no_trap
11737 0xDE00, // UDF #0 — signed-division overflow
11738 ],
11739 "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11740 );
11741 }
11742
11743 /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11744 /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11745 /// UDF (the zero-divisor trap) in the whole expansion.
11746 #[test]
11747 fn test_633_i64_rems_has_no_overflow_guard() {
11748 let encoder = ArmEncoder::new_thumb2();
11749 for (is_rem_s, op) in [
11750 (
11751 true,
11752 ArmOp::I64RemS {
11753 rdlo: Reg::R4,
11754 rdhi: Reg::R5,
11755 rnlo: Reg::R0,
11756 rnhi: Reg::R1,
11757 rmlo: Reg::R2,
11758 rmhi: Reg::R3,
11759 elide_zero_guard: false,
11760 },
11761 ),
11762 (
11763 false,
11764 ArmOp::I64DivS {
11765 rdlo: Reg::R4,
11766 rdhi: Reg::R5,
11767 rnlo: Reg::R0,
11768 rnhi: Reg::R1,
11769 rmlo: Reg::R2,
11770 rmhi: Reg::R3,
11771 elide_zero_guard: false,
11772 elide_overflow_guard: false,
11773 },
11774 ),
11775 ] {
11776 let code = encoder.encode(&op).unwrap();
11777 let udfs = code
11778 .chunks(2)
11779 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11780 .count();
11781 let want = if is_rem_s { 1 } else { 2 };
11782 assert_eq!(
11783 udfs, want,
11784 "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11785 );
11786 }
11787 }
11788
11789 /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11790 /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11791 /// rest of the expansion is byte-identical (splice check).
11792 #[test]
11793 fn test_494_i64_zero_guard_elision_is_exact_splice() {
11794 let encoder = ArmEncoder::new_thumb2();
11795 let mk = |elide_zero_guard: bool| {
11796 encoder
11797 .encode(&ArmOp::I64DivU {
11798 rdlo: Reg::R4,
11799 rdhi: Reg::R5,
11800 rnlo: Reg::R0,
11801 rnhi: Reg::R1,
11802 rmlo: Reg::R2,
11803 rmhi: Reg::R3,
11804 elide_zero_guard,
11805 })
11806 .unwrap()
11807 };
11808 let full = mk(false);
11809 let elided = mk(true);
11810 assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11811 // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11812 assert_eq!(&full[..26], &elided[..26]);
11813 assert_eq!(
11814 &full[26..34],
11815 &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11816 "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11817 );
11818 assert_eq!(&full[34..], &elided[26..]);
11819 }
11820
11821 /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11822 /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11823 /// OVERFLOW guard is a separate obligation and must survive
11824 /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11825 #[test]
11826 fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11827 let encoder = ArmEncoder::new_thumb2();
11828 let mk = |zero: bool, ovf: bool| {
11829 encoder
11830 .encode(&ArmOp::I64DivS {
11831 rdlo: Reg::R4,
11832 rdhi: Reg::R5,
11833 rnlo: Reg::R0,
11834 rnhi: Reg::R1,
11835 rmlo: Reg::R2,
11836 rmhi: Reg::R3,
11837 elide_zero_guard: zero,
11838 elide_overflow_guard: ovf,
11839 })
11840 .unwrap()
11841 };
11842 let udf_count = |code: &[u8]| {
11843 code.chunks(2)
11844 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11845 .count()
11846 };
11847 let full = mk(false, false);
11848 let zero_only = mk(true, false);
11849 let both = mk(true, true);
11850 assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11851 assert_eq!(
11852 udf_count(&zero_only),
11853 1,
11854 "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11855 guard must be retained"
11856 );
11857 // The retained guard is the 22-byte overflow sequence, now right
11858 // after the 26-byte marshal prologue.
11859 let guard: Vec<u16> = zero_only[26..48]
11860 .chunks(2)
11861 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11862 .collect();
11863 assert_eq!(
11864 guard,
11865 vec![
11866 0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11867 0xDE00,
11868 ],
11869 "the surviving guard is the INT64_MIN/-1 overflow trap"
11870 );
11871 assert_eq!(full.len(), zero_only.len() + 8);
11872 assert_eq!(zero_only.len(), both.len() + 22);
11873 assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11874 }
11875
11876 /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11877 /// and the A32 overflow guard survives a zero-only elision.
11878 #[test]
11879 fn test_494_a32_i64_guard_elision() {
11880 let encoder = ArmEncoder::new_arm32();
11881 let mk = |zero: bool, ovf: bool| {
11882 encoder
11883 .encode(&ArmOp::I64DivS {
11884 rdlo: Reg::R4,
11885 rdhi: Reg::R5,
11886 rnlo: Reg::R0,
11887 rnhi: Reg::R1,
11888 rmlo: Reg::R2,
11889 rmhi: Reg::R3,
11890 elide_zero_guard: zero,
11891 elide_overflow_guard: ovf,
11892 })
11893 .unwrap()
11894 };
11895 let full = mk(false, false);
11896 let zero_only = mk(true, false);
11897 let both = mk(true, true);
11898 // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11899 assert_eq!(full.len(), zero_only.len() + 12);
11900 assert_eq!(zero_only.len(), both.len() + 24);
11901 let udf_count = |code: &[u8]| {
11902 code.chunks(4)
11903 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11904 .count()
11905 };
11906 assert_eq!(udf_count(&full), 2);
11907 assert_eq!(
11908 udf_count(&zero_only),
11909 1,
11910 "A32: overflow guard retained under zero-only elision"
11911 );
11912 assert_eq!(udf_count(&both), 0);
11913 }
11914
11915 /// #633: A32 twin — the conditional-execution overflow guard on the
11916 /// ARM-mode I64DivS, and its absence from I64RemS.
11917 #[test]
11918 fn test_633_a32_i64_divs_overflow_guard() {
11919 let encoder = ArmEncoder::new_arm32();
11920 let mk_divs = ArmOp::I64DivS {
11921 rdlo: Reg::R4,
11922 rdhi: Reg::R5,
11923 rnlo: Reg::R0,
11924 rnhi: Reg::R1,
11925 rmlo: Reg::R2,
11926 rmhi: Reg::R3,
11927 elide_zero_guard: false,
11928 elide_overflow_guard: false,
11929 };
11930 let code = encoder.encode(&mk_divs).unwrap();
11931 let words: Vec<u32> = code
11932 .chunks(4)
11933 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11934 .collect();
11935 let guard = [
11936 0xE002_C003u32, // AND R12, R2, R3
11937 0xE37C_0001, // CMN R12, #1
11938 0x0350_0000, // CMPEQ R0, #0
11939 0x0351_0102, // CMPEQ R1, #0x80000000
11940 0x1A00_0000, // BNE +1 insn
11941 0xE7F0_00F0, // UDF #0
11942 ];
11943 assert!(
11944 words.windows(6).any(|w| w == guard),
11945 "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
11946 );
11947 let rems = encoder
11948 .encode(&ArmOp::I64RemS {
11949 rdlo: Reg::R4,
11950 rdhi: Reg::R5,
11951 rnlo: Reg::R0,
11952 rnhi: Reg::R1,
11953 rmlo: Reg::R2,
11954 rmhi: Reg::R3,
11955 elide_zero_guard: false,
11956 })
11957 .unwrap();
11958 let rems_udfs = rems
11959 .chunks(4)
11960 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11961 .count();
11962 assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
11963 }
11964
11965 #[test]
11966 fn test_encode_nop_thumb2() {
11967 let encoder = ArmEncoder::new_thumb2();
11968 let op = ArmOp::Nop;
11969 let code = encoder.encode(&op).unwrap();
11970 assert_eq!(code.len(), 2); // 16-bit
11971
11972 // NOP: 0xBF00 in little-endian
11973 assert_eq!(code, vec![0x00, 0xBF]);
11974 }
11975
11976 // =========================================================================
11977 // i64 Thumb-2 encoding tests
11978 // =========================================================================
11979
11980 #[test]
11981 fn test_encode_i64_add_thumb2() {
11982 let encoder = ArmEncoder::new_thumb2();
11983 let op = ArmOp::I64Add {
11984 rdlo: Reg::R0,
11985 rdhi: Reg::R1,
11986 rnlo: Reg::R0,
11987 rnhi: Reg::R1,
11988 rmlo: Reg::R2,
11989 rmhi: Reg::R3,
11990 };
11991 let code = encoder.encode(&op).unwrap();
11992 // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
11993 assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
11994 }
11995
11996 #[test]
11997 fn test_encode_i64_sub_thumb2() {
11998 let encoder = ArmEncoder::new_thumb2();
11999 let op = ArmOp::I64Sub {
12000 rdlo: Reg::R0,
12001 rdhi: Reg::R1,
12002 rnlo: Reg::R0,
12003 rnhi: Reg::R1,
12004 rmlo: Reg::R2,
12005 rmhi: Reg::R3,
12006 };
12007 let code = encoder.encode(&op).unwrap();
12008 // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
12009 assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
12010 }
12011
12012 #[test]
12013 fn test_encode_i64_and_thumb2() {
12014 let encoder = ArmEncoder::new_thumb2();
12015 let op = ArmOp::I64And {
12016 rdlo: Reg::R0,
12017 rdhi: Reg::R1,
12018 rnlo: Reg::R0,
12019 rnhi: Reg::R1,
12020 rmlo: Reg::R2,
12021 rmhi: Reg::R3,
12022 };
12023 let code = encoder.encode(&op).unwrap();
12024 // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
12025 assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
12026 }
12027
12028 #[test]
12029 fn test_encode_i64_or_thumb2() {
12030 let encoder = ArmEncoder::new_thumb2();
12031 let op = ArmOp::I64Or {
12032 rdlo: Reg::R0,
12033 rdhi: Reg::R1,
12034 rnlo: Reg::R0,
12035 rnhi: Reg::R1,
12036 rmlo: Reg::R2,
12037 rmhi: Reg::R3,
12038 };
12039 let code = encoder.encode(&op).unwrap();
12040 assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
12041 }
12042
12043 #[test]
12044 fn test_encode_i64_xor_thumb2() {
12045 let encoder = ArmEncoder::new_thumb2();
12046 let op = ArmOp::I64Xor {
12047 rdlo: Reg::R0,
12048 rdhi: Reg::R1,
12049 rnlo: Reg::R0,
12050 rnhi: Reg::R1,
12051 rmlo: Reg::R2,
12052 rmhi: Reg::R3,
12053 };
12054 let code = encoder.encode(&op).unwrap();
12055 assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
12056 }
12057
12058 #[test]
12059 fn test_encode_i64_const_small_thumb2() {
12060 let encoder = ArmEncoder::new_thumb2();
12061 // Small constant: only needs MOVW for each half
12062 let op = ArmOp::I64Const {
12063 rdlo: Reg::R0,
12064 rdhi: Reg::R1,
12065 value: 42,
12066 };
12067 let code = encoder.encode(&op).unwrap();
12068 // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
12069 assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
12070 }
12071
12072 #[test]
12073 fn test_encode_i64_const_large_thumb2() {
12074 let encoder = ArmEncoder::new_thumb2();
12075 // Large constant: needs MOVW+MOVT for each half
12076 let op = ArmOp::I64Const {
12077 rdlo: Reg::R0,
12078 rdhi: Reg::R1,
12079 value: 0x1234_5678_9ABC_DEF0_u64 as i64,
12080 };
12081 let code = encoder.encode(&op).unwrap();
12082 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12083 assert_eq!(
12084 code.len(),
12085 16,
12086 "I64Const with large value should be 16 bytes"
12087 );
12088 }
12089
12090 #[test]
12091 fn test_encode_i64_extend_i32_s_thumb2() {
12092 let encoder = ArmEncoder::new_thumb2();
12093 let op = ArmOp::I64ExtendI32S {
12094 rdlo: Reg::R0,
12095 rdhi: Reg::R1,
12096 rn: Reg::R0,
12097 };
12098 let code = encoder.encode(&op).unwrap();
12099 // When rdlo == rn, only ASR (4 bytes) is emitted
12100 assert_eq!(
12101 code.len(),
12102 4,
12103 "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
12104 );
12105 }
12106
12107 #[test]
12108 fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
12109 let encoder = ArmEncoder::new_thumb2();
12110 let op = ArmOp::I64ExtendI32S {
12111 rdlo: Reg::R0,
12112 rdhi: Reg::R1,
12113 rn: Reg::R2,
12114 };
12115 let code = encoder.encode(&op).unwrap();
12116 // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
12117 assert!(
12118 code.len() >= 6,
12119 "I64ExtendI32S (diff reg) should be at least 6 bytes"
12120 );
12121 }
12122
12123 #[test]
12124 fn test_encode_i64_extend_i32_u_thumb2() {
12125 let encoder = ArmEncoder::new_thumb2();
12126 let op = ArmOp::I64ExtendI32U {
12127 rdlo: Reg::R0,
12128 rdhi: Reg::R1,
12129 rn: Reg::R0,
12130 };
12131 let code = encoder.encode(&op).unwrap();
12132 // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
12133 assert_eq!(
12134 code.len(),
12135 2,
12136 "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
12137 );
12138 }
12139
12140 #[test]
12141 fn test_encode_i32_wrap_i64_nop_thumb2() {
12142 let encoder = ArmEncoder::new_thumb2();
12143 // When rd == rnlo, should be a NOP
12144 let op = ArmOp::I32WrapI64 {
12145 rd: Reg::R0,
12146 rnlo: Reg::R0,
12147 };
12148 let code = encoder.encode(&op).unwrap();
12149 assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
12150 assert_eq!(code, vec![0x00, 0xBF]); // NOP
12151 }
12152
12153 #[test]
12154 fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
12155 let encoder = ArmEncoder::new_thumb2();
12156 let op = ArmOp::I32WrapI64 {
12157 rd: Reg::R2,
12158 rnlo: Reg::R0,
12159 };
12160 let code = encoder.encode(&op).unwrap();
12161 // MOV R2, R0 (2 or 4 bytes)
12162 assert!(
12163 code.len() >= 2,
12164 "I32WrapI64 diff reg should emit at least 2 bytes"
12165 );
12166 }
12167
12168 #[test]
12169 fn test_encode_i64_eqz_thumb2() {
12170 let encoder = ArmEncoder::new_thumb2();
12171 let op = ArmOp::I64Eqz {
12172 rd: Reg::R0,
12173 rnlo: Reg::R0,
12174 rnhi: Reg::R1,
12175 };
12176 let code = encoder.encode(&op).unwrap();
12177 // Delegates to I64SetCondZ which is already encoded
12178 assert!(
12179 code.len() >= 6,
12180 "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12181 );
12182 }
12183
12184 #[test]
12185 fn test_encode_i64_eq_thumb2() {
12186 let encoder = ArmEncoder::new_thumb2();
12187 let op = ArmOp::I64Eq {
12188 rd: Reg::R0,
12189 rnlo: Reg::R0,
12190 rnhi: Reg::R1,
12191 rmlo: Reg::R2,
12192 rmhi: Reg::R3,
12193 };
12194 let code = encoder.encode(&op).unwrap();
12195 // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12196 assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12197 }
12198
12199 #[test]
12200 fn test_encode_i64_ldr_thumb2() {
12201 let encoder = ArmEncoder::new_thumb2();
12202 let op = ArmOp::I64Ldr {
12203 rdlo: Reg::R0,
12204 rdhi: Reg::R1,
12205 addr: MemAddr::imm(Reg::SP, 0),
12206 };
12207 let code = encoder.encode(&op).unwrap();
12208 // Two LDR instructions (lo at offset, hi at offset+4)
12209 assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12210 }
12211
12212 #[test]
12213 fn test_372_i64_ldr_indexed_materializes_address() {
12214 // #372: a memory i64.load carries an index register (R11 + addr + off).
12215 // The encoder must materialize `ip = base + index` (ADD.W) and load via
12216 // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12217 // stay byte-identical (plain `[base,#off]`, no ADD).
12218 let encoder = ArmEncoder::new_thumb2();
12219 let indexed = encoder
12220 .encode(&ArmOp::I64Ldr {
12221 rdlo: Reg::R0,
12222 rdhi: Reg::R1,
12223 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12224 })
12225 .unwrap();
12226 // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12227 assert_eq!(
12228 &indexed[0..4],
12229 &[0x0b, 0xeb, 0x00, 0x0c],
12230 "indexed I64Ldr must start with ADD.W ip, base, index"
12231 );
12232 let frame = encoder
12233 .encode(&ArmOp::I64Ldr {
12234 rdlo: Reg::R0,
12235 rdhi: Reg::R1,
12236 addr: MemAddr::imm(Reg::SP, 8),
12237 })
12238 .unwrap();
12239 // No index -> no ADD.W prefix (byte-identical frame access).
12240 assert_ne!(
12241 &frame[0..2],
12242 &[0x0b, 0xeb],
12243 "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12244 );
12245 }
12246
12247 #[test]
12248 fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12249 // #382: an indexed i64.load/store whose static offset > 0xFFF must
12250 // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12251 // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12252 // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12253 // vs arm-none-eabi-as.
12254 let encoder = ArmEncoder::new_thumb2();
12255 // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12256
12257 let ld = encoder
12258 .encode(&ArmOp::I64Ldr {
12259 rdlo: Reg::R0,
12260 rdhi: Reg::R1,
12261 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12262 })
12263 .expect("large-offset i64.load must lower, not skip");
12264 // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12265 assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12266 // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12267 // that path can only reach imm12 offsets.
12268 assert_ne!(
12269 &ld[0..2],
12270 &[0x0b, 0xeb],
12271 "must materialize the large offset"
12272 );
12273 // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12274 assert_eq!(
12275 &ld[4..20],
12276 &[
12277 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12278 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12279 0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12280 0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12281 ],
12282 "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12283 );
12284
12285 // Store: same base materialization, STR halves.
12286 let st = encoder
12287 .encode(&ArmOp::I64Str {
12288 rdlo: Reg::R2,
12289 rdhi: Reg::R3,
12290 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12291 })
12292 .expect("large-offset i64.store must lower, not skip");
12293 assert_eq!(st.len(), 20);
12294 assert_eq!(
12295 &st[4..20],
12296 &[
12297 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12298 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12299 0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12300 0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12301 ],
12302 "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12303 );
12304
12305 // Small-offset (imm12) indexed access stays byte-identical (#372): the
12306 // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12307 // folded immediates — NO extra MOVW/ADD.
12308 let small = encoder
12309 .encode(&ArmOp::I64Ldr {
12310 rdlo: Reg::R0,
12311 rdhi: Reg::R1,
12312 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12313 })
12314 .unwrap();
12315 assert_eq!(
12316 &small[0..4],
12317 &[0x0b, 0xeb, 0x00, 0x0c],
12318 "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12319 );
12320 assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12321 }
12322
12323 /// RQ-63-ARMI64OFF (#1165): A32 `I64Ldr`/`I64Str` at and past the pair
12324 /// form's immediate boundary. Bytes verified against `arm-none-eabi-as`
12325 /// (`.arch armv7-r`, `.arm`). 4091 is the last offset the pair folds (the
12326 /// high half sits at 4095 = imm12 max); 4092 is the straddle the v0.62
12327 /// census declined on (low half fits, high half does not).
12328 #[test]
12329 fn test_1165_a32_i64_ldst_offset_boundary_materializes() {
12330 let enc = ArmEncoder::new_arm32();
12331 let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12332 let ld = |off: i32| {
12333 enc.encode(&ArmOp::I64Ldr {
12334 rdlo: Reg::R0,
12335 rdhi: Reg::R1,
12336 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, off),
12337 })
12338 };
12339 // AT the boundary (4091): folded, byte-identical to the pre-#1165 form.
12340 assert_eq!(
12341 ld(4091).unwrap(),
12342 le(&[0xE08BC000, 0xE59C0FFB, 0xE59C1FFF]),
12343 "4091: add ip,fp,r0 ; ldr r0,[ip,#4091] ; ldr r1,[ip,#4095]"
12344 );
12345 // ONE ABOVE (4092): the high half would need #4096 → materialize.
12346 assert_eq!(
12347 ld(4092).unwrap(),
12348 le(&[0xE300CFFC, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004]),
12349 "4092: movw ip,#4092 ; add ip,ip,fp ; add ip,ip,r0 ; ldr r0,[ip] ; ldr r1,[ip,#4]"
12350 );
12351 // 4095 (low half at the imm12 max), 4096 (both out), 5000.
12352 assert_eq!(ld(4095).unwrap()[..4], le(&[0xE300CFFF])[..]);
12353 assert_eq!(ld(4096).unwrap()[..4], le(&[0xE301C000])[..]);
12354 assert_eq!(
12355 ld(5000).unwrap(),
12356 le(&[0xE301C388, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004])
12357 );
12358 // 70000 needs MOVT; store form.
12359 assert_eq!(
12360 enc.encode(&ArmOp::I64Str {
12361 rdlo: Reg::R2,
12362 rdhi: Reg::R3,
12363 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 70000),
12364 })
12365 .unwrap(),
12366 le(&[
12367 0xE301C170, 0xE340C001, 0xE08CC00B, 0xE08CC000, 0xE58C2000, 0xE58C3004
12368 ]),
12369 "70000: movw ip,#0x1170 ; movt ip,#1 ; add ip,ip,fp ; add ip,ip,r0 ; str r2,[ip] ; str r3,[ip,#4]"
12370 );
12371 // A memarg >= 2^31 arrives negative (0xFFFFFFF8 as i32 = -8): the full
12372 // 32 bits are materialized, mod 2^32 — never clamped, never masked.
12373 assert_eq!(
12374 ld(-8).unwrap(),
12375 le(&[
12376 0xE30FCFF8, 0xE34FCFFF, 0xE08CC00B, 0xE08CC000, 0xE59C0000, 0xE59C1004
12377 ]),
12378 "0xfffffff8: movw ip,#0xfff8 ; movt ip,#0xffff ; add ; add ; ldr ; ldr"
12379 );
12380 // No index register (frame-style base), out of range: MOVW ; ADD ip,ip,base.
12381 assert_eq!(
12382 enc.encode(&ArmOp::I64Ldr {
12383 rdlo: Reg::R0,
12384 rdhi: Reg::R1,
12385 addr: MemAddr::imm(Reg::SP, 5000),
12386 })
12387 .unwrap(),
12388 le(&[0xE301C388, 0xE08CC00D, 0xE59C0000, 0xE59C1004])
12389 );
12390 // The base IS the scratch: no free register → typed error, not a clobber.
12391 assert!(
12392 enc.encode(&ArmOp::I64Ldr {
12393 rdlo: Reg::R0,
12394 rdhi: Reg::R1,
12395 addr: MemAddr::imm(Reg::R12, 5000),
12396 })
12397 .is_err(),
12398 "materializing over an R12 base must be refused, not clobber it"
12399 );
12400 }
12401
12402 /// RQ-63-ARMI64OFF (#1165): the A32 WORD and SUB-WORD immediate arms used
12403 /// to MASK an out-of-range offset (`& 0xFFF` / `& 0xFF`) — a silent wrong
12404 /// address (`i32.load offset=5000` → `ldr r0,[ip,#904]`, `i32.load16_u
12405 /// offset=256` → `ldrh r0,[ip]`). At the field maximum the bytes are
12406 /// unchanged; one above, the offset is materialized. Bytes verified
12407 /// against `arm-none-eabi-as`.
12408 #[test]
12409 fn test_1165_a32_word_subword_offset_boundary_materializes_not_masks() {
12410 let enc = ArmEncoder::new_arm32();
12411 let le = |ws: &[u32]| ws.iter().flat_map(|w| w.to_le_bytes()).collect::<Vec<u8>>();
12412 let idx = |off: i32| MemAddr::reg_imm(Reg::R11, Reg::R0, off);
12413 let ldr = |addr| enc.encode(&ArmOp::Ldr { rd: Reg::R0, addr }).unwrap();
12414 let ldrh = |addr| enc.encode(&ArmOp::Ldrh { rd: Reg::R0, addr }).unwrap();
12415 let ldrsb = |addr| enc.encode(&ArmOp::Ldrsb { rd: Reg::R0, addr }).unwrap();
12416 let strh = |addr| enc.encode(&ArmOp::Strh { rd: Reg::R1, addr }).unwrap();
12417 let ldrb = |addr| enc.encode(&ArmOp::Ldrb { rd: Reg::R0, addr }).unwrap();
12418 // LDR imm12: 4095 at, 4096 above.
12419 assert_eq!(ldr(idx(4095)), le(&[0xE08BC000, 0xE59C0FFF]));
12420 assert_eq!(
12421 ldr(idx(4096)),
12422 le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE59C0000]),
12423 "ldr 4096 must materialize, not mask to #0"
12424 );
12425 // LDRH imm8: 255 at, 256 above (the pre-fix bytes were `ldrh r0,[ip]`).
12426 assert_eq!(ldrh(idx(255)), le(&[0xE08BC000, 0xE1DC0FBF]));
12427 assert_eq!(
12428 ldrh(idx(256)),
12429 le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00B0]),
12430 "ldrh 256 must materialize, not mask to #0"
12431 );
12432 // LDRSB imm8 (256 above), STRH imm8 (300 above).
12433 assert_eq!(
12434 ldrsb(idx(256)),
12435 le(&[0xE300C100, 0xE08CC00B, 0xE08CC000, 0xE1DC00D0])
12436 );
12437 assert_eq!(
12438 strh(idx(300)),
12439 le(&[0xE300C12C, 0xE08CC00B, 0xE08CC000, 0xE1CC10B0])
12440 );
12441 // LDRB is an imm12 form: 4095 at, 4096 above.
12442 assert_eq!(ldrb(idx(4095)), le(&[0xE08BC000, 0xE5DC0FFF]));
12443 assert_eq!(
12444 ldrb(idx(4096)),
12445 le(&[0xE301C000, 0xE08CC00B, 0xE08CC000, 0xE5DC0000])
12446 );
12447 // No index, out of range: MOVW ; ADD ip,ip,base ; access [ip].
12448 assert_eq!(
12449 ldr(MemAddr::imm(Reg::R0, 5000)),
12450 le(&[0xE301C388, 0xE08CC000, 0xE59C0000])
12451 );
12452 // The immediate arms are now a TRIPWIRE, never a mask.
12453 assert!(encode_mem_addr(&MemAddr::imm(Reg::R1, 0xFFF)).is_ok());
12454 assert!(
12455 encode_mem_addr(&MemAddr::imm(Reg::R1, 0x1000)).is_err(),
12456 "imm12 arm must refuse 0x1000, not mask it to #0"
12457 );
12458 assert!(encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0xFF)).is_ok());
12459 assert!(
12460 encode_mem_addr_imm8(&MemAddr::imm(Reg::R1, 0x100)).is_err(),
12461 "imm8 arm must refuse 0x100, not mask it to #0"
12462 );
12463 // Base is the scratch: refused, not clobbered.
12464 assert!(
12465 enc.encode(&ArmOp::Ldr {
12466 rd: Reg::R0,
12467 addr: MemAddr::imm(Reg::R12, 5000),
12468 })
12469 .is_err()
12470 );
12471 }
12472
12473 /// RQ-63-ARMI64OFF (#1165): Thumb-2 `i64_effective_base` used to CLAMP a
12474 /// negative offset (a memarg >= 2^31 after the selector's `as i32` cast)
12475 /// to 0 — `i64.load offset=0xfffffff8` read `[R11+addr]`. It now
12476 /// materializes the full 32 bits (MOVW+MOVT), the arithmetic the i32 word
12477 /// path uses for the same memarg. Bytes verified against
12478 /// `arm-none-eabi-as` (`.arch armv7e-m`, `.thumb`). A FRAME access with a
12479 /// negative offset (no index register) is refused loudly by
12480 /// `check_ldst_imm12`, never clamped.
12481 #[test]
12482 fn test_1165_thumb2_i64_negative_offset_materializes_not_clamps() {
12483 let enc = ArmEncoder::new_thumb2();
12484 let ld = enc
12485 .encode(&ArmOp::I64Ldr {
12486 rdlo: Reg::R0,
12487 rdhi: Reg::R1,
12488 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, -8),
12489 })
12490 .unwrap();
12491 assert_eq!(
12492 ld,
12493 vec![
12494 0x4f, 0xf6, 0xf8, 0x7c, // movw ip, #0xfff8
12495 0xcf, 0xf6, 0xff, 0x7c, // movt ip, #0xffff
12496 0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12497 0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12498 0xdc, 0xf8, 0x00, 0x00, // ldr.w r0, [ip]
12499 0xdc, 0xf8, 0x04, 0x10, // ldr.w r1, [ip, #4]
12500 ],
12501 "0xfffffff8 must be materialized in full, not clamped to [ip,#0]"
12502 );
12503 let st = enc
12504 .encode(&ArmOp::I64Str {
12505 rdlo: Reg::R2,
12506 rdhi: Reg::R3,
12507 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, i32::MIN),
12508 })
12509 .unwrap();
12510 assert_eq!(
12511 st,
12512 vec![
12513 0x40, 0xf2, 0x00, 0x0c, // movw ip, #0
12514 0xc8, 0xf2, 0x00, 0x0c, // movt ip, #0x8000
12515 0x00, 0xeb, 0x0c, 0x0c, // add.w ip, r0, ip
12516 0x0c, 0xeb, 0x0b, 0x0c, // add.w ip, ip, fp
12517 0xcc, 0xf8, 0x00, 0x20, // str.w r2, [ip]
12518 0xcc, 0xf8, 0x04, 0x30, // str.w r3, [ip, #4]
12519 ]
12520 );
12521 assert!(
12522 enc.encode(&ArmOp::I64Ldr {
12523 rdlo: Reg::R0,
12524 rdhi: Reg::R1,
12525 addr: MemAddr::imm(Reg::SP, -8),
12526 })
12527 .is_err(),
12528 "a negative frame offset must be refused, not clamped to [sp,#0]"
12529 );
12530 }
12531
12532 #[test]
12533 fn test_encode_i64_str_thumb2() {
12534 let encoder = ArmEncoder::new_thumb2();
12535 let op = ArmOp::I64Str {
12536 rdlo: Reg::R0,
12537 rdhi: Reg::R1,
12538 addr: MemAddr::imm(Reg::SP, 0),
12539 };
12540 let code = encoder.encode(&op).unwrap();
12541 // Two STR instructions (lo at offset, hi at offset+4)
12542 assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12543 }
12544
12545 #[test]
12546 fn test_encode_i64_all_comparisons_thumb2() {
12547 let encoder = ArmEncoder::new_thumb2();
12548
12549 let ops = vec![
12550 ArmOp::I64Ne {
12551 rd: Reg::R0,
12552 rnlo: Reg::R0,
12553 rnhi: Reg::R1,
12554 rmlo: Reg::R2,
12555 rmhi: Reg::R3,
12556 },
12557 ArmOp::I64LtS {
12558 rd: Reg::R0,
12559 rnlo: Reg::R0,
12560 rnhi: Reg::R1,
12561 rmlo: Reg::R2,
12562 rmhi: Reg::R3,
12563 },
12564 ArmOp::I64LtU {
12565 rd: Reg::R0,
12566 rnlo: Reg::R0,
12567 rnhi: Reg::R1,
12568 rmlo: Reg::R2,
12569 rmhi: Reg::R3,
12570 },
12571 ArmOp::I64LeS {
12572 rd: Reg::R0,
12573 rnlo: Reg::R0,
12574 rnhi: Reg::R1,
12575 rmlo: Reg::R2,
12576 rmhi: Reg::R3,
12577 },
12578 ArmOp::I64LeU {
12579 rd: Reg::R0,
12580 rnlo: Reg::R0,
12581 rnhi: Reg::R1,
12582 rmlo: Reg::R2,
12583 rmhi: Reg::R3,
12584 },
12585 ArmOp::I64GtS {
12586 rd: Reg::R0,
12587 rnlo: Reg::R0,
12588 rnhi: Reg::R1,
12589 rmlo: Reg::R2,
12590 rmhi: Reg::R3,
12591 },
12592 ArmOp::I64GtU {
12593 rd: Reg::R0,
12594 rnlo: Reg::R0,
12595 rnhi: Reg::R1,
12596 rmlo: Reg::R2,
12597 rmhi: Reg::R3,
12598 },
12599 ArmOp::I64GeS {
12600 rd: Reg::R0,
12601 rnlo: Reg::R0,
12602 rnhi: Reg::R1,
12603 rmlo: Reg::R2,
12604 rmhi: Reg::R3,
12605 },
12606 ArmOp::I64GeU {
12607 rd: Reg::R0,
12608 rnlo: Reg::R0,
12609 rnhi: Reg::R1,
12610 rmlo: Reg::R2,
12611 rmhi: Reg::R3,
12612 },
12613 ];
12614
12615 for op in &ops {
12616 let code = encoder.encode(op).unwrap();
12617 assert!(
12618 code.len() >= 8,
12619 "i64 comparison {:?} should emit at least 8 bytes, got {}",
12620 op,
12621 code.len()
12622 );
12623 }
12624 }
12625
12626 #[test]
12627 fn test_encode_i64_const_zero_thumb2() {
12628 let encoder = ArmEncoder::new_thumb2();
12629 let op = ArmOp::I64Const {
12630 rdlo: Reg::R0,
12631 rdhi: Reg::R1,
12632 value: 0,
12633 };
12634 let code = encoder.encode(&op).unwrap();
12635 // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12636 assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12637 }
12638
12639 #[test]
12640 fn test_encode_i64_const_negative_one_thumb2() {
12641 let encoder = ArmEncoder::new_thumb2();
12642 let op = ArmOp::I64Const {
12643 rdlo: Reg::R0,
12644 rdhi: Reg::R1,
12645 value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12646 };
12647 let code = encoder.encode(&op).unwrap();
12648 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12649 assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12650 }
12651
12652 // =========================================================================
12653 // Sub-word load/store encoding tests
12654 // =========================================================================
12655
12656 #[test]
12657 fn test_encode_ldrb_arm32() {
12658 let encoder = ArmEncoder::new_arm32();
12659 let op = ArmOp::Ldrb {
12660 rd: Reg::R0,
12661 addr: MemAddr::imm(Reg::R1, 4),
12662 };
12663 let code = encoder.encode(&op).unwrap();
12664 assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12665 // LDRB R0, [R1, #4] = 0xE5D10004
12666 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12667 assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12668 }
12669
12670 #[test]
12671 fn test_encode_strb_arm32() {
12672 let encoder = ArmEncoder::new_arm32();
12673 let op = ArmOp::Strb {
12674 rd: Reg::R0,
12675 addr: MemAddr::imm(Reg::R1, 0),
12676 };
12677 let code = encoder.encode(&op).unwrap();
12678 assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12679 // STRB R0, [R1, #0] = 0xE5C10000
12680 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12681 assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12682 }
12683
12684 #[test]
12685 fn test_encode_ldrh_arm32() {
12686 let encoder = ArmEncoder::new_arm32();
12687 let op = ArmOp::Ldrh {
12688 rd: Reg::R0,
12689 addr: MemAddr::imm(Reg::R1, 2),
12690 };
12691 let code = encoder.encode(&op).unwrap();
12692 assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12693 }
12694
12695 #[test]
12696 fn test_encode_strh_arm32() {
12697 let encoder = ArmEncoder::new_arm32();
12698 let op = ArmOp::Strh {
12699 rd: Reg::R0,
12700 addr: MemAddr::imm(Reg::R1, 0),
12701 };
12702 let code = encoder.encode(&op).unwrap();
12703 assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12704 }
12705
12706 #[test]
12707 fn test_encode_ldrsb_arm32() {
12708 let encoder = ArmEncoder::new_arm32();
12709 let op = ArmOp::Ldrsb {
12710 rd: Reg::R0,
12711 addr: MemAddr::imm(Reg::R1, 0),
12712 };
12713 let code = encoder.encode(&op).unwrap();
12714 assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12715 }
12716
12717 #[test]
12718 fn test_encode_ldrsh_arm32() {
12719 let encoder = ArmEncoder::new_arm32();
12720 let op = ArmOp::Ldrsh {
12721 rd: Reg::R0,
12722 addr: MemAddr::imm(Reg::R1, 0),
12723 };
12724 let code = encoder.encode(&op).unwrap();
12725 assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12726 }
12727
12728 #[test]
12729 fn test_encode_ldrb_thumb2_16bit() {
12730 let encoder = ArmEncoder::new_thumb2();
12731 let op = ArmOp::Ldrb {
12732 rd: Reg::R0,
12733 addr: MemAddr::imm(Reg::R1, 4),
12734 };
12735 let code = encoder.encode(&op).unwrap();
12736 // Low registers + small offset -> 16-bit encoding
12737 assert_eq!(
12738 code.len(),
12739 2,
12740 "Thumb-2 LDRB with small offset should be 16-bit"
12741 );
12742 }
12743
12744 #[test]
12745 fn test_encode_ldrb_thumb2_32bit() {
12746 let encoder = ArmEncoder::new_thumb2();
12747 let op = ArmOp::Ldrb {
12748 rd: Reg::R0,
12749 addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12750 };
12751 let code = encoder.encode(&op).unwrap();
12752 assert_eq!(
12753 code.len(),
12754 4,
12755 "Thumb-2 LDRB with large offset should be 32-bit"
12756 );
12757 }
12758
12759 #[test]
12760 fn test_encode_strb_thumb2_16bit() {
12761 let encoder = ArmEncoder::new_thumb2();
12762 let op = ArmOp::Strb {
12763 rd: Reg::R0,
12764 addr: MemAddr::imm(Reg::R1, 10),
12765 };
12766 let code = encoder.encode(&op).unwrap();
12767 assert_eq!(
12768 code.len(),
12769 2,
12770 "Thumb-2 STRB with small offset should be 16-bit"
12771 );
12772 }
12773
12774 #[test]
12775 fn test_encode_ldrh_thumb2_16bit() {
12776 let encoder = ArmEncoder::new_thumb2();
12777 let op = ArmOp::Ldrh {
12778 rd: Reg::R0,
12779 addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12780 };
12781 let code = encoder.encode(&op).unwrap();
12782 assert_eq!(
12783 code.len(),
12784 2,
12785 "Thumb-2 LDRH with small aligned offset should be 16-bit"
12786 );
12787 }
12788
12789 #[test]
12790 fn test_encode_strh_thumb2_16bit() {
12791 let encoder = ArmEncoder::new_thumb2();
12792 let op = ArmOp::Strh {
12793 rd: Reg::R0,
12794 addr: MemAddr::imm(Reg::R1, 4),
12795 };
12796 let code = encoder.encode(&op).unwrap();
12797 assert_eq!(
12798 code.len(),
12799 2,
12800 "Thumb-2 STRH with small aligned offset should be 16-bit"
12801 );
12802 }
12803
12804 #[test]
12805 fn test_encode_ldrsb_thumb2() {
12806 let encoder = ArmEncoder::new_thumb2();
12807 let op = ArmOp::Ldrsb {
12808 rd: Reg::R0,
12809 addr: MemAddr::imm(Reg::R1, 0),
12810 };
12811 let code = encoder.encode(&op).unwrap();
12812 // LDRSB has no 16-bit immediate form, always 32-bit
12813 assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12814 }
12815
12816 #[test]
12817 fn test_encode_ldrsh_thumb2() {
12818 let encoder = ArmEncoder::new_thumb2();
12819 let op = ArmOp::Ldrsh {
12820 rd: Reg::R0,
12821 addr: MemAddr::imm(Reg::R1, 0),
12822 };
12823 let code = encoder.encode(&op).unwrap();
12824 assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12825 }
12826
12827 #[test]
12828 fn test_encode_memory_size_thumb2() {
12829 let encoder = ArmEncoder::new_thumb2();
12830 let op = ArmOp::MemorySize { rd: Reg::R0 };
12831 let code = encoder.encode(&op).unwrap();
12832 // R0 and R10 are not both low registers, so this needs careful handling
12833 assert!(!code.is_empty(), "MemorySize should produce code");
12834 }
12835
12836 #[test]
12837 fn test_encode_memory_grow_thumb2() {
12838 let encoder = ArmEncoder::new_thumb2();
12839 let op = ArmOp::MemoryGrow {
12840 rd: Reg::R0,
12841 rn: Reg::R0,
12842 };
12843 let code = encoder.encode(&op).unwrap();
12844 assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12845 }
12846
12847 #[test]
12848 fn test_encode_subword_reg_offset_thumb2() {
12849 let encoder = ArmEncoder::new_thumb2();
12850
12851 // LDRB with register offset
12852 let op = ArmOp::Ldrb {
12853 rd: Reg::R0,
12854 addr: MemAddr::reg(Reg::R1, Reg::R2),
12855 };
12856 let code = encoder.encode(&op).unwrap();
12857 assert_eq!(
12858 code.len(),
12859 4,
12860 "Thumb-2 LDRB with reg offset should be 32-bit"
12861 );
12862
12863 // STRB with register offset
12864 let op = ArmOp::Strb {
12865 rd: Reg::R0,
12866 addr: MemAddr::reg(Reg::R1, Reg::R2),
12867 };
12868 let code = encoder.encode(&op).unwrap();
12869 assert_eq!(
12870 code.len(),
12871 4,
12872 "Thumb-2 STRB with reg offset should be 32-bit"
12873 );
12874
12875 // LDRH with register offset
12876 let op = ArmOp::Ldrh {
12877 rd: Reg::R0,
12878 addr: MemAddr::reg(Reg::R1, Reg::R2),
12879 };
12880 let code = encoder.encode(&op).unwrap();
12881 assert_eq!(
12882 code.len(),
12883 4,
12884 "Thumb-2 LDRH with reg offset should be 32-bit"
12885 );
12886
12887 // STRH with register offset
12888 let op = ArmOp::Strh {
12889 rd: Reg::R0,
12890 addr: MemAddr::reg(Reg::R1, Reg::R2),
12891 };
12892 let code = encoder.encode(&op).unwrap();
12893 assert_eq!(
12894 code.len(),
12895 4,
12896 "Thumb-2 STRH with reg offset should be 32-bit"
12897 );
12898 }
12899
12900 #[test]
12901 fn test_encode_subword_reg_imm_offset_thumb2() {
12902 let encoder = ArmEncoder::new_thumb2();
12903
12904 // LDRB with both register and immediate offset
12905 let op = ArmOp::Ldrb {
12906 rd: Reg::R0,
12907 addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12908 };
12909 let code = encoder.encode(&op).unwrap();
12910 // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12911 assert_eq!(
12912 code.len(),
12913 8,
12914 "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12915 );
12916 }
12917
12918 // ========================================================================
12919 // Helium MVE encoding tests
12920 // ========================================================================
12921
12922 #[test]
12923 fn test_encode_mve_addi32_thumb2() {
12924 let encoder = ArmEncoder::new_thumb2();
12925 let op = ArmOp::MveAddI {
12926 qd: QReg::Q0,
12927 qn: QReg::Q1,
12928 qm: QReg::Q2,
12929 size: MveSize::S32,
12930 };
12931 let code = encoder.encode(&op).unwrap();
12932 assert_eq!(
12933 code.len(),
12934 4,
12935 "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
12936 );
12937 }
12938
12939 #[test]
12940 fn test_encode_mve_subi16_thumb2() {
12941 let encoder = ArmEncoder::new_thumb2();
12942 let op = ArmOp::MveSubI {
12943 qd: QReg::Q0,
12944 qn: QReg::Q1,
12945 qm: QReg::Q2,
12946 size: MveSize::S16,
12947 };
12948 let code = encoder.encode(&op).unwrap();
12949 assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
12950 }
12951
12952 #[test]
12953 fn test_encode_mve_muli8_thumb2() {
12954 let encoder = ArmEncoder::new_thumb2();
12955 let op = ArmOp::MveMulI {
12956 qd: QReg::Q0,
12957 qn: QReg::Q1,
12958 qm: QReg::Q2,
12959 size: MveSize::S8,
12960 };
12961 let code = encoder.encode(&op).unwrap();
12962 assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
12963 }
12964
12965 #[test]
12966 fn test_encode_mve_bitwise_thumb2() {
12967 let encoder = ArmEncoder::new_thumb2();
12968
12969 let ops = vec![
12970 ArmOp::MveAnd {
12971 qd: QReg::Q0,
12972 qn: QReg::Q1,
12973 qm: QReg::Q2,
12974 },
12975 ArmOp::MveOrr {
12976 qd: QReg::Q0,
12977 qn: QReg::Q1,
12978 qm: QReg::Q2,
12979 },
12980 ArmOp::MveEor {
12981 qd: QReg::Q0,
12982 qn: QReg::Q1,
12983 qm: QReg::Q2,
12984 },
12985 ArmOp::MveBic {
12986 qd: QReg::Q0,
12987 qn: QReg::Q1,
12988 qm: QReg::Q2,
12989 },
12990 ];
12991 for op in ops {
12992 let code = encoder.encode(&op).unwrap();
12993 assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
12994 }
12995 }
12996
12997 #[test]
12998 fn test_encode_mve_mvn_thumb2() {
12999 let encoder = ArmEncoder::new_thumb2();
13000 let op = ArmOp::MveMvn {
13001 qd: QReg::Q0,
13002 qm: QReg::Q1,
13003 };
13004 let code = encoder.encode(&op).unwrap();
13005 assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
13006 }
13007
13008 #[test]
13009 fn test_encode_mve_load_store_thumb2() {
13010 let encoder = ArmEncoder::new_thumb2();
13011
13012 let load = ArmOp::MveLoad {
13013 qd: QReg::Q0,
13014 addr: MemAddr::imm(Reg::R0, 16),
13015 };
13016 let code = encoder.encode(&load).unwrap();
13017 assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
13018
13019 let store = ArmOp::MveStore {
13020 qd: QReg::Q1,
13021 addr: MemAddr::imm(Reg::R1, 0),
13022 };
13023 let code = encoder.encode(&store).unwrap();
13024 assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
13025 }
13026
13027 #[test]
13028 fn test_encode_mve_const_thumb2() {
13029 let encoder = ArmEncoder::new_thumb2();
13030 let op = ArmOp::MveConst {
13031 qd: QReg::Q0,
13032 bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
13033 };
13034 let code = encoder.encode(&op).unwrap();
13035 // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
13036 // Some words with hi16=0 skip MOVT, so length varies
13037 assert!(
13038 code.len() >= 24,
13039 "MVE const should produce multiple instructions"
13040 );
13041 }
13042
13043 #[test]
13044 fn test_encode_mve_dup_thumb2() {
13045 let encoder = ArmEncoder::new_thumb2();
13046 let op = ArmOp::MveDup {
13047 qd: QReg::Q0,
13048 rn: Reg::R0,
13049 size: MveSize::S32,
13050 };
13051 let code = encoder.encode(&op).unwrap();
13052 assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
13053 }
13054
13055 #[test]
13056 fn test_encode_mve_extract_lane_thumb2() {
13057 let encoder = ArmEncoder::new_thumb2();
13058 let op = ArmOp::MveExtractLane {
13059 rd: Reg::R0,
13060 qn: QReg::Q1,
13061 lane: 2,
13062 size: MveSize::S32,
13063 };
13064 let code = encoder.encode(&op).unwrap();
13065 assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
13066 }
13067
13068 #[test]
13069 fn test_encode_mve_insert_lane_thumb2() {
13070 let encoder = ArmEncoder::new_thumb2();
13071 let op = ArmOp::MveInsertLane {
13072 qd: QReg::Q0,
13073 rn: Reg::R1,
13074 lane: 3,
13075 size: MveSize::S32,
13076 };
13077 let code = encoder.encode(&op).unwrap();
13078 assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
13079 }
13080
13081 #[test]
13082 fn test_encode_mve_addf32_thumb2() {
13083 let encoder = ArmEncoder::new_thumb2();
13084 let op = ArmOp::MveAddF32 {
13085 qd: QReg::Q0,
13086 qn: QReg::Q1,
13087 qm: QReg::Q2,
13088 };
13089 let code = encoder.encode(&op).unwrap();
13090 assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
13091 }
13092
13093 #[test]
13094 fn test_encode_mve_divf32_thumb2() {
13095 let encoder = ArmEncoder::new_thumb2();
13096 let op = ArmOp::MveDivF32 {
13097 qd: QReg::Q0,
13098 qn: QReg::Q1,
13099 qm: QReg::Q2,
13100 };
13101 let code = encoder.encode(&op).unwrap();
13102 // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
13103 assert_eq!(
13104 code.len(),
13105 16,
13106 "MVE VDIV.F32 (lane-wise) should be 16 bytes"
13107 );
13108 }
13109
13110 #[test]
13111 fn test_encode_mve_sqrtf32_thumb2() {
13112 let encoder = ArmEncoder::new_thumb2();
13113 let op = ArmOp::MveSqrtF32 {
13114 qd: QReg::Q0,
13115 qm: QReg::Q1,
13116 };
13117 let code = encoder.encode(&op).unwrap();
13118 // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
13119 assert_eq!(
13120 code.len(),
13121 16,
13122 "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
13123 );
13124 }
13125
13126 #[test]
13127 fn test_encode_mve_negf32_thumb2() {
13128 let encoder = ArmEncoder::new_thumb2();
13129 let op = ArmOp::MveNegF32 {
13130 qd: QReg::Q0,
13131 qm: QReg::Q1,
13132 };
13133 let code = encoder.encode(&op).unwrap();
13134 assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
13135 }
13136
13137 #[test]
13138 fn test_encode_mve_absf32_thumb2() {
13139 let encoder = ArmEncoder::new_thumb2();
13140 let op = ArmOp::MveAbsF32 {
13141 qd: QReg::Q0,
13142 qm: QReg::Q1,
13143 };
13144 let code = encoder.encode(&op).unwrap();
13145 assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
13146 }
13147
13148 /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
13149 /// immediate encoding for the byte range and documents its bound.
13150 ///
13151 /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
13152 /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
13153 /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
13154 /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
13155 /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
13156 /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
13157 /// ThumbExpandImm pattern (rotation / replication), which is NOT
13158 /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
13159 /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
13160 /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
13161 /// This bound covers the measured `flat_flight` waste (#209).
13162 #[test]
13163 fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
13164 let encoder = ArmEncoder::new_thumb2();
13165 let op = ArmOp::And {
13166 rd: Reg::R2,
13167 rn: Reg::R0,
13168 op2: Operand2::Imm(0x7e),
13169 };
13170 let code = encoder.encode(&op).unwrap();
13171 assert_eq!(
13172 code,
13173 vec![0x00, 0xf0, 0x7e, 0x02],
13174 "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
13175 );
13176 }
13177
13178 /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
13179 /// data-processing immediate fix. Encodable modified immediates round-trip to
13180 /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
13181 /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
13182 /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
13183 /// this encodes it correctly.
13184 #[test]
13185 fn try_thumb_expand_imm_encodes_modified_immediates() {
13186 assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
13187 assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
13188 assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
13189 assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
13190 assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
13191 assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
13192 assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
13193 assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
13194 // Genuinely unrepresentable (bits too far apart for an 8-bit window).
13195 assert_eq!(try_thumb_expand_imm(0x101), None);
13196 assert_eq!(try_thumb_expand_imm(0x12345), None);
13197 }
13198
13199 /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
13200 /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
13201 /// forcing the selector to materialize into a register — closing the
13202 /// silent-miscompile class of #251/#253.
13203 #[test]
13204 fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
13205 let encoder = ArmEncoder::new_thumb2();
13206 // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
13207 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
13208 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
13209 // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
13210 assert!(
13211 encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
13212 "cmp #0x101 must error, not compare the wrong constant"
13213 );
13214 assert!(
13215 encoder
13216 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
13217 .is_err()
13218 );
13219 assert!(
13220 encoder
13221 .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
13222 .is_err()
13223 );
13224 // ...but a valid modified immediate still encodes.
13225 assert!(
13226 encoder
13227 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
13228 .is_ok()
13229 );
13230 }
13231
13232 /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
13233 /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
13234 #[test]
13235 fn mla_thumb2_encodes_correctly() {
13236 let encoder = ArmEncoder::new_thumb2();
13237 let code = encoder
13238 .encode(&ArmOp::Mla {
13239 rd: Reg::R2,
13240 rn: Reg::R3,
13241 rm: Reg::R4,
13242 ra: Reg::R8,
13243 })
13244 .unwrap();
13245 // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
13246 assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
13247 }
13248
13249 /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
13250 /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
13251 /// They now error (the selector must use register-offset addressing) — the
13252 /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
13253 #[test]
13254 fn ldst_imm12_offset_errors_when_out_of_range() {
13255 let encoder = ArmEncoder::new_thumb2();
13256 // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
13257 assert!(
13258 encoder
13259 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
13260 .is_ok()
13261 );
13262 // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
13263 assert!(
13264 encoder
13265 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
13266 .is_err(),
13267 "ldr offset 4096 must error, not wrap to 0"
13268 );
13269 assert!(
13270 encoder
13271 .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
13272 .is_err()
13273 );
13274 assert!(
13275 encoder
13276 .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
13277 .is_err()
13278 );
13279 assert!(
13280 encoder
13281 .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
13282 .is_err()
13283 );
13284 }
13285
13286 /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
13287 /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
13288 /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
13289 /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
13290 /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
13291 /// beyond 4095.
13292 #[test]
13293 fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
13294 let encoder = ArmEncoder::new_thumb2();
13295 // add sp, sp, #256 → ADDW (T4) SP, SP, #256 = 0d f2 00 1d
13296 assert_eq!(
13297 encoder
13298 .encode(&ArmOp::Add {
13299 rd: Reg::SP,
13300 rn: Reg::SP,
13301 op2: Operand2::Imm(256),
13302 })
13303 .unwrap(),
13304 vec![0x0d, 0xf2, 0x00, 0x1d],
13305 "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
13306 );
13307 // sub sp, sp, #256 → SUBW (T4) SP, SP, #256 = ad f2 00 1d
13308 assert_eq!(
13309 encoder
13310 .encode(&ArmOp::Sub {
13311 rd: Reg::SP,
13312 rn: Reg::SP,
13313 op2: Operand2::Imm(256),
13314 })
13315 .unwrap(),
13316 vec![0xad, 0xf2, 0x00, 0x1d],
13317 );
13318 // > 4095 has no single-instruction encoding → error, not silent wrong.
13319 assert!(
13320 encoder
13321 .encode(&ArmOp::Add {
13322 rd: Reg::SP,
13323 rn: Reg::SP,
13324 op2: Operand2::Imm(5000),
13325 })
13326 .is_err(),
13327 "add #5000 must error (no single ADDW), not mis-encode"
13328 );
13329 }
13330
13331 /// Closes the data-proc immediate class: AND and CMN now go through
13332 /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
13333 /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
13334 /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
13335 #[test]
13336 fn and_cmn_immediate_thumb_expand_else_error() {
13337 let encoder = ArmEncoder::new_thumb2();
13338 // byte range unchanged (bit-identical with the pre-retrofit encoding)
13339 assert_eq!(
13340 encoder
13341 .encode(&ArmOp::And {
13342 rd: Reg::R2,
13343 rn: Reg::R0,
13344 op2: Operand2::Imm(0x7e),
13345 })
13346 .unwrap(),
13347 vec![0x00, 0xf0, 0x7e, 0x02],
13348 );
13349 // a valid replicated modified immediate now encodes (was silently wrong)
13350 assert!(
13351 encoder
13352 .encode(&ArmOp::And {
13353 rd: Reg::R2,
13354 rn: Reg::R0,
13355 op2: Operand2::Imm(0xff00ff00u32 as i32),
13356 })
13357 .is_ok()
13358 );
13359 // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
13360 assert!(
13361 encoder
13362 .encode(&ArmOp::And {
13363 rd: Reg::R2,
13364 rn: Reg::R0,
13365 op2: Operand2::Imm(0x101),
13366 })
13367 .is_err()
13368 );
13369 assert!(
13370 encoder
13371 .encode(&ArmOp::Cmn {
13372 rn: Reg::R0,
13373 op2: Operand2::Imm(0x101),
13374 })
13375 .is_err(),
13376 "CMN #0x101 must error, not emit a NOP"
13377 );
13378 }
13379
13380 /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
13381 /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
13382 /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
13383 #[test]
13384 fn orr_eor_immediate_encode_in_byte_range_else_error() {
13385 let encoder = ArmEncoder::new_thumb2();
13386 // orr r2, r0, #0x7e → ORR.W T1, imm8=0x7e
13387 assert_eq!(
13388 encoder
13389 .encode(&ArmOp::Orr {
13390 rd: Reg::R2,
13391 rn: Reg::R0,
13392 op2: Operand2::Imm(0x7e),
13393 })
13394 .unwrap(),
13395 vec![0x40, 0xf0, 0x7e, 0x02],
13396 );
13397 // eor r2, r0, #0x7e → EOR.W T1, imm8=0x7e
13398 assert_eq!(
13399 encoder
13400 .encode(&ArmOp::Eor {
13401 rd: Reg::R2,
13402 rn: Reg::R0,
13403 op2: Operand2::Imm(0x7e),
13404 })
13405 .unwrap(),
13406 vec![0x80, 0xf0, 0x7e, 0x02],
13407 );
13408 // Out-of-range immediates error rather than silently mis-encode / NOP.
13409 assert!(
13410 encoder
13411 .encode(&ArmOp::Orr {
13412 rd: Reg::R2,
13413 rn: Reg::R0,
13414 op2: Operand2::Imm(0x140),
13415 })
13416 .is_err(),
13417 "ORR #0x140 must error, not emit a NOP"
13418 );
13419 }
13420
13421 #[test]
13422 fn test_encode_mve_different_qregs() {
13423 let encoder = ArmEncoder::new_thumb2();
13424
13425 // Test that different Q-register numbers produce different encodings
13426 let op1 = ArmOp::MveAddI {
13427 qd: QReg::Q0,
13428 qn: QReg::Q0,
13429 qm: QReg::Q0,
13430 size: MveSize::S32,
13431 };
13432 let op2 = ArmOp::MveAddI {
13433 qd: QReg::Q3,
13434 qn: QReg::Q5,
13435 qm: QReg::Q7,
13436 size: MveSize::S32,
13437 };
13438 let code1 = encoder.encode(&op1).unwrap();
13439 let code2 = encoder.encode(&op2).unwrap();
13440 assert_ne!(
13441 code1, code2,
13442 "Different Q-registers should produce different encodings"
13443 );
13444 }
13445
13446 #[test]
13447 fn test_encode_mve_arm32_loud_err() {
13448 // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13449 // a silent NOP here (dropping the vector op); it must now be a typed
13450 // Err so a broken "MVE implies Thumb" invariant fails loudly.
13451 let encoder = ArmEncoder::new_arm32();
13452 let op = ArmOp::MveAddI {
13453 qd: QReg::Q0,
13454 qn: QReg::Q1,
13455 qm: QReg::Q2,
13456 size: MveSize::S32,
13457 };
13458 let err = encoder
13459 .encode(&op)
13460 .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13461 assert!(
13462 err.to_string().contains("Thumb-2 only"),
13463 "unexpected error message: {err}"
13464 );
13465 }
13466}