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: encode an ARM32 (A32) load/store whose address uses a register
55 /// offset (`[rn, rm{, #off}]`). Returns `None` for ops with no register
56 /// offset (the caller falls through to the immediate-form arms). Computes
57 /// `ip = base + rm` then re-encodes the op against `[ip, #off]`, which works
58 /// uniformly for word/byte/halfword/signed forms. IP (R12) is the scratch
59 /// register the selector already treats as clobberable across memory ops.
60 fn encode_arm_reg_offset_mem(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
61 use synth_synthesis::Reg;
62 let addr = match op {
63 ArmOp::Ldr { addr, .. }
64 | ArmOp::Str { addr, .. }
65 | ArmOp::Ldrb { addr, .. }
66 | ArmOp::Strb { addr, .. }
67 | ArmOp::Ldrh { addr, .. }
68 | ArmOp::Strh { addr, .. }
69 | ArmOp::Ldrsb { addr, .. }
70 | ArmOp::Ldrsh { addr, .. } => addr,
71 _ => return Ok(None),
72 };
73 let Some(rm) = addr.offset_reg else {
74 return Ok(None);
75 };
76 let ip = Reg::R12;
77 // ADD ip, base, rm (cond=AL, opcode=ADD, S=0, register operand2)
78 let add: u32 = 0xE0800000
79 | (reg_to_bits(&addr.base) << 16)
80 | (reg_to_bits(&ip) << 12)
81 | reg_to_bits(&rm);
82 let mut bytes = add.to_le_bytes().to_vec();
83 // Re-encode the op against [ip, #off] (immediate form → no offset_reg,
84 // so this recursion hits the immediate arms, not this helper again).
85 let imm_addr = MemAddr::imm(ip, addr.offset);
86 let imm_op = match op {
87 ArmOp::Ldr { rd, .. } => ArmOp::Ldr {
88 rd: *rd,
89 addr: imm_addr,
90 },
91 ArmOp::Str { rd, .. } => ArmOp::Str {
92 rd: *rd,
93 addr: imm_addr,
94 },
95 ArmOp::Ldrb { rd, .. } => ArmOp::Ldrb {
96 rd: *rd,
97 addr: imm_addr,
98 },
99 ArmOp::Strb { rd, .. } => ArmOp::Strb {
100 rd: *rd,
101 addr: imm_addr,
102 },
103 ArmOp::Ldrh { rd, .. } => ArmOp::Ldrh {
104 rd: *rd,
105 addr: imm_addr,
106 },
107 ArmOp::Strh { rd, .. } => ArmOp::Strh {
108 rd: *rd,
109 addr: imm_addr,
110 },
111 ArmOp::Ldrsb { rd, .. } => ArmOp::Ldrsb {
112 rd: *rd,
113 addr: imm_addr,
114 },
115 ArmOp::Ldrsh { rd, .. } => ArmOp::Ldrsh {
116 rd: *rd,
117 addr: imm_addr,
118 },
119 _ => unreachable!(),
120 };
121 bytes.extend(self.encode_arm(&imm_op)?);
122 Ok(Some(bytes))
123 }
124
125 /// #594: A32 expansion of `ArmOp::CallIndirect` — mirror of the Thumb-2
126 /// arm (same contract: R11 holds the function-pointer table base, entry
127 /// `i` is a 4-byte code address, R12 is the encoder-scratch register):
128 ///
129 /// ```text
130 /// MOVW r12, #size ; #642: table size (compile-time immediate)
131 /// [MOVT r12, #size>>16] ; only when size exceeds 16 bits
132 /// CMP idx, r12 ; bounds guard: index >= size must TRAP
133 /// BLO +1 insn ; skip the trap when in bounds
134 /// UDF ; WASM Core §4.4.8 out-of-bounds trap
135 /// MOV r12, idx, LSL #2 ; table byte offset
136 /// LDR r12, [r11, r12] ; load function pointer
137 /// BLX r12 ; indirect call
138 /// ```
139 ///
140 /// #650, `table_byte_offset != 0` (a non-zero table of the contiguous
141 /// R11 region): the pointer load becomes
142 /// `ADD r12, r11, r12; LDR r12, [r12, #offset]` — offset 0 keeps the
143 /// single-load form (single-table modules byte-identical by
144 /// construction).
145 ///
146 /// #664, `null_check` (the table has null slots, linked as ZERO words
147 /// per the layout contract): `CMP r12, #0; BNE +1; UDF` between the
148 /// pointer load and the `BLX` — a call reaching an uninitialized slot
149 /// traps (§4.4.8). `false` keeps the expansion byte-identical.
150 ///
151 /// #676, `type_check` (heterogeneous table): the §4.4.8 type check is
152 /// discharged at RUNTIME against the type-id sidecar — after the bounds
153 /// guard, `MOV r12, idx, LSL #2; ADD r12, r11, r12;
154 /// LDR r12, [r12, #type_off]; CMP r12, #expected_id; BEQ +1; UDF`
155 /// (mirror of the Thumb-2 arm; the dispatch tail recomputes `idx*4`).
156 /// Null slots carry the reserved class id 0, subsuming the #664 null
157 /// trap. `None` (every homogeneous table — the verdict discharged at
158 /// COMPILE time by the closed-world verification, see the #642 selector
159 /// guard) emits nothing and keeps the expansion byte-identical.
160 fn encode_arm_call_indirect(
161 table_index_reg: &Reg,
162 table_size: u32,
163 table_byte_offset: u32,
164 null_check: bool,
165 type_check: Option<(u32, u32)>,
166 ) -> Vec<u8> {
167 let idx = reg_to_bits(table_index_reg);
168 let mut bytes = Vec::with_capacity(32);
169 // MOVW r12, #(size & 0xFFFF) — cond=E 0011 0000 imm4 Rd imm12.
170 let size_lo = table_size & 0xFFFF;
171 let movw: u32 = 0xE300_0000 | ((size_lo >> 12) << 16) | (12 << 12) | (size_lo & 0xFFF);
172 bytes.extend_from_slice(&movw.to_le_bytes());
173 // MOVT r12, #(size >> 16) — only for a table size above 16 bits.
174 let size_hi = table_size >> 16;
175 if size_hi != 0 {
176 let movt: u32 = 0xE340_0000 | ((size_hi >> 12) << 16) | (12 << 12) | (size_hi & 0xFFF);
177 bytes.extend_from_slice(&movt.to_le_bytes());
178 }
179 // CMP idx, r12 — cond=E, opcode=1010, S=1, Rn=idx, Rm=r12.
180 let cmp: u32 = 0xE150_000C | (idx << 16);
181 bytes.extend_from_slice(&cmp.to_le_bytes());
182 // BLO +1 insn (skip the UDF when index < size) — cond=LO(0011),
183 // imm24=0: target = branch + 8.
184 bytes.extend_from_slice(&0x3A00_0000u32.to_le_bytes());
185 // UDF — permanently undefined (same trap idiom as the A32 div-by-zero
186 // guards): call_indirect out-of-bounds trap.
187 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
188 // #676: runtime type check for a heterogeneous table — load the
189 // indexed slot's structural class id from the type-id sidecar and
190 // trap on mismatch (§4.4.8). Mirror of the Thumb-2 arm; `None`
191 // emits nothing (homogeneous tables byte-identical by construction).
192 if let Some((expected_id, type_off)) = type_check {
193 // RQ-61-IMMRANGE (#1072): these `debug_assert`s are compiled out
194 // in release, where the `& 0xFF` / `& 0xFFF` masks below would
195 // silently TRUNCATE an out-of-range value (id 256 compares as 0,
196 // letting a NULL slot pass the §4.4.8 check). The enforcement
197 // claim is DEMONSTRATED, not assumed: the sole `Some` producer is
198 // `resolve_runtime_type_check` (instruction_selector.rs), which
199 // loud-declines id > 255 and offset > 4095, and
200 // `test_676_call_indirect_runtime_check_range_declines` trips
201 // both declines (mutation-checked: disabling either turns it red).
202 debug_assert!(expected_id <= 255, "selector enforces the CMP imm8 range");
203 debug_assert!(type_off <= 4095, "selector enforces the LDR imm12 range");
204 // MOV r12, idx, LSL #2 (same as the dispatch tail's scale).
205 bytes.extend_from_slice(&(0xE1A0C000u32 | (2 << 7) | idx).to_le_bytes());
206 // ADD r12, r11, r12 — data-processing ADD (register).
207 bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
208 // LDR r12, [r12, #type_off] — immediate offset, P=1 U=1 L=1.
209 bytes.extend_from_slice(&(0xE59CC000u32 | (type_off & 0xFFF)).to_le_bytes());
210 // CMP r12, #expected_id — data-processing CMP (immediate).
211 bytes.extend_from_slice(&(0xE35C_0000u32 | (expected_id & 0xFF)).to_le_bytes());
212 // BEQ +1 insn (skip the UDF when the class id matches) —
213 // cond=EQ(0000), imm24=0: target = branch + 8.
214 bytes.extend_from_slice(&0x0A00_0000u32.to_le_bytes());
215 // UDF — the §4.4.8 type-mismatch trap.
216 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
217 }
218 // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
219 // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
220 let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
221 bytes.extend_from_slice(&mov.to_le_bytes());
222 if table_byte_offset == 0 {
223 // Table 0 (base = R11 itself): the pre-#650 single-load form.
224 // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
225 let ldr: u32 = 0xE79BC00C;
226 bytes.extend_from_slice(&ldr.to_le_bytes());
227 } else {
228 // #650: fold the table's compile-time base offset into the
229 // pointer load via the LDR imm12 form.
230 assert!(
231 table_byte_offset <= 4095,
232 "call_indirect table base offset {table_byte_offset} exceeds \
233 LDR imm12 — the selector must have declined this (#650)"
234 );
235 // ADD r12, r11, r12 — data-processing ADD (register).
236 bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
237 // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
238 let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
239 bytes.extend_from_slice(&ldr.to_le_bytes());
240 }
241 // #664: null-slot trap — only when the table image has null slots
242 // (zero-linked words). A fully-initialized table keeps the pre-#664
243 // bytes identical by construction.
244 if null_check {
245 // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
246 bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
247 // BNE +1 insn (skip the UDF when the pointer is non-null) —
248 // cond=NE(0001), imm24=0: target = branch + 8.
249 bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
250 // UDF — the §4.4.8 uninitialized-element trap (same idiom as
251 // the bounds guard).
252 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
253 }
254 // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
255 let blx: u32 = 0xE12FFF3C;
256 bytes.extend_from_slice(&blx.to_le_bytes());
257 bytes
258 }
259
260 /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
261 /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
262 /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
263 /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
264 /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
265 /// register contract and semantics exactly (A32 conditional execution
266 /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
267 /// not handle; the caller's match encodes or loudly rejects those.
268 fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
269 use synth_synthesis::Condition;
270
271 /// A32 condition-field bits (instruction bits [31:28]).
272 fn cond_bits(cond: &Condition) -> u32 {
273 match cond {
274 Condition::EQ => 0x0,
275 Condition::NE => 0x1,
276 Condition::HS => 0x2, // CS: unsigned >=
277 Condition::LO => 0x3, // CC: unsigned <
278 Condition::HI => 0x8, // unsigned >
279 Condition::LS => 0x9, // unsigned <=
280 Condition::GE => 0xA,
281 Condition::LT => 0xB,
282 Condition::GT => 0xC,
283 Condition::LE => 0xD,
284 }
285 }
286 fn w(b: &mut Vec<u8>, word: u32) {
287 b.extend_from_slice(&word.to_le_bytes());
288 }
289 /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
290 fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
291 w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
292 }
293 /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
294 fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
295 mov_cond_imm(b, cond_bits(cond), rd, 1);
296 mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
297 }
298 /// CMP rn, rm (register form).
299 fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
300 w(b, 0xE150_0000 | (rn << 16) | rm);
301 }
302 /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
303 fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
304 w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
305 }
306 /// MOVW rd, #imm16.
307 fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
308 w(
309 b,
310 0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
311 );
312 }
313 /// MOVT rd, #imm16.
314 fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
315 w(
316 b,
317 0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
318 );
319 }
320 /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
321 /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
322 /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
323 /// semantics the Thumb-2 expansions rely on.
324 fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
325 w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
326 }
327 const LSL: u32 = 0;
328 const LSR: u32 = 1;
329 const ASR: u32 = 2;
330 /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
331 fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
332 w(
333 b,
334 0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
335 );
336 }
337 /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
338 /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
339 fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
340 w(b, base | (rn << 16) | (rd << 12) | rm);
341 }
342 /// Data-processing with an immediate-shifted register operand:
343 /// `<op> rd, rn, rm, <LSL|LSR|ASR> #imm` — the A32 barrel shifter
344 /// folds a shift into the second operand for free. #1021 uses this to
345 /// run the popcnt SWAR fold on R12 alone (no second scratch, so R11 —
346 /// the linear-memory base — is never touched).
347 fn dp_reg_shift(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32, ty: u32, imm: u32) {
348 w(
349 b,
350 base | (rn << 16) | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rm,
351 );
352 }
353 /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
354 /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
355 fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
356 w(
357 b,
358 0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
359 );
360 }
361 /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
362 fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
363 w(b, 0xE1E0_0000 | (lo << 12) | lo); // MVN lo, lo
364 w(b, 0xE1E0_0000 | (hi << 12) | hi); // MVN hi, hi
365 w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); // ADDS lo, lo, #1
366 w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); // ADC hi, hi, #0
367 }
368 /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
369 /// sign bit is clear" guard of the signed div/rem arms.
370 fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
371 w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
372 w(b, 0x5A00_0003); // BPL +4 insns (past negate64)
373 }
374 /// The 64-iteration shift-subtract division loop — A32 transcription
375 /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
376 /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
377 fn div_loop(b: &mut Vec<u8>, counter: u32) {
378 w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
379 let loop_start = b.len();
380 // quotient <<= 1
381 shift_imm(b, LSL, 5, 5, 1);
382 orr_lsr31(b, 5, 4);
383 shift_imm(b, LSL, 4, 4, 1);
384 // remainder <<= 1, OR in dividend MSB
385 shift_imm(b, LSL, 7, 7, 1);
386 orr_lsr31(b, 7, 6);
387 shift_imm(b, LSL, 6, 6, 1);
388 orr_lsr31(b, 6, 1);
389 // dividend <<= 1
390 shift_imm(b, LSL, 1, 1, 1);
391 orr_lsr31(b, 1, 0);
392 shift_imm(b, LSL, 0, 0, 1);
393 // if remainder >= divisor (64-bit unsigned): subtract, set q bit
394 w(b, 0xE157_0003); // CMP R7, R3 (high words)
395 w(b, 0x8A00_0002); // BHI .subtract (+2 insns)
396 w(b, 0x3A00_0004); // BLO .next (+4 insns)
397 w(b, 0xE156_0002); // CMP R6, R2 (low words, highs equal)
398 w(b, 0x3A00_0002); // BLO .next (+2 insns)
399 w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
400 w(b, 0xE0C7_7003); // SBC R7, R7, R3
401 w(b, 0xE384_4001); // ORR R4, R4, #1
402 // .next: decrement and loop
403 w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
404 let diff = (loop_start as i64) - (b.len() as i64 + 8);
405 w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
406 }
407 /// 32-bit population count on working register `x` — A32 transcription
408 /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
409 /// constant register, R12 the shifted temp. Both are clobbered.
410 fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
411 // x = x - ((x >> 1) & 0x55555555)
412 shift_imm(b, LSR, 12, x, 1);
413 movw(b, c, 0x5555);
414 movt(b, c, 0x5555);
415 dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
416 dp_reg(b, 0xE040_0000, x, x, 12); // SUB x, x, R12
417 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
418 movw(b, c, 0x3333);
419 movt(b, c, 0x3333);
420 dp_reg(b, 0xE000_0000, 12, x, c); // AND R12, x, c
421 shift_imm(b, LSR, x, x, 2);
422 dp_reg(b, 0xE000_0000, x, x, c); // AND x, x, c
423 dp_reg(b, 0xE080_0000, x, x, 12); // ADD x, x, R12
424 // x = (x + (x >> 4)) & 0x0F0F0F0F
425 shift_imm(b, LSR, 12, x, 4);
426 dp_reg(b, 0xE080_0000, x, x, 12); // ADD x, x, R12
427 movw(b, c, 0x0F0F);
428 movt(b, c, 0x0F0F);
429 dp_reg(b, 0xE000_0000, x, x, c); // AND x, x, c
430 // x = (x * 0x01010101) >> 24
431 movw(b, c, 0x0101);
432 movt(b, c, 0x0101);
433 w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
434 shift_imm(b, LSR, x, x, 24);
435 }
436
437 let mut b: Vec<u8> = Vec::new();
438 match op {
439 // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
440 // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
441 ArmOp::SetCond { rd, cond } => {
442 set_cond(&mut b, cond, reg_to_bits(rd));
443 }
444
445 // SelectMove: conditional register move (Thumb: IT cond; MOV).
446 ArmOp::SelectMove { rd, rm, cond } => {
447 w(
448 &mut b,
449 (cond_bits(cond) << 28)
450 | 0x01A0_0000
451 | (reg_to_bits(rd) << 12)
452 | reg_to_bits(rm),
453 );
454 }
455
456 // I64SetCond: compare two i64 register pairs, 0/1 into rd.
457 // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
458 // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
459 // operand-swap + condition mapping as the Thumb-2 arm.
460 ArmOp::I64SetCond {
461 rd,
462 rn_lo,
463 rn_hi,
464 rm_lo,
465 rm_hi,
466 cond,
467 } => {
468 let rd_b = reg_to_bits(rd);
469 let (n_lo, n_hi, m_lo, m_hi) = (
470 reg_to_bits(rn_lo),
471 reg_to_bits(rn_hi),
472 reg_to_bits(rm_lo),
473 reg_to_bits(rm_hi),
474 );
475 match cond {
476 Condition::EQ | Condition::NE => {
477 cmp_reg(&mut b, n_lo, m_lo);
478 // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
479 w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
480 set_cond(&mut b, cond, rd_b);
481 }
482 // (swap operands?, condition after SBCS) per the Thumb arm:
483 // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
484 Condition::LT => {
485 cmp_reg(&mut b, n_lo, m_lo);
486 sbcs(&mut b, rd_b, n_hi, m_hi);
487 set_cond(&mut b, &Condition::LT, rd_b);
488 }
489 Condition::GE => {
490 cmp_reg(&mut b, n_lo, m_lo);
491 sbcs(&mut b, rd_b, n_hi, m_hi);
492 set_cond(&mut b, &Condition::GE, rd_b);
493 }
494 Condition::GT => {
495 cmp_reg(&mut b, m_lo, n_lo);
496 sbcs(&mut b, rd_b, m_hi, n_hi);
497 set_cond(&mut b, &Condition::LT, rd_b);
498 }
499 Condition::LE => {
500 cmp_reg(&mut b, m_lo, n_lo);
501 sbcs(&mut b, rd_b, m_hi, n_hi);
502 set_cond(&mut b, &Condition::GE, rd_b);
503 }
504 Condition::LO => {
505 cmp_reg(&mut b, n_lo, m_lo);
506 sbcs(&mut b, rd_b, n_hi, m_hi);
507 set_cond(&mut b, &Condition::LO, rd_b);
508 }
509 Condition::HS => {
510 cmp_reg(&mut b, n_lo, m_lo);
511 sbcs(&mut b, rd_b, n_hi, m_hi);
512 set_cond(&mut b, &Condition::HS, rd_b);
513 }
514 Condition::HI => {
515 cmp_reg(&mut b, m_lo, n_lo);
516 sbcs(&mut b, rd_b, m_hi, n_hi);
517 set_cond(&mut b, &Condition::LO, rd_b);
518 }
519 Condition::LS => {
520 cmp_reg(&mut b, m_lo, n_lo);
521 sbcs(&mut b, rd_b, m_hi, n_hi);
522 set_cond(&mut b, &Condition::HS, rd_b);
523 }
524 }
525 }
526
527 // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
528 ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
529 let rd_b = reg_to_bits(rd);
530 w(
531 &mut b,
532 0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
533 );
534 set_cond(&mut b, &Condition::EQ, rd_b);
535 }
536
537 // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
538 // Thumb-2 delegation arms.
539 ArmOp::I64Eqz { rd, rnlo, rnhi } => {
540 return self
541 .encode_arm(&ArmOp::I64SetCondZ {
542 rd: *rd,
543 rn_lo: *rnlo,
544 rn_hi: *rnhi,
545 })
546 .map(Some);
547 }
548 ArmOp::I64Eq {
549 rd,
550 rnlo,
551 rnhi,
552 rmlo,
553 rmhi,
554 }
555 | ArmOp::I64Ne {
556 rd,
557 rnlo,
558 rnhi,
559 rmlo,
560 rmhi,
561 }
562 | ArmOp::I64LtS {
563 rd,
564 rnlo,
565 rnhi,
566 rmlo,
567 rmhi,
568 }
569 | ArmOp::I64LtU {
570 rd,
571 rnlo,
572 rnhi,
573 rmlo,
574 rmhi,
575 }
576 | ArmOp::I64LeS {
577 rd,
578 rnlo,
579 rnhi,
580 rmlo,
581 rmhi,
582 }
583 | ArmOp::I64LeU {
584 rd,
585 rnlo,
586 rnhi,
587 rmlo,
588 rmhi,
589 }
590 | ArmOp::I64GtS {
591 rd,
592 rnlo,
593 rnhi,
594 rmlo,
595 rmhi,
596 }
597 | ArmOp::I64GtU {
598 rd,
599 rnlo,
600 rnhi,
601 rmlo,
602 rmhi,
603 }
604 | ArmOp::I64GeS {
605 rd,
606 rnlo,
607 rnhi,
608 rmlo,
609 rmhi,
610 }
611 | ArmOp::I64GeU {
612 rd,
613 rnlo,
614 rnhi,
615 rmlo,
616 rmhi,
617 } => {
618 let cond = match op {
619 ArmOp::I64Eq { .. } => Condition::EQ,
620 ArmOp::I64Ne { .. } => Condition::NE,
621 ArmOp::I64LtS { .. } => Condition::LT,
622 ArmOp::I64LtU { .. } => Condition::LO,
623 ArmOp::I64LeS { .. } => Condition::LE,
624 ArmOp::I64LeU { .. } => Condition::LS,
625 ArmOp::I64GtS { .. } => Condition::GT,
626 ArmOp::I64GtU { .. } => Condition::HI,
627 ArmOp::I64GeS { .. } => Condition::GE,
628 _ => Condition::HS,
629 };
630 return self
631 .encode_arm(&ArmOp::I64SetCond {
632 rd: *rd,
633 rn_lo: *rnlo,
634 rn_hi: *rnhi,
635 rm_lo: *rmlo,
636 rm_hi: *rmhi,
637 cond,
638 })
639 .map(Some);
640 }
641
642 // I64Mul: cross products into R12, then UMULL — same sequence and
643 // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
644 ArmOp::I64Mul {
645 rd_lo,
646 rd_hi,
647 rn_lo,
648 rn_hi,
649 rm_lo,
650 rm_hi,
651 } => {
652 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
653 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
654 let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
655 // MUL R12, rn_lo, rm_hi (R12 = a_lo * b_hi)
656 w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
657 // MLA R12, rn_hi, rm_lo, R12 (R12 += a_hi * b_lo)
658 w(
659 &mut b,
660 0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
661 );
662 // UMULL rd_lo, rd_hi, rn_lo, rm_lo
663 w(
664 &mut b,
665 0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
666 );
667 // ADD rd_hi, rd_hi, R12
668 w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
669 }
670
671 // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
672 // the Thumb-2 arms. #1048: the expansion must never write its own
673 // input operands — the pre-#1048 A32 arms masked the amount in
674 // place (`AND ml, ml, #63`) and used the amount's home high
675 // register as scratch, identically to the Thumb-2 defect. R12
676 // (encoder scratch, never allocatable, #212) is the ONLY
677 // temporary; the masked amount is re-derived from the untouched
678 // rm_lo where a second live temp would otherwise be needed.
679 // Register-controlled shifts >= 32 yield 0, which the small path
680 // relies on for n = 0. Same #1039-style loud alias guards as the
681 // Thumb-2 arms.
682 ArmOp::I64Shl {
683 rd_lo,
684 rd_hi,
685 rn_lo,
686 rn_hi,
687 rm_lo,
688 rm_hi: _,
689 } => {
690 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
691 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
692 let ml = reg_to_bits(rm_lo);
693 if dh == nl || dh == ml {
694 return Err(synth_core::Error::synthesis(format!(
695 "I64Shl (A32): rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
696 )));
697 }
698 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
699 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
700 w(&mut b, 0x5A00_0007); // BPL .large
701 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
702 shift_reg(&mut b, LSL, dh, nh, 12); // dh = hi << n
703 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
704 shift_reg(&mut b, LSR, 12, nl, 12); // r12 = lo >> (32-n)
705 w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | 12); // ORR dh, dh, r12
706 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
707 shift_reg(&mut b, LSL, dl, nl, 12); // dl = lo << n
708 w(&mut b, 0xEA00_0001); // B .done
709 shift_reg(&mut b, LSL, dh, nl, 12); // .large: dh = lo << (n-32)
710 w(&mut b, 0xE3A0_0000 | (dl << 12)); // MOV dl, #0
711 }
712 ArmOp::I64ShrU {
713 rd_lo,
714 rd_hi,
715 rn_lo,
716 rn_hi,
717 rm_lo,
718 rm_hi: _,
719 } => {
720 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
721 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
722 let ml = reg_to_bits(rm_lo);
723 if dl == nh || dl == ml {
724 return Err(synth_core::Error::synthesis(format!(
725 "I64ShrU (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
726 )));
727 }
728 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
729 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
730 w(&mut b, 0x5A00_0007); // BPL .large
731 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
732 shift_reg(&mut b, LSR, dl, nl, 12); // dl = lo >> n
733 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
734 shift_reg(&mut b, LSL, 12, nh, 12); // r12 = hi << (32-n)
735 w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
736 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
737 shift_reg(&mut b, LSR, dh, nh, 12); // dh = hi >> n
738 w(&mut b, 0xEA00_0001); // B .done
739 shift_reg(&mut b, LSR, dl, nh, 12); // .large: dl = hi >> (n-32)
740 w(&mut b, 0xE3A0_0000 | (dh << 12)); // MOV dh, #0
741 }
742 ArmOp::I64ShrS {
743 rd_lo,
744 rd_hi,
745 rn_lo,
746 rn_hi,
747 rm_lo,
748 rm_hi: _,
749 } => {
750 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
751 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
752 let ml = reg_to_bits(rm_lo);
753 if dl == nh || dl == ml {
754 return Err(synth_core::Error::synthesis(format!(
755 "I64ShrS (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
756 )));
757 }
758 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
759 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
760 w(&mut b, 0x5A00_0007); // BPL .large
761 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
762 shift_reg(&mut b, LSR, dl, nl, 12); // dl = lo >> n
763 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
764 shift_reg(&mut b, LSL, 12, nh, 12); // r12 = hi << (32-n)
765 w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
766 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
767 shift_reg(&mut b, ASR, dh, nh, 12); // dh = hi >> n (arith)
768 w(&mut b, 0xEA00_0001); // B .done
769 shift_reg(&mut b, ASR, dl, nh, 12); // .large: dl = hi >> (n-32)
770 w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
771 }
772
773 // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
774 // the same fixed-register core as the Thumb-2 arms — value in
775 // R0:R1, amount in R2, scratch R3 + R12.
776 ArmOp::I64Rotl {
777 rdlo,
778 rdhi,
779 rnlo,
780 rnhi,
781 shift,
782 } => {
783 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
784 for word in [
785 0xE202_203Fu32, // AND R2, R2, #63 (mask amount mod 64)
786 0xE252_3020, // SUBS R3, R2, #32 (R3 = n-32, sets N)
787 0x5A00_0007, // BPL .large (n >= 32)
788 // --- small rotation (n < 32) ---
789 0xE262_3020, // RSB R3, R2, #32 (R3 = 32-n)
790 0xE1A0_C330, // LSR R12, R0, R3 (lo >> (32-n))
791 0xE1A0_3331, // LSR R3, R1, R3 (hi >> (32-n))
792 0xE1A0_1211, // LSL R1, R1, R2 (hi << n)
793 0xE181_100C, // ORR R1, R1, R12 (new_hi)
794 0xE1A0_0210, // LSL R0, R0, R2 (lo << n)
795 0xE180_0003, // ORR R0, R0, R3 (new_lo)
796 0xEA00_0007, // B .done
797 // --- large rotation (n >= 32), R3 = m = n-32 ---
798 0xE263_2020, // RSB R2, R3, #32 (R2 = 32-m = 64-n)
799 0xE1A0_C231, // LSR R12, R1, R2 (hi >> (64-n))
800 0xE1A0_2230, // LSR R2, R0, R2 (lo >> (64-n))
801 0xE1A0_0310, // LSL R0, R0, R3 (lo << m)
802 0xE1A0_1311, // LSL R1, R1, R3 (hi << m)
803 0xE180_C00C, // ORR R12, R0, R12 (new_hi = (lo<<m)|(hi>>(64-n)))
804 0xE181_0002, // ORR R0, R1, R2 (new_lo = (hi<<m)|(lo>>(64-n)))
805 0xE1A0_100C, // MOV R1, R12 (new_hi into place)
806 ] {
807 w(&mut b, word);
808 }
809 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
810 }
811 ArmOp::I64Rotr {
812 rdlo,
813 rdhi,
814 rnlo,
815 rnhi,
816 shift,
817 } => {
818 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
819 for word in [
820 0xE202_203Fu32, // AND R2, R2, #63 (mask amount mod 64)
821 0xE252_3020, // SUBS R3, R2, #32 (R3 = n-32, sets N)
822 0x5A00_0007, // BPL .large (n >= 32)
823 // --- small rotation (n < 32) ---
824 0xE262_3020, // RSB R3, R2, #32 (R3 = 32-n)
825 0xE1A0_C311, // LSL R12, R1, R3 (hi << (32-n))
826 0xE1A0_3310, // LSL R3, R0, R3 (lo << (32-n))
827 0xE1A0_0230, // LSR R0, R0, R2 (lo >> n)
828 0xE180_000C, // ORR R0, R0, R12 (new_lo)
829 0xE1A0_1231, // LSR R1, R1, R2 (hi >> n)
830 0xE181_1003, // ORR R1, R1, R3 (new_hi)
831 0xEA00_0007, // B .done
832 // --- large rotation (n >= 32), R3 = m = n-32 ---
833 0xE263_2020, // RSB R2, R3, #32 (R2 = 32-m = 64-n)
834 0xE1A0_C210, // LSL R12, R0, R2 (lo << (64-n))
835 0xE1A0_2211, // LSL R2, R1, R2 (hi << (64-n))
836 0xE1A0_1331, // LSR R1, R1, R3 (hi >> m)
837 0xE181_C00C, // ORR R12, R1, R12 (new_lo = (hi>>m)|(lo<<(64-n)))
838 0xE1A0_1330, // LSR R1, R0, R3 (lo >> m)
839 0xE181_1002, // ORR R1, R1, R2 (new_hi = (lo>>m)|(hi<<(64-n)))
840 0xE1A0_000C, // MOV R0, R12 (new_lo into place)
841 ] {
842 w(&mut b, word);
843 }
844 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
845 }
846
847 // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
848 // execution replaces the Thumb branches; like the Thumb arm, the
849 // high word of the result pair (rnhi) is cleared last.
850 ArmOp::I64Clz { rd, rnlo, rnhi } => {
851 let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
852 w(&mut b, 0xE350_0000 | (hi << 16)); // CMP rnhi, #0
853 w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); // CLZNE rd, rnhi
854 w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); // CLZEQ rd, rnlo
855 w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
856 // #1048: the former trailing `MOV rnhi, #0` is GONE — it
857 // wrote the OPERAND's home high register (see the Thumb-2
858 // I64Clz comment). Callers that relied on the implicit clear
859 // emit their own explicit hi-zero op.
860 }
861
862 // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
863 // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
864 // conditional ADD.
865 ArmOp::I64Ctz { rd, rnlo, rnhi } => {
866 let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
867 w(&mut b, 0xE350_0000 | (lo << 16)); // CMP rnlo, #0
868 w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); // RBITNE rd, rnlo
869 w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); // RBITEQ rd, rnhi
870 w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); // CLZ rd, rd
871 w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
872 // #1048: no trailing `MOV rnhi, #0` — see I64Clz above.
873 }
874
875 // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
876 // 16 bits, mirroring the Thumb-2 arm).
877 ArmOp::I64Const { rdlo, rdhi, value } => {
878 let lo32 = *value as u32;
879 let hi32 = (*value >> 32) as u32;
880 movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
881 if lo32 > 0xFFFF {
882 movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
883 }
884 movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
885 if hi32 > 0xFFFF {
886 movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
887 }
888 }
889
890 // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
891 // A register offset is materialized into IP once (the #206/#372
892 // hazard: dropping it would read the wrong address).
893 ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
894 let base = if let Some(rm) = addr.offset_reg {
895 // ADD ip, base, rm
896 w(
897 &mut b,
898 0xE080_0000
899 | (reg_to_bits(&addr.base) << 16)
900 | (12 << 12)
901 | reg_to_bits(&rm),
902 );
903 12
904 } else {
905 reg_to_bits(&addr.base)
906 };
907 if addr.offset < 0 || addr.offset > 0xFFB {
908 return Err(synth_core::Error::synthesis(format!(
909 "i64 load/store offset {} out of the A32 imm12 range (0..=4091) — materialize the offset into a register",
910 addr.offset
911 )));
912 }
913 let off = addr.offset as u32;
914 let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
915 0xE590_0000 // LDR
916 } else {
917 0xE580_0000 // STR
918 };
919 w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
920 w(
921 &mut b,
922 opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
923 );
924 }
925
926 // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
927 ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
928 if rdlo != rn {
929 w(
930 &mut b,
931 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
932 );
933 }
934 w(
935 &mut b,
936 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
937 );
938 }
939
940 // I64ExtendI32U: rdlo = rn; rdhi = 0.
941 ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
942 if rdlo != rn {
943 w(
944 &mut b,
945 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
946 );
947 }
948 w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
949 }
950
951 // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
952 ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
953 w(
954 &mut b,
955 0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
956 );
957 w(
958 &mut b,
959 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
960 );
961 }
962 ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
963 w(
964 &mut b,
965 0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
966 );
967 w(
968 &mut b,
969 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
970 );
971 }
972 ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
973 if rdlo != rnlo {
974 w(
975 &mut b,
976 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
977 );
978 }
979 w(
980 &mut b,
981 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
982 );
983 }
984
985 // I32WrapI64: take the low word. When rd == rnlo this is a genuine
986 // no-op (the one case where a NOP word is the correct encoding).
987 ArmOp::I32WrapI64 { rd, rnlo } => {
988 w(
989 &mut b,
990 0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
991 );
992 }
993
994 // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
995 // The selector emits these as separate Adds/Adc ops; the fused
996 // variants are verification-constructed, but they encode for real.
997 ArmOp::I64Add {
998 rdlo,
999 rdhi,
1000 rnlo,
1001 rnhi,
1002 rmlo,
1003 rmhi,
1004 } => {
1005 dp_reg(
1006 &mut b,
1007 0xE090_0000, // ADDS
1008 reg_to_bits(rdlo),
1009 reg_to_bits(rnlo),
1010 reg_to_bits(rmlo),
1011 );
1012 dp_reg(
1013 &mut b,
1014 0xE0A0_0000, // ADC
1015 reg_to_bits(rdhi),
1016 reg_to_bits(rnhi),
1017 reg_to_bits(rmhi),
1018 );
1019 }
1020 ArmOp::I64Sub {
1021 rdlo,
1022 rdhi,
1023 rnlo,
1024 rnhi,
1025 rmlo,
1026 rmhi,
1027 } => {
1028 dp_reg(
1029 &mut b,
1030 0xE050_0000, // SUBS
1031 reg_to_bits(rdlo),
1032 reg_to_bits(rnlo),
1033 reg_to_bits(rmlo),
1034 );
1035 dp_reg(
1036 &mut b,
1037 0xE0C0_0000, // SBC
1038 reg_to_bits(rdhi),
1039 reg_to_bits(rnhi),
1040 reg_to_bits(rmhi),
1041 );
1042 }
1043
1044 // I64And / I64Or / I64Xor: two independent word ops.
1045 ArmOp::I64And {
1046 rdlo,
1047 rdhi,
1048 rnlo,
1049 rnhi,
1050 rmlo,
1051 rmhi,
1052 }
1053 | ArmOp::I64Or {
1054 rdlo,
1055 rdhi,
1056 rnlo,
1057 rnhi,
1058 rmlo,
1059 rmhi,
1060 }
1061 | ArmOp::I64Xor {
1062 rdlo,
1063 rdhi,
1064 rnlo,
1065 rnhi,
1066 rmlo,
1067 rmhi,
1068 } => {
1069 let base = match op {
1070 ArmOp::I64And { .. } => 0xE000_0000, // AND
1071 ArmOp::I64Or { .. } => 0xE180_0000, // ORR
1072 _ => 0xE020_0000, // EOR
1073 };
1074 dp_reg(
1075 &mut b,
1076 base,
1077 reg_to_bits(rdlo),
1078 reg_to_bits(rnlo),
1079 reg_to_bits(rmlo),
1080 );
1081 dp_reg(
1082 &mut b,
1083 base,
1084 reg_to_bits(rdhi),
1085 reg_to_bits(rnhi),
1086 reg_to_bits(rmhi),
1087 );
1088 }
1089
1090 // I64DivU: binary long division — A32 transcription of the Thumb-2
1091 // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1092 // shift-subtract core, quotient to R0:R1, result to rd pair).
1093 ArmOp::I64DivU {
1094 rdlo,
1095 rdhi,
1096 rnlo,
1097 rnhi,
1098 rmlo,
1099 rmhi,
1100 elide_zero_guard,
1101 } => {
1102 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1103 // #494 phase 2b: elided only under a certificate-discharged
1104 // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1105 if !elide_zero_guard {
1106 emit_a32_i64_divisor_zero_trap(&mut b);
1107 }
1108 w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1109 for r in 4..8u32 {
1110 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1111 }
1112 div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1113 w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1114 w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1115 w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1116 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1117 }
1118
1119 // I64DivS: sign-extract, unsigned core, conditional negate —
1120 // A32 transcription of the Thumb-2 arm.
1121 ArmOp::I64DivS {
1122 rdlo,
1123 rdhi,
1124 rnlo,
1125 rnhi,
1126 rmlo,
1127 rmhi,
1128 elide_zero_guard,
1129 elide_overflow_guard,
1130 } => {
1131 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1132 // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1133 // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1134 // the #633 overflow guard falls ONLY to
1135 // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1136 // divisor-nonzero fact alone must keep it.
1137 if !elide_zero_guard {
1138 emit_a32_i64_divisor_zero_trap(&mut b);
1139 }
1140 if !elide_overflow_guard {
1141 // #633: INT64_MIN / -1 overflows — trap like the i32 path
1142 // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1143 emit_a32_i64_divs_overflow_trap(&mut b);
1144 }
1145 w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1146 w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1147 skip_negate_if_positive(&mut b, 1);
1148 negate64(&mut b, 0, 1);
1149 skip_negate_if_positive(&mut b, 3);
1150 negate64(&mut b, 2, 3);
1151 for r in 4..8u32 {
1152 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1153 }
1154 div_loop(&mut b, 8); // counter in R8 (saved above)
1155 w(&mut b, 0xE1A0_0004); // MOV R0, R4
1156 w(&mut b, 0xE1A0_1005); // MOV R1, R5
1157 skip_negate_if_positive(&mut b, 9);
1158 negate64(&mut b, 0, 1);
1159 w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1160 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1161 }
1162
1163 // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1164 ArmOp::I64RemU {
1165 rdlo,
1166 rdhi,
1167 rnlo,
1168 rnhi,
1169 rmlo,
1170 rmhi,
1171 elide_zero_guard,
1172 } => {
1173 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1174 if !elide_zero_guard {
1175 emit_a32_i64_divisor_zero_trap(&mut b);
1176 }
1177 w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1178 for r in 4..8u32 {
1179 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1180 }
1181 div_loop(&mut b, 8);
1182 w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1183 w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1184 w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1185 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1186 }
1187
1188 // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1189 ArmOp::I64RemS {
1190 rdlo,
1191 rdhi,
1192 rnlo,
1193 rnhi,
1194 rmlo,
1195 rmhi,
1196 elide_zero_guard,
1197 } => {
1198 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1199 if !elide_zero_guard {
1200 emit_a32_i64_divisor_zero_trap(&mut b);
1201 }
1202 w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1203 w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1204 skip_negate_if_positive(&mut b, 1);
1205 negate64(&mut b, 0, 1);
1206 skip_negate_if_positive(&mut b, 3);
1207 negate64(&mut b, 2, 3);
1208 for r in 4..8u32 {
1209 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1210 }
1211 div_loop(&mut b, 8);
1212 w(&mut b, 0xE1A0_0006); // MOV R0, R6
1213 w(&mut b, 0xE1A0_1007); // MOV R1, R7
1214 skip_negate_if_positive(&mut b, 9);
1215 negate64(&mut b, 0, 1);
1216 w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1217 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1218 }
1219
1220 // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1221 // mirroring the Thumb-2 arm's #1021 register contract: R12 is the
1222 // ONLY scratch. The previous transcription copied the old Thumb
1223 // contract's R11 borrow — but R11 is the linear-memory base on
1224 // this path too, so it inherited the same live miscompile. A32
1225 // has no ThumbExpandImm for 0xXYXYXYXY masks, so instead the
1226 // barrel shifter folds each shift into the mask AND itself
1227 // (`AND R12, R12, rd, LSR #n`), and step 2 recovers `x & C` from
1228 // one term via `x - (((x >> 2) & C) << 2) = x & C` — the second
1229 // temp disappears algebraically. Straight-line, no PUSH/POP,
1230 // nothing to skip on a trap edge.
1231 ArmOp::Popcnt { rd, rm } => {
1232 let rd_b = reg_to_bits(rd);
1233 // Defensive (#1021), same contract as the Thumb-2 arm.
1234 if rd_b >= 11 {
1235 return Err(synth_core::Error::synthesis(
1236 "Popcnt destination must be R0-R10: R11 is the linear-memory \
1237 base and R12 is the expansion's scratch (#1021)",
1238 ));
1239 }
1240 if rd != rm {
1241 w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1242 }
1243 // x = x - ((x >> 1) & 0x55555555)
1244 movw(&mut b, 12, 0x5555);
1245 movt(&mut b, 12, 0x5555);
1246 dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 1); // AND R12, R12, rd, LSR #1
1247 dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 12); // SUB rd, rd, R12
1248 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333), one temp:
1249 // R12 = (x >> 2) & C; x - (R12 << 2) = x & C; then + R12.
1250 movw(&mut b, 12, 0x3333);
1251 movt(&mut b, 12, 0x3333);
1252 dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 2); // AND R12, R12, rd, LSR #2
1253 dp_reg_shift(&mut b, 0xE040_0000, rd_b, rd_b, 12, LSL, 2); // SUB rd, rd, R12, LSL #2
1254 dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 12); // ADD rd, rd, R12
1255 // x = (x + (x >> 4)) & 0x0F0F0F0F
1256 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 4); // ADD rd, rd, rd, LSR #4
1257 movw(&mut b, 12, 0x0F0F);
1258 movt(&mut b, 12, 0x0F0F);
1259 dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1260 // x += x >> 8; x += x >> 16; x &= 0x3F
1261 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 8);
1262 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 16);
1263 w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1264 }
1265
1266 // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1267 // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1268 // result word rnhi cleared last, mirroring the Thumb contract).
1269 ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1270 let hi = reg_to_bits(rnhi);
1271 w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1272 // #632 audit: route rnlo through R12 so a pair living at
1273 // (R3,R4) cannot read a clobbered R4 (sources read before any
1274 // scratch register they could occupy is written).
1275 w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1276 w(&mut b, 0xE1A0_5000 | hi); // MOV R5, rnhi
1277 w(&mut b, 0xE1A0_400C); // MOV R4, R12
1278 popcnt_word(&mut b, 4, 3);
1279 popcnt_word(&mut b, 5, 3);
1280 // #632: carry the count across the scratch restore in R12 —
1281 // rd is allocator-assigned and can land inside {R3,R4,R5};
1282 // the old `ADD rd, R4, R5` before the POP was destroyed by
1283 // the restore. R12 is never allocatable and never restored.
1284 dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1285 w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1286 w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1287 // #1048: no trailing `MOV rnhi, #0` — the hi-word clear wrote
1288 // the OPERAND's home high register; callers emit it explicitly.
1289 }
1290
1291 _ => return Ok(None),
1292 }
1293 Ok(Some(b))
1294 }
1295
1296 fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1297 // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1298 // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1299 // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1300 // so the value silently vanished. Mirror of the #594 CallIndirect
1301 // early-return: if the expansion helper covers the op, its bytes are
1302 // the encoding.
1303 if let Some(bytes) = self.encode_arm_expanded(op)? {
1304 return Ok(bytes);
1305 }
1306 // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1307 // returns the 12-bit immediate, so the immediate-form arms below
1308 // silently DROP `addr.offset_reg` — a runtime address index vanished,
1309 // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1310 // to the wrong address). Compute the effective base into IP and re-encode
1311 // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1312 if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1313 return Ok(bytes);
1314 }
1315 // #594: call_indirect was encoded as a literal NOP on the A32 path
1316 // (`--target cortex-r5`) — the call never happened and the function
1317 // silently returned garbage. Emit the same three-instruction expansion
1318 // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1319 // MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1320 if let ArmOp::CallIndirect {
1321 table_index_reg,
1322 table_size,
1323 table_byte_offset,
1324 null_check,
1325 type_check,
1326 ..
1327 } = op
1328 {
1329 return Ok(Self::encode_arm_call_indirect(
1330 table_index_reg,
1331 *table_size,
1332 *table_byte_offset,
1333 *null_check,
1334 *type_check,
1335 ));
1336 }
1337 let instr: u32 = match op {
1338 // Data processing instructions
1339 ArmOp::Add { rd, rn, op2 } => {
1340 let rd_bits = reg_to_bits(rd);
1341 let rn_bits = reg_to_bits(rn);
1342 let (op2_bits, i_flag) = encode_operand2(op2)?;
1343
1344 // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1345 0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1346 | (i_flag << 25)
1347 | (rn_bits << 16)
1348 | (rd_bits << 12)
1349 | op2_bits
1350 }
1351
1352 ArmOp::Sub { rd, rn, op2 } => {
1353 let rd_bits = reg_to_bits(rd);
1354 let rn_bits = reg_to_bits(rn);
1355 let (op2_bits, i_flag) = encode_operand2(op2)?;
1356
1357 // SUB encoding: opcode=0010
1358 0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1359 }
1360
1361 // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1362 ArmOp::Adds { rd, rn, op2 } => {
1363 let rd_bits = reg_to_bits(rd);
1364 let rn_bits = reg_to_bits(rn);
1365 let (op2_bits, i_flag) = encode_operand2(op2)?;
1366
1367 // ADDS encoding: opcode=0100, S=1
1368 0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1369 }
1370
1371 ArmOp::Adc { rd, rn, op2 } => {
1372 let rd_bits = reg_to_bits(rd);
1373 let rn_bits = reg_to_bits(rn);
1374 let (op2_bits, i_flag) = encode_operand2(op2)?;
1375
1376 // ADC encoding: opcode=0101
1377 0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1378 }
1379
1380 ArmOp::Subs { rd, rn, op2 } => {
1381 let rd_bits = reg_to_bits(rd);
1382 let rn_bits = reg_to_bits(rn);
1383 let (op2_bits, i_flag) = encode_operand2(op2)?;
1384
1385 // SUBS encoding: opcode=0010, S=1
1386 0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1387 }
1388
1389 ArmOp::Sbc { rd, rn, op2 } => {
1390 let rd_bits = reg_to_bits(rd);
1391 let rn_bits = reg_to_bits(rn);
1392 let (op2_bits, i_flag) = encode_operand2(op2)?;
1393
1394 // SBC encoding: opcode=0110
1395 0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1396 }
1397
1398 ArmOp::Mul { rd, rn, rm } => {
1399 let rd_bits = reg_to_bits(rd);
1400 let rn_bits = reg_to_bits(rn);
1401 let rm_bits = reg_to_bits(rm);
1402
1403 // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1404 0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1405 }
1406
1407 ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1408 let rdlo_bits = reg_to_bits(rdlo);
1409 let rdhi_bits = reg_to_bits(rdhi);
1410 let rn_bits = reg_to_bits(rn);
1411 let rm_bits = reg_to_bits(rm);
1412
1413 // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1414 0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1415 }
1416
1417 ArmOp::Sdiv { rd, rn, rm } => {
1418 let rd_bits = reg_to_bits(rd);
1419 let rn_bits = reg_to_bits(rn);
1420 let rm_bits = reg_to_bits(rm);
1421
1422 // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1423 // ARMv7-M and above
1424 0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1425 }
1426
1427 ArmOp::Udiv { rd, rn, rm } => {
1428 let rd_bits = reg_to_bits(rd);
1429 let rn_bits = reg_to_bits(rn);
1430 let rm_bits = reg_to_bits(rm);
1431
1432 // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1433 // ARMv7-M and above
1434 0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1435 }
1436
1437 ArmOp::Mls { 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 // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1444 // Rd = Ra - (Rn * Rm)
1445 0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1446 }
1447
1448 ArmOp::Mla { rd, rn, rm, ra } => {
1449 let rd_bits = reg_to_bits(rd);
1450 let rn_bits = reg_to_bits(rn);
1451 let rm_bits = reg_to_bits(rm);
1452 let ra_bits = reg_to_bits(ra);
1453
1454 // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1455 // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1456 0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1457 }
1458
1459 ArmOp::And { rd, rn, op2 } => {
1460 let rd_bits = reg_to_bits(rd);
1461 let rn_bits = reg_to_bits(rn);
1462 let (op2_bits, i_flag) = encode_operand2(op2)?;
1463
1464 // AND encoding: opcode=0000
1465 0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1466 }
1467
1468 ArmOp::Orr { rd, rn, op2 } => {
1469 let rd_bits = reg_to_bits(rd);
1470 let rn_bits = reg_to_bits(rn);
1471 let (op2_bits, i_flag) = encode_operand2(op2)?;
1472
1473 // ORR encoding: opcode=1100
1474 0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1475 }
1476
1477 ArmOp::Eor { rd, rn, op2 } => {
1478 let rd_bits = reg_to_bits(rd);
1479 let rn_bits = reg_to_bits(rn);
1480 let (op2_bits, i_flag) = encode_operand2(op2)?;
1481
1482 // EOR encoding: opcode=0001
1483 0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1484 }
1485
1486 // Shift instructions
1487 ArmOp::Lsl { rd, rn, shift } => {
1488 let rd_bits = reg_to_bits(rd);
1489 let rn_bits = reg_to_bits(rn);
1490 let shift_bits = *shift & 0x1F;
1491
1492 // LSL encoding: MOV with shift
1493 0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1494 }
1495
1496 ArmOp::Lsr { rd, rn, shift } => {
1497 let rd_bits = reg_to_bits(rd);
1498 let rn_bits = reg_to_bits(rn);
1499 let shift_bits = *shift & 0x1F;
1500
1501 // LSR encoding
1502 0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1503 }
1504
1505 ArmOp::Asr { rd, rn, shift } => {
1506 let rd_bits = reg_to_bits(rd);
1507 let rn_bits = reg_to_bits(rn);
1508 let shift_bits = *shift & 0x1F;
1509
1510 // ASR encoding
1511 0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1512 }
1513
1514 ArmOp::Ror { rd, rn, shift } => {
1515 let rd_bits = reg_to_bits(rd);
1516 let rn_bits = reg_to_bits(rn);
1517 let shift_bits = *shift & 0x1F;
1518
1519 // ROR encoding: MOV with ROR shift
1520 0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1521 }
1522
1523 // Register-based shifts (ARM32)
1524 // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1525 ArmOp::LslReg { rd, rn, rm } => {
1526 let rd_bits = reg_to_bits(rd);
1527 let rn_bits = reg_to_bits(rn);
1528 let rm_bits = reg_to_bits(rm);
1529 0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1530 }
1531 ArmOp::LsrReg { rd, rn, rm } => {
1532 let rd_bits = reg_to_bits(rd);
1533 let rn_bits = reg_to_bits(rn);
1534 let rm_bits = reg_to_bits(rm);
1535 0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1536 }
1537 ArmOp::AsrReg { rd, rn, rm } => {
1538 let rd_bits = reg_to_bits(rd);
1539 let rn_bits = reg_to_bits(rn);
1540 let rm_bits = reg_to_bits(rm);
1541 0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1542 }
1543 ArmOp::RorReg { rd, rn, rm } => {
1544 let rd_bits = reg_to_bits(rd);
1545 let rn_bits = reg_to_bits(rn);
1546 let rm_bits = reg_to_bits(rm);
1547 0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1548 }
1549
1550 // RSB (Reverse Subtract): Rd = imm - Rn
1551 ArmOp::Rsb { rd, rn, imm } => {
1552 let rd_bits = reg_to_bits(rd);
1553 let rn_bits = reg_to_bits(rn);
1554 // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1555 // Opcode for RSB = 0011, I=1 (immediate), S=0
1556 //
1557 // #681 class audit: the A32 imm12 is a rotate(4):imm8 modified
1558 // immediate; `*imm & 0xFF` silently encoded a WRONG constant
1559 // for imm > 0xFF (#378 masking class). All current emitters use
1560 // imm 32, so erroring here is byte-identical for real codegen.
1561 if *imm > 0xFF {
1562 return Err(synth_core::Error::synthesis(
1563 "A32 RSB immediate > 0xFF requires a rotated-immediate encoding \
1564 (not supported) — materialize into a register",
1565 ));
1566 }
1567 0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1568 }
1569
1570 // Bit manipulation instructions
1571 ArmOp::Clz { rd, rm } => {
1572 let rd_bits = reg_to_bits(rd);
1573 let rm_bits = reg_to_bits(rm);
1574
1575 // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1576 // ARMv5T and above
1577 0xE16F0F10 | (rd_bits << 12) | rm_bits
1578 }
1579
1580 ArmOp::Rbit { rd, rm } => {
1581 let rd_bits = reg_to_bits(rd);
1582 let rm_bits = reg_to_bits(rm);
1583
1584 // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1585 // ARMv6T2 and above
1586 0xE6FF0F30 | (rd_bits << 12) | rm_bits
1587 }
1588
1589 ArmOp::Sxtb { rd, rm } => {
1590 let rd_bits = reg_to_bits(rd);
1591 let rm_bits = reg_to_bits(rm);
1592
1593 // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1594 // ARMv6 and above. rotate=00 for no rotation
1595 0xE6AF0070 | (rd_bits << 12) | rm_bits
1596 }
1597
1598 ArmOp::Sxth { rd, rm } => {
1599 let rd_bits = reg_to_bits(rd);
1600 let rm_bits = reg_to_bits(rm);
1601
1602 // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1603 // ARMv6 and above. rotate=00 for no rotation
1604 0xE6BF0070 | (rd_bits << 12) | rm_bits
1605 }
1606
1607 ArmOp::Uxtb { rd, rm } => {
1608 let rd_bits = reg_to_bits(rd);
1609 let rm_bits = reg_to_bits(rm);
1610 // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1611 0xE6EF0070 | (rd_bits << 12) | rm_bits
1612 }
1613
1614 ArmOp::Uxth { rd, rm } => {
1615 let rd_bits = reg_to_bits(rd);
1616 let rm_bits = reg_to_bits(rm);
1617 // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1618 0xE6FF0070 | (rd_bits << 12) | rm_bits
1619 }
1620
1621 // Move instructions
1622 ArmOp::Mov { rd, op2 } => {
1623 let rd_bits = reg_to_bits(rd);
1624 let (op2_bits, i_flag) = encode_operand2(op2)?;
1625
1626 // MOV encoding: opcode=1101
1627 0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1628 }
1629
1630 ArmOp::Mvn { rd, op2 } => {
1631 let rd_bits = reg_to_bits(rd);
1632 let (op2_bits, i_flag) = encode_operand2(op2)?;
1633
1634 // MVN encoding: opcode=1111
1635 0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1636 }
1637
1638 // MOVW - Move Wide (ARM32)
1639 // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1640 ArmOp::Movw { rd, imm16 } => {
1641 let rd_bits = reg_to_bits(rd);
1642 let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1643 let imm12 = (*imm16 as u32) & 0xFFF;
1644 0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1645 }
1646
1647 // MOVT - Move Top (ARM32)
1648 // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1649 ArmOp::Movt { rd, imm16 } => {
1650 let rd_bits = reg_to_bits(rd);
1651 let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1652 let imm12 = (*imm16 as u32) & 0xFFF;
1653 0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1654 }
1655
1656 // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1657 // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1658 ArmOp::MovwSym { rd, addend, .. } => {
1659 let rd_bits = reg_to_bits(rd);
1660 let v = (*addend as u32) & 0xffff;
1661 0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1662 }
1663 ArmOp::MovtSym { rd, addend, .. } => {
1664 let rd_bits = reg_to_bits(rd);
1665 let v = ((*addend as u32) >> 16) & 0xffff;
1666 0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1667 }
1668
1669 // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1670 // not used for relocatable native-pointer objects; fail loudly rather
1671 // than miscompile if it is ever reached here.
1672 ArmOp::LdrSym { .. } => {
1673 return Err(synth_core::Error::synthesis(
1674 "LdrSym (literal-pool address load) is Thumb-2-only",
1675 ));
1676 }
1677
1678 // Compare
1679 ArmOp::Cmp { rn, op2 } => {
1680 let rn_bits = reg_to_bits(rn);
1681 let (op2_bits, i_flag) = encode_operand2(op2)?;
1682
1683 // CMP encoding: opcode=1010, S=1
1684 0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1685 }
1686
1687 // Compare Negative (CMN) - computes Rn + op2 and sets flags
1688 ArmOp::Cmn { rn, op2 } => {
1689 let rn_bits = reg_to_bits(rn);
1690 let (op2_bits, i_flag) = encode_operand2(op2)?;
1691
1692 // CMN encoding: opcode=1011, S=1
1693 0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1694 }
1695
1696 // Load/Store
1697 ArmOp::Ldr { rd, addr } => {
1698 let rd_bits = reg_to_bits(rd);
1699 let (base_bits, offset_bits) = encode_mem_addr(addr);
1700
1701 // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1702 // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1703 0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1704 }
1705
1706 ArmOp::Str { rd, addr } => {
1707 let rd_bits = reg_to_bits(rd);
1708 let (base_bits, offset_bits) = encode_mem_addr(addr);
1709
1710 // STR encoding: L=0 (store)
1711 0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1712 }
1713
1714 // Sub-word loads (ARM32 encoding)
1715 ArmOp::Ldrb { rd, addr } => {
1716 let rd_bits = reg_to_bits(rd);
1717 let (base_bits, offset_bits) = encode_mem_addr(addr);
1718 // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1719 0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1720 }
1721
1722 ArmOp::Ldrsb { rd, addr } => {
1723 let rd_bits = reg_to_bits(rd);
1724 let (base_bits, offset_bits) = encode_mem_addr(addr);
1725 // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1726 // Simplified with immediate offset
1727 let offset_val = offset_bits & 0xFF;
1728 let imm4h = (offset_val >> 4) & 0xF;
1729 let imm4l = offset_val & 0xF;
1730 0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1731 }
1732
1733 ArmOp::Ldrh { rd, addr } => {
1734 let rd_bits = reg_to_bits(rd);
1735 let (base_bits, offset_bits) = encode_mem_addr(addr);
1736 // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1737 let offset_val = offset_bits & 0xFF;
1738 let imm4h = (offset_val >> 4) & 0xF;
1739 let imm4l = offset_val & 0xF;
1740 0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1741 }
1742
1743 ArmOp::Ldrsh { rd, addr } => {
1744 let rd_bits = reg_to_bits(rd);
1745 let (base_bits, offset_bits) = encode_mem_addr(addr);
1746 // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1747 let offset_val = offset_bits & 0xFF;
1748 let imm4h = (offset_val >> 4) & 0xF;
1749 let imm4l = offset_val & 0xF;
1750 0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1751 }
1752
1753 // Sub-word stores (ARM32 encoding)
1754 ArmOp::Strb { rd, addr } => {
1755 let rd_bits = reg_to_bits(rd);
1756 let (base_bits, offset_bits) = encode_mem_addr(addr);
1757 // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1758 0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1759 }
1760
1761 ArmOp::Strh { rd, addr } => {
1762 let rd_bits = reg_to_bits(rd);
1763 let (base_bits, offset_bits) = encode_mem_addr(addr);
1764 // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1765 let offset_val = offset_bits & 0xFF;
1766 let imm4h = (offset_val >> 4) & 0xF;
1767 let imm4l = offset_val & 0xF;
1768 0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1769 }
1770
1771 // Memory management (ARM32 encoding)
1772 ArmOp::MemorySize { rd } => {
1773 let rd_bits = reg_to_bits(rd);
1774 // MOV rd, R10, LSR #16 (memory size in bytes / 65536 = pages)
1775 // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1776 // LSR #16: shift5=10000, type=01
1777 0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1778 }
1779
1780 ArmOp::MemoryGrow { rd, .. } => {
1781 let rd_bits = reg_to_bits(rd);
1782 // On embedded, always fail: MOV rd, #-1
1783 0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1784 }
1785
1786 // Label pseudo-instruction: emits no machine code
1787 ArmOp::Label { .. } => {
1788 return Ok(Vec::new());
1789 }
1790
1791 // Branch instructions
1792 ArmOp::B { label: _ } => {
1793 // B encoding: cond(4) | 1010 | offset(24)
1794 // Simplified: branch to offset 0 (will be patched by linker/resolver)
1795 0xEA000000
1796 }
1797
1798 // Conditional branch to label (generic)
1799 ArmOp::Bcc { cond, label: _ } => {
1800 use synth_synthesis::Condition;
1801 let cond_bits: u32 = match cond {
1802 Condition::EQ => 0x0,
1803 Condition::NE => 0x1,
1804 Condition::HS => 0x2,
1805 Condition::LO => 0x3,
1806 Condition::HI => 0x8,
1807 Condition::LS => 0x9,
1808 Condition::GE => 0xA,
1809 Condition::LT => 0xB,
1810 Condition::GT => 0xC,
1811 Condition::LE => 0xD,
1812 };
1813 // B<cond> with offset 0 (will be patched)
1814 (cond_bits << 28) | 0x0A000000
1815 }
1816
1817 // BHS (Branch if Higher or Same) - used for bounds checking
1818 ArmOp::Bhs { label: _ } => {
1819 // BHS encoding: cond(2=HS) | 1010 | offset(24)
1820 0x2A000000 // BHS with offset 0
1821 }
1822
1823 // BLO (Branch if Lower) - complementary to BHS
1824 ArmOp::Blo { label: _ } => {
1825 // BLO encoding: cond(3=LO) | 1010 | offset(24)
1826 0x3A000000 // BLO with offset 0
1827 }
1828
1829 // Branch with numeric offset (in instructions)
1830 // ARM32 B instruction: offset is in instructions, stored as words
1831 // The offset is relative to PC+8 (due to ARM pipeline)
1832 ArmOp::BOffset { offset } => {
1833 // B encoding: cond(4) | 1010 | offset(24)
1834 // Offset is signed, in words (4-byte units)
1835 // ARM adds PC+8 to the offset, so we need to adjust:
1836 // target = PC + 8 + (offset * 4)
1837 // For backward branch of N instructions: offset = -(N + 2)
1838 // wrapping_sub keeps the encoder total under fuzzing (#186): an
1839 // extreme i32::MIN offset would otherwise overflow-panic; for any
1840 // real branch offset this is identical to `- 2`.
1841 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1842 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1843 0xEA000000 | offset_bits
1844 }
1845
1846 // Conditional branch with numeric offset
1847 ArmOp::BCondOffset { cond, offset } => {
1848 use synth_synthesis::Condition;
1849 let cond_bits: u32 = match cond {
1850 Condition::EQ => 0x0,
1851 Condition::NE => 0x1,
1852 Condition::HS => 0x2,
1853 Condition::LO => 0x3,
1854 Condition::HI => 0x8,
1855 Condition::LS => 0x9,
1856 Condition::GE => 0xA,
1857 Condition::LT => 0xB,
1858 Condition::GT => 0xC,
1859 Condition::LE => 0xD,
1860 };
1861 // B<cond> encoding: cond(4) | 1010 | offset(24)
1862 // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1863 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1864 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1865 (cond_bits << 28) | 0x0A000000 | offset_bits
1866 }
1867
1868 ArmOp::Bl { label: _ } => {
1869 // BL encoding: cond(4) | 1011 | offset(24). Relocatable
1870 // placeholder; an R_ARM_CALL relocation patches the target.
1871 //
1872 // #1040: the placeholder must carry an embedded addend of -8,
1873 // not 0. A32 `BL` computes `target = P + 8 + (imm24 << 2)`, so
1874 // under REL semantics a 0 addend (`eb000000`) resolves two
1875 // instructions PAST the callee entry — the A32 twin of the
1876 // Thumb #174 bug. The correct word is what `gas` emits for
1877 // `bl <extern>` in ARM mode:
1878 // ebfffffe -> `bl <self>` (imm24 = -2, offset = -8),
1879 // which nets to exactly S. Verified against
1880 // `arm-none-eabi-as -march=armv7-r`, which emits `ebfffffe`
1881 // with an R_ARM_CALL relocation.
1882 0xEBFFFFFE
1883 }
1884
1885 ArmOp::Bx { rm } => {
1886 let rm_bits = reg_to_bits(rm);
1887
1888 // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1889 0xE12FFF10 | rm_bits
1890 }
1891
1892 ArmOp::Blx { rm } => {
1893 let rm_bits = reg_to_bits(rm);
1894
1895 // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1896 0xE12FFF30 | rm_bits
1897 }
1898
1899 ArmOp::Push { regs } => {
1900 // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1901 let mut reg_list: u32 = 0;
1902 for r in regs {
1903 reg_list |= 1 << reg_to_bits(r);
1904 }
1905 0xE92D0000 | reg_list
1906 }
1907
1908 ArmOp::Pop { regs } => {
1909 // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1910 let mut reg_list: u32 = 0;
1911 for r in regs {
1912 reg_list |= 1 << reg_to_bits(r);
1913 }
1914 0xE8BD0000 | reg_list
1915 }
1916
1917 ArmOp::Nop => {
1918 // NOP encoding: MOV R0, R0
1919 0xE1A00000
1920 }
1921
1922 ArmOp::Udf { imm } => {
1923 // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1924 // We only use imm8, so split into imm4_hi and imm4_lo
1925 let imm8 = *imm as u32;
1926 0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1927 }
1928
1929 // #615: handled by the `encode_arm_expanded` early return at the
1930 // top of this function — a real MOV{cond}/MOV pair now, never a
1931 // silent NOP again.
1932 ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1933 unreachable!("handled by encode_arm_expanded (#615)")
1934 }
1935
1936 // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1937 // models these, but NO codegen path constructs them (the selector
1938 // lowers select/locals/globals/br_table/call to real instruction
1939 // sequences before the encoder). Encoding one as a NOP silently
1940 // dropped the operation (#615 class); a typed Err keeps the
1941 // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1942 // while making any future reachability LOUD.
1943 ArmOp::Select { .. }
1944 | ArmOp::LocalGet { .. }
1945 | ArmOp::LocalSet { .. }
1946 | ArmOp::LocalTee { .. }
1947 | ArmOp::GlobalGet { .. }
1948 | ArmOp::GlobalSet { .. }
1949 | ArmOp::BrTable { .. }
1950 | ArmOp::Call { .. } => {
1951 return Err(synth_core::Error::synthesis(format!(
1952 "verification-only pseudo-op {op:?} reached the A32 encoder — \
1953 codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1954 )));
1955 }
1956
1957 // #594: CallIndirect is expanded to a real multi-instruction
1958 // sequence by the early return at the top of this function —
1959 // it must NEVER fall through to a silent NOP again.
1960 ArmOp::CallIndirect { .. } => {
1961 unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1962 }
1963
1964 // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1965 // multi-instruction sequence by `encode_arm_expanded` — the
1966 // "encode as NOP for now" era ended with the value silently
1967 // vanishing on `--target cortex-r5`.
1968 ArmOp::I64Add { .. }
1969 | ArmOp::I64Sub { .. }
1970 | ArmOp::I64DivS { .. }
1971 | ArmOp::I64DivU { .. }
1972 | ArmOp::I64RemS { .. }
1973 | ArmOp::I64RemU { .. }
1974 | ArmOp::I64Clz { .. }
1975 | ArmOp::I64Ctz { .. }
1976 | ArmOp::I64Popcnt { .. }
1977 | ArmOp::I64And { .. }
1978 | ArmOp::I64Or { .. }
1979 | ArmOp::I64Xor { .. }
1980 | ArmOp::I64Eqz { .. }
1981 | ArmOp::I64Eq { .. }
1982 | ArmOp::I64Ne { .. }
1983 | ArmOp::I64LtS { .. }
1984 | ArmOp::I64LtU { .. }
1985 | ArmOp::I64LeS { .. }
1986 | ArmOp::I64LeU { .. }
1987 | ArmOp::I64GtS { .. }
1988 | ArmOp::I64GtU { .. }
1989 | ArmOp::I64GeS { .. }
1990 | ArmOp::I64GeU { .. }
1991 | ArmOp::I64Const { .. }
1992 | ArmOp::I64Ldr { .. }
1993 | ArmOp::I64Str { .. }
1994 | ArmOp::I64ExtendI32S { .. }
1995 | ArmOp::I64ExtendI32U { .. }
1996 | ArmOp::I64Extend8S { .. }
1997 | ArmOp::I64Extend16S { .. }
1998 | ArmOp::I64Extend32S { .. }
1999 | ArmOp::I32WrapI64 { .. } => {
2000 unreachable!("handled by encode_arm_expanded (#615)")
2001 }
2002
2003 // f32 VFP single-precision instructions
2004 ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
2005 ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
2006 ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
2007 ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
2008 ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
2009 ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
2010 ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
2011
2012 // f32 pseudo-ops — multi-instruction sequences
2013 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2014 ArmOp::F32Ceil { sd, sm } => {
2015 return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
2016 }
2017 ArmOp::F32Floor { sd, sm } => {
2018 return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
2019 }
2020 ArmOp::F32Trunc { sd, sm } => {
2021 return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2022 }
2023 ArmOp::F32Nearest { sd, sm } => {
2024 return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2025 }
2026 ArmOp::F32Min { sd, sn, sm } => {
2027 return self.encode_arm_f32_minmax(sd, sn, sm, true);
2028 }
2029 ArmOp::F32Max { sd, sn, sm } => {
2030 return self.encode_arm_f32_minmax(sd, sn, sm, false);
2031 }
2032 ArmOp::F32Copysign { sd, sn, sm } => {
2033 return self.encode_arm_f32_copysign(sd, sn, sm);
2034 }
2035
2036 // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2037 ArmOp::F32Eq { rd, sn, sm } => {
2038 return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2039 }
2040 ArmOp::F32Ne { rd, sn, sm } => {
2041 return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2042 }
2043 ArmOp::F32Lt { rd, sn, sm } => {
2044 return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2045 }
2046 ArmOp::F32Le { rd, sn, sm } => {
2047 return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2048 }
2049 ArmOp::F32Gt { rd, sn, sm } => {
2050 return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2051 }
2052 ArmOp::F32Ge { rd, sn, sm } => {
2053 return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2054 }
2055
2056 // f32 const — multi-instruction: MOVW + MOVT + VMOV
2057 ArmOp::F32Const { sd, value } => {
2058 return self.encode_arm_f32_const(sd, *value);
2059 }
2060
2061 ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2062 ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2063
2064 // f32 conversions — multi-instruction sequences
2065 ArmOp::F32ConvertI32S { sd, rm } => {
2066 return self.encode_arm_f32_convert_i32(sd, rm, true);
2067 }
2068 ArmOp::F32ConvertI32U { sd, rm } => {
2069 return self.encode_arm_f32_convert_i32(sd, rm, false);
2070 }
2071 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2072 return Err(synth_core::Error::synthesis(
2073 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2074 ));
2075 }
2076 ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2077 ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2078 ArmOp::I32TruncF32S { rd, sm } => {
2079 return self.encode_arm_i32_trunc_f32(rd, sm, true);
2080 }
2081 ArmOp::I32TruncF32U { rd, sm } => {
2082 return self.encode_arm_i32_trunc_f32(rd, sm, false);
2083 }
2084
2085 // f64 VFP double-precision instructions (ARM32)
2086 // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2087 ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2088 ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2089 ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2090 ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2091 ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2092 ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2093 ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2094
2095 // f64 pseudo-ops
2096 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2097 ArmOp::F64Ceil { dd, dm } => {
2098 return self.encode_arm_f64_rounding(dd, dm, 0b01);
2099 }
2100 ArmOp::F64Floor { dd, dm } => {
2101 return self.encode_arm_f64_rounding(dd, dm, 0b10);
2102 }
2103 ArmOp::F64Trunc { dd, dm } => {
2104 return self.encode_arm_f64_rounding(dd, dm, 0b11);
2105 }
2106 ArmOp::F64Nearest { dd, dm } => {
2107 return self.encode_arm_f64_rounding(dd, dm, 0b00);
2108 }
2109 ArmOp::F64Min { dd, dn, dm } => {
2110 return self.encode_arm_f64_minmax(dd, dn, dm, true);
2111 }
2112 ArmOp::F64Max { dd, dn, dm } => {
2113 return self.encode_arm_f64_minmax(dd, dn, dm, false);
2114 }
2115 ArmOp::F64Copysign { dd, dn, dm } => {
2116 return self.encode_arm_f64_copysign(dd, dn, dm);
2117 }
2118
2119 // f64 comparisons
2120 ArmOp::F64Eq { rd, dn, dm } => {
2121 return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2122 }
2123 ArmOp::F64Ne { rd, dn, dm } => {
2124 return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2125 }
2126 ArmOp::F64Lt { rd, dn, dm } => {
2127 return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2128 }
2129 ArmOp::F64Le { rd, dn, dm } => {
2130 return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2131 }
2132 ArmOp::F64Gt { rd, dn, dm } => {
2133 return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2134 }
2135 ArmOp::F64Ge { rd, dn, dm } => {
2136 return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2137 }
2138
2139 ArmOp::F64Const { dd, value } => {
2140 return self.encode_arm_f64_const(dd, *value);
2141 }
2142
2143 ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2144 ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2145
2146 ArmOp::F64ConvertI32S { dd, rm } => {
2147 return self.encode_arm_f64_convert_i32(dd, rm, true);
2148 }
2149 ArmOp::F64ConvertI32U { dd, rm } => {
2150 return self.encode_arm_f64_convert_i32(dd, rm, false);
2151 }
2152 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2153 return Err(synth_core::Error::synthesis(
2154 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2155 ));
2156 }
2157 ArmOp::F64PromoteF32 { dd, sm } => {
2158 return self.encode_arm_f64_promote_f32(dd, sm);
2159 }
2160 // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2161 // has none — the selector declines every float op there), so the
2162 // A32 encoder refuses loudly instead of shipping an untested
2163 // encoding (#615: never a silent wrong byte).
2164 ArmOp::F32DemoteF64 { .. } => {
2165 return Err(synth_core::Error::synthesis(
2166 "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2167 ));
2168 }
2169 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2170 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2171 }
2172 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2173 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2174 }
2175 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2176 return Err(synth_core::Error::synthesis(
2177 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2178 ));
2179 }
2180 ArmOp::I32TruncF64S { rd, dm } => {
2181 return self.encode_arm_i32_trunc_f64(rd, dm, true);
2182 }
2183 ArmOp::I32TruncF64U { rd, dm } => {
2184 return self.encode_arm_i32_trunc_f64(rd, dm, false);
2185 }
2186 // #615: multi-instruction i64 sequences — expanded to real A32 by
2187 // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2188 ArmOp::I64SetCond { .. }
2189 | ArmOp::I64SetCondZ { .. }
2190 | ArmOp::I64Mul { .. }
2191 | ArmOp::I64Shl { .. }
2192 | ArmOp::I64ShrS { .. }
2193 | ArmOp::I64ShrU { .. }
2194 | ArmOp::I64Rotl { .. }
2195 | ArmOp::I64Rotr { .. } => {
2196 unreachable!("handled by encode_arm_expanded (#615)")
2197 }
2198
2199 // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2200 ArmOp::MveLoad { .. }
2201 | ArmOp::MveStore { .. }
2202 | ArmOp::MveConst { .. }
2203 | ArmOp::MveAnd { .. }
2204 | ArmOp::MveOrr { .. }
2205 | ArmOp::MveEor { .. }
2206 | ArmOp::MveMvn { .. }
2207 | ArmOp::MveBic { .. }
2208 | ArmOp::MveAddI { .. }
2209 | ArmOp::MveSubI { .. }
2210 | ArmOp::MveMulI { .. }
2211 | ArmOp::MveNegI { .. }
2212 | ArmOp::MveCmpEqI { .. }
2213 | ArmOp::MveCmpNeI { .. }
2214 | ArmOp::MveCmpLtS { .. }
2215 | ArmOp::MveCmpLtU { .. }
2216 | ArmOp::MveCmpGtS { .. }
2217 | ArmOp::MveCmpGtU { .. }
2218 | ArmOp::MveCmpLeS { .. }
2219 | ArmOp::MveCmpLeU { .. }
2220 | ArmOp::MveCmpGeS { .. }
2221 | ArmOp::MveCmpGeU { .. }
2222 | ArmOp::MveDup { .. }
2223 | ArmOp::MveExtractLane { .. }
2224 | ArmOp::MveInsertLane { .. }
2225 | ArmOp::MveAddF32 { .. }
2226 | ArmOp::MveSubF32 { .. }
2227 | ArmOp::MveMulF32 { .. }
2228 | ArmOp::MveNegF32 { .. }
2229 | ArmOp::MveAbsF32 { .. }
2230 | ArmOp::MveCmpEqF32 { .. }
2231 | ArmOp::MveCmpNeF32 { .. }
2232 | ArmOp::MveCmpLtF32 { .. }
2233 | ArmOp::MveCmpLeF32 { .. }
2234 | ArmOp::MveCmpGtF32 { .. }
2235 | ArmOp::MveCmpGeF32 { .. }
2236 | ArmOp::MveDupF32 { .. }
2237 | ArmOp::MveExtractLaneF32 { .. }
2238 | ArmOp::MveReplaceLaneF32 { .. }
2239 | ArmOp::MveDivF32 { .. }
2240 | ArmOp::MveSqrtF32 { .. } => {
2241 // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2242 // is no A32 encoding. The selector only emits MVE ops for
2243 // Thumb targets — a NOP here silently dropped the vector op
2244 // if that invariant ever broke (#615 class). Err keeps the
2245 // encoder total and the failure loud.
2246 return Err(synth_core::Error::synthesis(format!(
2247 "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2248 )));
2249 }
2250 };
2251
2252 // ARM32 instructions are little-endian
2253 Ok(instr.to_le_bytes().to_vec())
2254 }
2255
2256 // === ARM32 VFP multi-instruction helpers ===
2257
2258 /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2259 fn encode_arm_f32_compare(
2260 &self,
2261 rd: &Reg,
2262 sn: &VfpReg,
2263 sm: &VfpReg,
2264 cond_code: u32,
2265 ) -> Result<Vec<u8>> {
2266 let mut bytes = Vec::new();
2267
2268 // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2269 let sn_num = vfp_sreg_to_num(sn)?;
2270 let sm_num = vfp_sreg_to_num(sm)?;
2271 let (vd, d) = encode_sreg(sn_num);
2272 let (vm, m) = encode_sreg(sm_num);
2273 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2274 bytes.extend_from_slice(&vcmp.to_le_bytes());
2275
2276 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2277 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2278
2279 // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2280 let rd_bits = reg_to_bits(rd);
2281 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2282 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2283
2284 // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2285 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2286 bytes.extend_from_slice(&mov_one.to_le_bytes());
2287
2288 Ok(bytes)
2289 }
2290
2291 /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2292 fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2293 let mut bytes = Vec::new();
2294 let bits = value.to_bits();
2295
2296 // Use R12 as temp register for constant loading
2297 let rt: u32 = 12; // R12/IP
2298
2299 // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2300 let lo16 = bits & 0xFFFF;
2301 let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2302 bytes.extend_from_slice(&movw.to_le_bytes());
2303
2304 // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2305 let hi16 = (bits >> 16) & 0xFFFF;
2306 let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2307 bytes.extend_from_slice(&movt.to_le_bytes());
2308
2309 // VMOV Sd, R12
2310 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2311 bytes.extend_from_slice(&vmov.to_le_bytes());
2312
2313 Ok(bytes)
2314 }
2315
2316 /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2317 fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2318 let mut bytes = Vec::new();
2319
2320 // VMOV Sd, Rm — move integer to VFP register
2321 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2322 bytes.extend_from_slice(&vmov.to_le_bytes());
2323
2324 // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2325 // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2326 // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2327 // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2328 // the two were previously swapped, silently making `convert_i32_s`
2329 // an unsigned conversion).
2330 let sd_num = vfp_sreg_to_num(sd)?;
2331 let (vd, d) = encode_sreg(sd_num);
2332 let (vm, m) = encode_sreg(sd_num); // same register as source
2333 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2334 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2335 bytes.extend_from_slice(&vcvt.to_le_bytes());
2336
2337 Ok(bytes)
2338 }
2339
2340 /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2341 /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2342 /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2343 /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2344 /// Simplified: convert to int (toward zero for trunc) then back to float.
2345 /// Encode F32 rounding as ARM32.
2346 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2347 ///
2348 /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2349 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2350 /// which honours FPSCR rmode), then restores FPSCR.
2351 fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2352 let mut bytes = Vec::new();
2353 let sm_num = vfp_sreg_to_num(sm)?;
2354 let sd_num = vfp_sreg_to_num(sd)?;
2355 let (vd_s, d_s) = encode_sreg(sd_num);
2356 let (vm_s, m_s) = encode_sreg(sm_num);
2357
2358 if mode == 0b11 {
2359 // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2360 // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2361 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2362 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2363 } else {
2364 // ceil/floor/nearest: manipulate FPSCR rounding mode
2365 let rt: u32 = 12; // R12/IP as temp
2366
2367 // VMRS R12, FPSCR
2368 let vmrs = 0xEEF10A10 | (rt << 12);
2369 bytes.extend_from_slice(&vmrs.to_le_bytes());
2370
2371 // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2372 // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2373 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2374 bytes.extend_from_slice(&bic.to_le_bytes());
2375
2376 // ORR R12, R12, #(mode << 22) — set desired rounding mode
2377 if mode != 0 {
2378 // mode<<22: rotation=5, imm8=mode
2379 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2380 bytes.extend_from_slice(&orr.to_le_bytes());
2381 }
2382
2383 // VMSR FPSCR, R12
2384 let vmsr = 0xEEE10A10 | (rt << 12);
2385 bytes.extend_from_slice(&vmsr.to_le_bytes());
2386
2387 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2388 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2389 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2390
2391 // Restore FPSCR: clear rmode bits back to nearest (default)
2392 bytes.extend_from_slice(&vmrs.to_le_bytes());
2393 bytes.extend_from_slice(&bic.to_le_bytes());
2394 bytes.extend_from_slice(&vmsr.to_le_bytes());
2395 }
2396
2397 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2398 let (vd2, d2) = encode_sreg(sd_num);
2399 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2400 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2401
2402 Ok(bytes)
2403 }
2404
2405 /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2406 fn encode_arm_f32_minmax(
2407 &self,
2408 sd: &VfpReg,
2409 sn: &VfpReg,
2410 sm: &VfpReg,
2411 is_min: bool,
2412 ) -> Result<Vec<u8>> {
2413 let mut bytes = Vec::new();
2414 let sn_num = vfp_sreg_to_num(sn)?;
2415 let sm_num = vfp_sreg_to_num(sm)?;
2416 let sd_num = vfp_sreg_to_num(sd)?;
2417
2418 // VMOV Sd, Sn (start with first operand)
2419 let (vd, d) = encode_sreg(sd_num);
2420 let (vn, n) = encode_sreg(sn_num);
2421 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2422 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2423
2424 // VCMP.F32 Sn, Sm
2425 let (vm, m) = encode_sreg(sm_num);
2426 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2427 bytes.extend_from_slice(&vcmp.to_le_bytes());
2428
2429 // VMRS APSR_nzcv, FPSCR
2430 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2431
2432 // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2433 // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2434 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2435
2436 // VMOV{cond} Sd, Sm — conditional VMOV
2437 let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2438 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2439
2440 Ok(bytes)
2441 }
2442
2443 /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2444 fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2445 let mut bytes = Vec::new();
2446
2447 // VMOV R12, Sm (get sign source bits)
2448 let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2449 bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2450
2451 // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2452 let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2453 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2454
2455 // AND R12, R12, #0x80000000 (keep only sign bit)
2456 // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2457 // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2458 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2459 bytes.extend_from_slice(&and_sign.to_le_bytes());
2460
2461 // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2462 // R0 = register 0, so Rn and Rd fields are 0
2463 let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2464 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2465
2466 // ORR R0, R0, R12 (combine sign + magnitude)
2467 // R0 = register 0, so Rn and Rd fields are 0
2468 let orr = 0xE1800000u32 | 12;
2469 bytes.extend_from_slice(&orr.to_le_bytes());
2470
2471 // VMOV Sd, R0
2472 let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2473 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2474
2475 Ok(bytes)
2476 }
2477
2478 /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2479 fn encode_arm_f64_compare(
2480 &self,
2481 rd: &Reg,
2482 dn: &VfpReg,
2483 dm: &VfpReg,
2484 cond_code: u32,
2485 ) -> Result<Vec<u8>> {
2486 let mut bytes = Vec::new();
2487
2488 // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2489 let dn_num = vfp_dreg_to_num(dn)?;
2490 let dm_num = vfp_dreg_to_num(dm)?;
2491 let (vd, d) = encode_dreg(dn_num);
2492 let (vm, m) = encode_dreg(dm_num);
2493 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2494 bytes.extend_from_slice(&vcmp.to_le_bytes());
2495
2496 // VMRS APSR_nzcv, FPSCR
2497 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2498
2499 // MOV rd, #0
2500 let rd_bits = reg_to_bits(rd);
2501 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2502 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2503
2504 // MOVcond rd, #1
2505 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2506 bytes.extend_from_slice(&mov_one.to_le_bytes());
2507
2508 Ok(bytes)
2509 }
2510
2511 /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2512 fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2513 let mut bytes = Vec::new();
2514 let bits = value.to_bits();
2515 let lo32 = bits as u32;
2516 let hi32 = (bits >> 32) as u32;
2517
2518 // Load low 32 bits into R0 (Rd field = 0 for R0)
2519 let lo16 = lo32 & 0xFFFF;
2520 let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2521 bytes.extend_from_slice(&movw_r0.to_le_bytes());
2522 let hi16 = (lo32 >> 16) & 0xFFFF;
2523 let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2524 bytes.extend_from_slice(&movt_r0.to_le_bytes());
2525
2526 // Load high 32 bits into R12
2527 let lo16 = hi32 & 0xFFFF;
2528 let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2529 bytes.extend_from_slice(&movw_r12.to_le_bytes());
2530 let hi16 = (hi32 >> 16) & 0xFFFF;
2531 let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2532 bytes.extend_from_slice(&movt_r12.to_le_bytes());
2533
2534 // VMOV Dd, R0, R12
2535 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2536 bytes.extend_from_slice(&vmov.to_le_bytes());
2537
2538 Ok(bytes)
2539 }
2540
2541 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2542 fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2543 let mut bytes = Vec::new();
2544
2545 // Use S0 as intermediate: VMOV S0, Rm
2546 let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2547 bytes.extend_from_slice(&vmov.to_le_bytes());
2548
2549 // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2550 // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2551 let dd_num = vfp_dreg_to_num(dd)?;
2552 let (vd, d) = encode_dreg(dd_num);
2553 let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2554 // S0 is register 0: Vm=0, M=0
2555 let vcvt = base | (d << 22) | (vd << 12);
2556 bytes.extend_from_slice(&vcvt.to_le_bytes());
2557
2558 Ok(bytes)
2559 }
2560
2561 /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2562 fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2563 let dd_num = vfp_dreg_to_num(dd)?;
2564 let sm_num = vfp_sreg_to_num(sm)?;
2565 let (vd, d) = encode_dreg(dd_num);
2566 let (vm, m) = encode_sreg(sm_num);
2567
2568 // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2569 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2570 Ok(vcvt.to_le_bytes().to_vec())
2571 }
2572
2573 /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2574 fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2575 let mut bytes = Vec::new();
2576 let dm_num = vfp_dreg_to_num(dm)?;
2577 let (vm, m) = encode_dreg(dm_num);
2578
2579 // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2580 // S0: Vd=0, D=0
2581 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2582 let vcvt = base | (m << 5) | vm;
2583 bytes.extend_from_slice(&vcvt.to_le_bytes());
2584
2585 // VMOV Rd, S0
2586 let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2587 bytes.extend_from_slice(&vmov.to_le_bytes());
2588
2589 Ok(bytes)
2590 }
2591
2592 /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2593 /// Encode F64 rounding as ARM32.
2594 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2595 ///
2596 /// For trunc: uses VCVTR.S32.F64 (always truncates).
2597 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2598 /// then restores FPSCR.
2599 fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2600 let mut bytes = Vec::new();
2601 let dm_num = vfp_dreg_to_num(dm)?;
2602 let dd_num = vfp_dreg_to_num(dd)?;
2603 let (vm, m) = encode_dreg(dm_num);
2604 let (vd, d) = encode_dreg(dd_num);
2605
2606 if mode == 0b11 {
2607 // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2608 let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2609 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2610 } else {
2611 // ceil/floor/nearest: manipulate FPSCR rounding mode
2612 let rt: u32 = 12;
2613
2614 // VMRS R12, FPSCR
2615 let vmrs = 0xEEF10A10 | (rt << 12);
2616 bytes.extend_from_slice(&vmrs.to_le_bytes());
2617
2618 // BIC R12, R12, #(3 << 22)
2619 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2620 bytes.extend_from_slice(&bic.to_le_bytes());
2621
2622 // ORR R12, R12, #(mode << 22)
2623 if mode != 0 {
2624 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2625 bytes.extend_from_slice(&orr.to_le_bytes());
2626 }
2627
2628 // VMSR FPSCR, R12
2629 let vmsr = 0xEEE10A10 | (rt << 12);
2630 bytes.extend_from_slice(&vmsr.to_le_bytes());
2631
2632 // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2633 let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2634 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2635
2636 // Restore FPSCR
2637 bytes.extend_from_slice(&vmrs.to_le_bytes());
2638 bytes.extend_from_slice(&bic.to_le_bytes());
2639 bytes.extend_from_slice(&vmsr.to_le_bytes());
2640 }
2641
2642 // VCVT.F64.S32 Dd, S0 (convert back to double)
2643 let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2644 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2645
2646 Ok(bytes)
2647 }
2648
2649 /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2650 fn encode_arm_f64_minmax(
2651 &self,
2652 dd: &VfpReg,
2653 dn: &VfpReg,
2654 dm: &VfpReg,
2655 is_min: bool,
2656 ) -> Result<Vec<u8>> {
2657 let mut bytes = Vec::new();
2658 let dn_num = vfp_dreg_to_num(dn)?;
2659 let dm_num = vfp_dreg_to_num(dm)?;
2660 let dd_num = vfp_dreg_to_num(dd)?;
2661
2662 // VMOV.F64 Dd, Dn (start with first operand)
2663 let (vd, d) = encode_dreg(dd_num);
2664 let (vn, n) = encode_dreg(dn_num);
2665 let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2666 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2667
2668 // VCMP.F64 Dn, Dm
2669 let (vm, m) = encode_dreg(dm_num);
2670 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2671 bytes.extend_from_slice(&vcmp.to_le_bytes());
2672
2673 // VMRS APSR_nzcv, FPSCR
2674 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2675
2676 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2677 let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2678 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2679
2680 Ok(bytes)
2681 }
2682
2683 /// Encode F64 copysign as ARM32
2684 fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2685 let mut bytes = Vec::new();
2686
2687 // VMOV R0, R12, Dm (get sign source bits)
2688 let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2689 bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2690
2691 // VMOV R1, R2, Dn (get magnitude source bits)
2692 // We use R1 (lo) and R2 (hi) for the magnitude
2693 let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2694 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2695
2696 // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2697 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2698 bytes.extend_from_slice(&and_sign.to_le_bytes());
2699
2700 // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2701 let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2702 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2703
2704 // ORR R2, R2, R12 (combine sign + magnitude)
2705 let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2706 bytes.extend_from_slice(&orr.to_le_bytes());
2707
2708 // VMOV Dd, R1, R2
2709 let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2710 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2711
2712 Ok(bytes)
2713 }
2714
2715 /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2716 fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2717 let mut bytes = Vec::new();
2718
2719 // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2720 // We use Sm as both source and destination for the intermediate result
2721 let sm_num = vfp_sreg_to_num(sm)?;
2722 let (vd, d) = encode_sreg(sm_num);
2723 let (vm, m) = encode_sreg(sm_num);
2724 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2725 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2726 bytes.extend_from_slice(&vcvt.to_le_bytes());
2727
2728 // VMOV Rd, Sm — move result back to core register
2729 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2730 bytes.extend_from_slice(&vmov.to_le_bytes());
2731
2732 Ok(bytes)
2733 }
2734
2735 /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2736 fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2737 // Thumb-2 supports both 16-bit and 32-bit instructions
2738 // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2739 match op {
2740 // === 16-bit Thumb encodings ===
2741 ArmOp::Add { rd, rn, op2 } => {
2742 let rd_bits = reg_to_bits(rd) as u16;
2743 let rn_bits = reg_to_bits(rn) as u16;
2744
2745 if let Operand2::Reg(rm) = op2 {
2746 let rm_bits = reg_to_bits(rm) as u16;
2747 // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2748 // high registers (e.g. R12, the MemLoad/MemStore base
2749 // scratch) the bits overflow into adjacent fields, silently
2750 // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2751 // was emitted as `adds r4,r5,r1`. Guard on all three regs
2752 // being low and fall back to 32-bit ADD.W otherwise, exactly
2753 // as the Sub handler below does.
2754 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2755 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2756 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2757 Ok(instr.to_le_bytes().to_vec())
2758 } else {
2759 // ADD.W Rd, Rn, Rm (32-bit) for high registers
2760 self.encode_thumb32_add_reg_raw(
2761 rd_bits as u32,
2762 rn_bits as u32,
2763 rm_bits as u32,
2764 )
2765 }
2766 } else if let Operand2::Imm(imm) = op2 {
2767 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2768 // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2769 let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2770 Ok(instr.to_le_bytes().to_vec())
2771 } else {
2772 // Use 32-bit ADD for larger immediates
2773 self.encode_thumb32_add(rd, rn, *imm as u32)
2774 }
2775 } else {
2776 // Fallback to 32-bit encoding
2777 self.encode_thumb32_add(rd, rn, 0)
2778 }
2779 }
2780
2781 ArmOp::Sub { rd, rn, op2 } => {
2782 let rd_bits = reg_to_bits(rd) as u16;
2783 let rn_bits = reg_to_bits(rn) as u16;
2784
2785 if let Operand2::Reg(rm) = op2 {
2786 let rm_bits = reg_to_bits(rm) as u16;
2787 // 16-bit SUBS can only use low registers (R0-R7)
2788 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2789 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2790 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2791 Ok(instr.to_le_bytes().to_vec())
2792 } else {
2793 // Use 32-bit SUB.W for high registers
2794 self.encode_thumb32_sub_reg_raw(
2795 rd_bits as u32,
2796 rn_bits as u32,
2797 rm_bits as u32,
2798 )
2799 }
2800 } else if let Operand2::Imm(imm) = op2 {
2801 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2802 // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2803 let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2804 Ok(instr.to_le_bytes().to_vec())
2805 } else {
2806 self.encode_thumb32_sub(rd, rn, *imm as u32)
2807 }
2808 } else {
2809 self.encode_thumb32_sub(rd, rn, 0)
2810 }
2811 }
2812
2813 ArmOp::Mov { rd, op2 } => {
2814 let rd_bits = reg_to_bits(rd) as u16;
2815
2816 if let Operand2::Imm(imm) = op2 {
2817 // #498: the old test here was the SIGNED `*imm <= 255`,
2818 // so a negative immediate (e.g. -1) fell into the 16-bit
2819 // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2820 // #0xFF). A positive imm above 0xFFFF was equally wrong:
2821 // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2822 // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2823 // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2824 // produces the wide shape today (both selectors
2825 // materialize wide constants as explicit Movw/Movt or
2826 // Movw+Mvn), so this is byte-identical on shipped paths —
2827 // it retires the latent wrong-value encodings the
2828 // `estimator_encoder_agreement` oracle had pinned.
2829 let uimm = *imm as u32;
2830 if uimm <= 255 && rd_bits < 8 {
2831 // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2832 let imm_bits = (*imm as u16) & 0xFF;
2833 let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2834 Ok(instr.to_le_bytes().to_vec())
2835 } else if uimm <= 0xFFFF {
2836 // Use 32-bit MOVW for 16-bit immediates
2837 self.encode_thumb32_movw(rd, uimm)
2838 } else {
2839 // Full 32-bit value: MOVW low16 + MOVT high16
2840 let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2841 bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2842 Ok(bytes)
2843 }
2844 } else if let Operand2::Reg(rm) = op2 {
2845 let rm_bits = reg_to_bits(rm) as u16;
2846 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2847 // D = Rd[3], Rd[2:0] in lower bits
2848 let d_bit = (rd_bits >> 3) & 1;
2849 let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2850 Ok(instr.to_le_bytes().to_vec())
2851 } else {
2852 let instr: u16 = 0xBF00; // NOP fallback
2853 Ok(instr.to_le_bytes().to_vec())
2854 }
2855 }
2856
2857 ArmOp::Push { regs } => {
2858 // Thumb-2 PUSH encoding:
2859 // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2860 // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2861 let mut reg_list: u16 = 0;
2862 let mut need_32bit = false;
2863 for r in regs {
2864 let bit = reg_to_bits(r);
2865 if bit >= 8 && *r != Reg::LR {
2866 need_32bit = true;
2867 }
2868 reg_list |= 1 << bit;
2869 }
2870 if !need_32bit {
2871 // 16-bit PUSH: 1011 010 M rrrrrrrr
2872 let m_bit = if reg_list & (1 << 14) != 0 {
2873 1u16
2874 } else {
2875 0u16
2876 };
2877 let low_regs = reg_list & 0xFF;
2878 let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2879 Ok(instr.to_le_bytes().to_vec())
2880 } else {
2881 // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2882 let hw1: u16 = 0xE92D;
2883 let hw2: u16 = reg_list;
2884 let mut bytes = hw1.to_le_bytes().to_vec();
2885 bytes.extend_from_slice(&hw2.to_le_bytes());
2886 Ok(bytes)
2887 }
2888 }
2889
2890 ArmOp::Pop { regs } => {
2891 // Thumb-2 POP encoding:
2892 // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2893 // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2894 let mut reg_list: u16 = 0;
2895 let mut need_32bit = false;
2896 for r in regs {
2897 let bit = reg_to_bits(r);
2898 if bit >= 8 && *r != Reg::PC {
2899 need_32bit = true;
2900 }
2901 reg_list |= 1 << bit;
2902 }
2903 if !need_32bit {
2904 // 16-bit POP: 1011 110 P rrrrrrrr
2905 let p_bit = if reg_list & (1 << 15) != 0 {
2906 1u16
2907 } else {
2908 0u16
2909 };
2910 let low_regs = reg_list & 0xFF;
2911 let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2912 Ok(instr.to_le_bytes().to_vec())
2913 } else {
2914 // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2915 let hw1: u16 = 0xE8BD;
2916 let hw2: u16 = reg_list;
2917 let mut bytes = hw1.to_le_bytes().to_vec();
2918 bytes.extend_from_slice(&hw2.to_le_bytes());
2919 Ok(bytes)
2920 }
2921 }
2922
2923 ArmOp::Nop => {
2924 let instr: u16 = 0xBF00; // NOP in Thumb-2
2925 Ok(instr.to_le_bytes().to_vec())
2926 }
2927
2928 ArmOp::Udf { imm } => {
2929 // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2930 // This triggers UsageFault/HardFault, used for WASM traps
2931 let instr: u16 = 0xDE00 | (*imm as u16);
2932 let bytes = instr.to_le_bytes().to_vec();
2933 encoding_contracts::verify_thumb16(&bytes);
2934 Ok(bytes)
2935 }
2936
2937 // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2938 // ADDS sets flags (carry), ADC uses carry from previous ADDS
2939 ArmOp::Adds { rd, rn, op2 } => {
2940 let rd_bits = reg_to_bits(rd) as u16;
2941 let rn_bits = reg_to_bits(rn) as u16;
2942
2943 if let Operand2::Reg(rm) = op2 {
2944 let rm_bits = reg_to_bits(rm) as u16;
2945 // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2946 // operands in R8-R11, which would overflow the 3-bit fields
2947 // and corrupt the operands (#178/#180 class). Guard and fall
2948 // back to 32-bit ADDS.W for high registers.
2949 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2950 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2951 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2952 Ok(instr.to_le_bytes().to_vec())
2953 } else {
2954 self.encode_thumb32_adds_reg_raw(
2955 rd_bits as u32,
2956 rn_bits as u32,
2957 rm_bits as u32,
2958 )
2959 }
2960 } else {
2961 // 32-bit Thumb-2 ADDS with immediate
2962 self.encode_thumb32_adds(rd, rn, 0)
2963 }
2964 }
2965
2966 // ADC: Add with Carry (Thumb-2 32-bit)
2967 // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2968 ArmOp::Adc { rd, rn, op2 } => {
2969 let rd_bits = reg_to_bits(rd);
2970 let rn_bits = reg_to_bits(rn);
2971
2972 if let Operand2::Reg(rm) = op2 {
2973 let rm_bits = reg_to_bits(rm);
2974 // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2975 let hw1: u16 = (0xEB40 | rn_bits) as u16;
2976 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2977
2978 let mut bytes = hw1.to_le_bytes().to_vec();
2979 bytes.extend_from_slice(&hw2.to_le_bytes());
2980 Ok(bytes)
2981 } else {
2982 // ADC with immediate - use 32-bit encoding
2983 let hw1: u16 = (0xF140 | rn_bits) as u16;
2984 let hw2: u16 = (rd_bits << 8) as u16;
2985 let mut bytes = hw1.to_le_bytes().to_vec();
2986 bytes.extend_from_slice(&hw2.to_le_bytes());
2987 Ok(bytes)
2988 }
2989 }
2990
2991 // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2992 ArmOp::Subs { rd, rn, op2 } => {
2993 let rd_bits = reg_to_bits(rd) as u16;
2994 let rn_bits = reg_to_bits(rn) as u16;
2995
2996 if let Operand2::Reg(rm) = op2 {
2997 let rm_bits = reg_to_bits(rm) as u16;
2998 // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2999 // would overflow the 3-bit fields (#178/#180 class). Guard
3000 // and fall back to 32-bit SUBS.W for high registers.
3001 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
3002 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
3003 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
3004 Ok(instr.to_le_bytes().to_vec())
3005 } else {
3006 self.encode_thumb32_subs_reg_raw(
3007 rd_bits as u32,
3008 rn_bits as u32,
3009 rm_bits as u32,
3010 )
3011 }
3012 } else {
3013 // 32-bit Thumb-2 SUBS with immediate
3014 self.encode_thumb32_subs(rd, rn, 0)
3015 }
3016 }
3017
3018 // SBC: Subtract with Carry (Thumb-2 32-bit)
3019 // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
3020 ArmOp::Sbc { rd, rn, op2 } => {
3021 let rd_bits = reg_to_bits(rd);
3022 let rn_bits = reg_to_bits(rn);
3023
3024 if let Operand2::Reg(rm) = op2 {
3025 let rm_bits = reg_to_bits(rm);
3026 // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3027 let hw1: u16 = (0xEB60 | rn_bits) as u16;
3028 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3029
3030 let mut bytes = hw1.to_le_bytes().to_vec();
3031 bytes.extend_from_slice(&hw2.to_le_bytes());
3032 Ok(bytes)
3033 } else {
3034 // SBC with immediate - use 32-bit encoding
3035 let hw1: u16 = (0xF160 | rn_bits) as u16;
3036 let hw2: u16 = (rd_bits << 8) as u16;
3037 let mut bytes = hw1.to_le_bytes().to_vec();
3038 bytes.extend_from_slice(&hw2.to_le_bytes());
3039 Ok(bytes)
3040 }
3041 }
3042
3043 // === 32-bit Thumb-2 encodings ===
3044
3045 // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3046 ArmOp::Sdiv { rd, rn, rm } => {
3047 let rd_bits = reg_to_bits(rd);
3048 let rn_bits = reg_to_bits(rn);
3049 let rm_bits = reg_to_bits(rm);
3050 reg_bits_checked(rd_bits)?;
3051 reg_bits_checked(rn_bits)?;
3052 reg_bits_checked(rm_bits)?;
3053
3054 // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3055 // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3056 // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3057 let hw1: u16 = (0xFB90 | rn_bits) as u16;
3058 let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3059
3060 // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3061 let mut bytes = hw1.to_le_bytes().to_vec();
3062 bytes.extend_from_slice(&hw2.to_le_bytes());
3063 encoding_contracts::verify_thumb32(&bytes);
3064 Ok(bytes)
3065 }
3066
3067 // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3068 ArmOp::Udiv { rd, rn, rm } => {
3069 let rd_bits = reg_to_bits(rd);
3070 let rn_bits = reg_to_bits(rn);
3071 let rm_bits = reg_to_bits(rm);
3072 reg_bits_checked(rd_bits)?;
3073 reg_bits_checked(rn_bits)?;
3074 reg_bits_checked(rm_bits)?;
3075
3076 // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3077 let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3078 let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3079
3080 let mut bytes = hw1.to_le_bytes().to_vec();
3081 bytes.extend_from_slice(&hw2.to_le_bytes());
3082 encoding_contracts::verify_thumb32(&bytes);
3083 Ok(bytes)
3084 }
3085
3086 ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3087 let rdlo_bits = reg_to_bits(rdlo);
3088 let rdhi_bits = reg_to_bits(rdhi);
3089 let rn_bits = reg_to_bits(rn);
3090 let rm_bits = reg_to_bits(rm);
3091 reg_bits_checked(rdlo_bits)?;
3092 reg_bits_checked(rdhi_bits)?;
3093 reg_bits_checked(rn_bits)?;
3094 reg_bits_checked(rm_bits)?;
3095
3096 // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3097 let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3098 let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3099
3100 let mut bytes = hw1.to_le_bytes().to_vec();
3101 bytes.extend_from_slice(&hw2.to_le_bytes());
3102 encoding_contracts::verify_thumb32(&bytes);
3103 Ok(bytes)
3104 }
3105
3106 // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3107 ArmOp::Mul { rd, rn, rm } => {
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
3112 // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3113 // 11111011 0000 Rn | 1111 Rd 0000 Rm
3114 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3115 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3116
3117 let mut bytes = hw1.to_le_bytes().to_vec();
3118 bytes.extend_from_slice(&hw2.to_le_bytes());
3119 Ok(bytes)
3120 }
3121
3122 // MLS: Rd = Ra - Rn * Rm
3123 ArmOp::Mls { 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 MLS: FB00 Rn | Ra Rd 0001 Rm
3130 // 11111011 0000 Rn | Ra Rd 0001 Rm
3131 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3132 let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | 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 ArmOp::Mla { rd, rn, rm, ra } => {
3140 let rd_bits = reg_to_bits(rd);
3141 let rn_bits = reg_to_bits(rn);
3142 let rm_bits = reg_to_bits(rm);
3143 let ra_bits = reg_to_bits(ra);
3144
3145 // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3146 // bit-4 (0x10) op flag. rd = ra + rn*rm.
3147 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3148 let hw2: u16 = ((ra_bits << 12) | (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 }
3154
3155 // AND (Thumb-2 32-bit)
3156 ArmOp::And { rd, rn, op2 } => {
3157 if let Operand2::Reg(rm) = op2 {
3158 let rd_bits = reg_to_bits(rd);
3159 let rn_bits = reg_to_bits(rn);
3160 let rm_bits = reg_to_bits(rm);
3161
3162 // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3163 let hw1: u16 = (0xEA00 | rn_bits) as u16;
3164 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3165
3166 let mut bytes = hw1.to_le_bytes().to_vec();
3167 bytes.extend_from_slice(&hw2.to_le_bytes());
3168 Ok(bytes)
3169 } else if let Operand2::Imm(imm) = op2 {
3170 let rd_bits = reg_to_bits(rd);
3171 let rn_bits = reg_to_bits(rn);
3172
3173 // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3174 // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3175 // encode it correctly (or error on an un-encodable value)
3176 // rather than packing raw bits, closing the silent-miscompile
3177 // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3178 // CMP (#255).
3179 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3180 synth_core::Error::synthesis(
3181 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3182 )
3183 })?;
3184 let i_bit = (field >> 11) & 1;
3185 let imm3 = (field >> 8) & 0x7;
3186 let imm8 = field & 0xFF;
3187
3188 let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3189 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3190
3191 let mut bytes = hw1.to_le_bytes().to_vec();
3192 bytes.extend_from_slice(&hw2.to_le_bytes());
3193 Ok(bytes)
3194 } else {
3195 // RegShift variant - fallback to NOP
3196 let instr: u16 = 0xBF00;
3197 Ok(instr.to_le_bytes().to_vec())
3198 }
3199 }
3200
3201 // ORR (Thumb-2 32-bit)
3202 ArmOp::Orr { rd, rn, op2 } => {
3203 if let Operand2::Reg(rm) = op2 {
3204 let rd_bits = reg_to_bits(rd);
3205 let rn_bits = reg_to_bits(rn);
3206 let rm_bits = reg_to_bits(rm);
3207
3208 // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3209 let hw1: u16 = (0xEA40 | rn_bits) as u16;
3210 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3211
3212 let mut bytes = hw1.to_le_bytes().to_vec();
3213 bytes.extend_from_slice(&hw2.to_le_bytes());
3214 Ok(bytes)
3215 } else if let Operand2::Imm(imm) = op2 {
3216 // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3217 // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3218 // larger modified immediates need ThumbExpandImm — return an
3219 // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3220 let imm_val = *imm as u32;
3221 if imm_val > 0xFF {
3222 return Err(synth_core::Error::synthesis(
3223 "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3224 ));
3225 }
3226 let rd_bits = reg_to_bits(rd);
3227 let rn_bits = reg_to_bits(rn);
3228 let hw1: u16 = (0xF040 | rn_bits) as u16;
3229 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3230 let mut bytes = hw1.to_le_bytes().to_vec();
3231 bytes.extend_from_slice(&hw2.to_le_bytes());
3232 Ok(bytes)
3233 } else {
3234 let instr: u16 = 0xBF00;
3235 Ok(instr.to_le_bytes().to_vec())
3236 }
3237 }
3238
3239 // EOR (Thumb-2 32-bit)
3240 ArmOp::Eor { rd, rn, op2 } => {
3241 if let Operand2::Reg(rm) = op2 {
3242 let rd_bits = reg_to_bits(rd);
3243 let rn_bits = reg_to_bits(rn);
3244 let rm_bits = reg_to_bits(rm);
3245
3246 // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3247 let hw1: u16 = (0xEA80 | rn_bits) as u16;
3248 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3249
3250 let mut bytes = hw1.to_le_bytes().to_vec();
3251 bytes.extend_from_slice(&hw2.to_le_bytes());
3252 Ok(bytes)
3253 } else if let Operand2::Imm(imm) = op2 {
3254 // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3255 // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3256 // error, not a silent NOP (Ok-or-Err, #180/#185).
3257 let imm_val = *imm as u32;
3258 if imm_val > 0xFF {
3259 return Err(synth_core::Error::synthesis(
3260 "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3261 ));
3262 }
3263 let rd_bits = reg_to_bits(rd);
3264 let rn_bits = reg_to_bits(rn);
3265 let hw1: u16 = (0xF080 | rn_bits) as u16;
3266 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3267 let mut bytes = hw1.to_le_bytes().to_vec();
3268 bytes.extend_from_slice(&hw2.to_le_bytes());
3269 Ok(bytes)
3270 } else {
3271 let instr: u16 = 0xBF00;
3272 Ok(instr.to_le_bytes().to_vec())
3273 }
3274 }
3275
3276 // Shift operations (16-bit for low registers)
3277 ArmOp::Lsl { rd, rn, shift } => {
3278 let rd_bits = reg_to_bits(rd) as u16;
3279 let rn_bits = reg_to_bits(rn) as u16;
3280 let shift_bits = (*shift as u16) & 0x1F;
3281
3282 if rd_bits < 8 && rn_bits < 8 {
3283 // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3284 let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3285 Ok(instr.to_le_bytes().to_vec())
3286 } else {
3287 // Use 32-bit encoding for high registers
3288 self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3289 }
3290 }
3291
3292 ArmOp::Lsr { rd, rn, shift } => {
3293 let rd_bits = reg_to_bits(rd) as u16;
3294 let rn_bits = reg_to_bits(rn) as u16;
3295 let shift_bits = (*shift as u16) & 0x1F;
3296
3297 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3298 // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3299 let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3300 Ok(instr.to_le_bytes().to_vec())
3301 } else {
3302 self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3303 }
3304 }
3305
3306 ArmOp::Asr { rd, rn, shift } => {
3307 let rd_bits = reg_to_bits(rd) as u16;
3308 let rn_bits = reg_to_bits(rn) as u16;
3309 let shift_bits = (*shift as u16) & 0x1F;
3310
3311 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3312 // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3313 let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3314 Ok(instr.to_le_bytes().to_vec())
3315 } else {
3316 self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3317 }
3318 }
3319
3320 ArmOp::Ror { rd, rn, shift } => {
3321 // ROR doesn't have a 16-bit immediate form, use 32-bit
3322 self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3323 }
3324
3325 // Register-based shifts (Thumb-2 32-bit)
3326 // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3327 // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3328 ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3329 ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3330 ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3331 ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3332
3333 // RSB (Reverse Subtract): Rd = imm - Rn
3334 // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3335 ArmOp::Rsb { rd, rn, imm } => {
3336 let rd_bits = reg_to_bits(rd);
3337 let rn_bits = reg_to_bits(rn);
3338
3339 // #681 class audit: the T2 `i:imm3:imm8` field is a
3340 // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3341 // (T4-style) form — packing a raw value > 0xFF silently encodes
3342 // a different constant (#253/#255 class). All current emitters
3343 // use imm 32 (shift complement), which expands to itself, so
3344 // this gate is byte-identical for existing codegen.
3345 let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3346 synth_core::Error::synthesis(
3347 "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3348 )
3349 })?;
3350 let i_bit = (field >> 11) & 1;
3351 let imm3 = (field >> 8) & 0x7;
3352 let imm8 = field & 0xFF;
3353
3354 // hw1: 11110 i 01110 0 Rn (S=0)
3355 let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3356 // hw2: 0 imm3 Rd imm8
3357 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3358
3359 let mut bytes = hw1.to_le_bytes().to_vec();
3360 bytes.extend_from_slice(&hw2.to_le_bytes());
3361 Ok(bytes)
3362 }
3363
3364 // CLZ (Thumb-2 32-bit)
3365 ArmOp::Clz { rd, rm } => {
3366 let rd_bits = reg_to_bits(rd);
3367 let rm_bits = reg_to_bits(rm);
3368
3369 // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3370 // 11111010 1011 Rm | 1111 1000 Rd Rm
3371 let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3372 let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3373
3374 let mut bytes = hw1.to_le_bytes().to_vec();
3375 bytes.extend_from_slice(&hw2.to_le_bytes());
3376 Ok(bytes)
3377 }
3378
3379 // RBIT (Thumb-2 32-bit)
3380 ArmOp::Rbit { rd, rm } => {
3381 let rd_bits = reg_to_bits(rd);
3382 let rm_bits = reg_to_bits(rm);
3383
3384 // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3385 // 11111010 1001 Rm | 1111 Rd 1010 Rm
3386 let hw1: u16 = (0xFA90 | rm_bits) as u16;
3387 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3388
3389 let mut bytes = hw1.to_le_bytes().to_vec();
3390 bytes.extend_from_slice(&hw2.to_le_bytes());
3391 Ok(bytes)
3392 }
3393
3394 // SXTB (16-bit for low registers)
3395 ArmOp::Sxtb { rd, rm } => {
3396 let rd_bits = reg_to_bits(rd) as u16;
3397 let rm_bits = reg_to_bits(rm) as u16;
3398
3399 if rd_bits < 8 && rm_bits < 8 {
3400 // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3401 let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3402 Ok(instr.to_le_bytes().to_vec())
3403 } else {
3404 // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3405 // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3406 let rd_bits32 = rd_bits as u32;
3407 let rm_bits32 = rm_bits as u32;
3408 let hw1: u16 = 0xFA4F;
3409 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3410 let mut bytes = hw1.to_le_bytes().to_vec();
3411 bytes.extend_from_slice(&hw2.to_le_bytes());
3412 Ok(bytes)
3413 }
3414 }
3415
3416 // SXTH (16-bit for low registers)
3417 ArmOp::Sxth { rd, rm } => {
3418 let rd_bits = reg_to_bits(rd) as u16;
3419 let rm_bits = reg_to_bits(rm) as u16;
3420
3421 if rd_bits < 8 && rm_bits < 8 {
3422 // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3423 let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3424 Ok(instr.to_le_bytes().to_vec())
3425 } else {
3426 // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3427 // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3428 let rd_bits32 = rd_bits as u32;
3429 let rm_bits32 = rm_bits as u32;
3430 let hw1: u16 = 0xFA0F;
3431 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3432 let mut bytes = hw1.to_le_bytes().to_vec();
3433 bytes.extend_from_slice(&hw2.to_le_bytes());
3434 Ok(bytes)
3435 }
3436 }
3437
3438 // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3439 ArmOp::Uxtb { rd, rm } => {
3440 let rd_bits = reg_to_bits(rd) as u16;
3441 let rm_bits = reg_to_bits(rm) as u16;
3442 if rd_bits < 8 && rm_bits < 8 {
3443 // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3444 let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3445 Ok(instr.to_le_bytes().to_vec())
3446 } else {
3447 // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3448 let hw1: u16 = 0xFA5F;
3449 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3450 let mut bytes = hw1.to_le_bytes().to_vec();
3451 bytes.extend_from_slice(&hw2.to_le_bytes());
3452 Ok(bytes)
3453 }
3454 }
3455
3456 // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3457 ArmOp::Uxth { rd, rm } => {
3458 let rd_bits = reg_to_bits(rd) as u16;
3459 let rm_bits = reg_to_bits(rm) as u16;
3460 if rd_bits < 8 && rm_bits < 8 {
3461 // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3462 let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3463 Ok(instr.to_le_bytes().to_vec())
3464 } else {
3465 // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3466 let hw1: u16 = 0xFA1F;
3467 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3468 let mut bytes = hw1.to_le_bytes().to_vec();
3469 bytes.extend_from_slice(&hw2.to_le_bytes());
3470 Ok(bytes)
3471 }
3472 }
3473
3474 // CMP (can be 16-bit for low registers)
3475 ArmOp::Cmp { rn, op2 } => {
3476 let rn_bits = reg_to_bits(rn) as u16;
3477
3478 if let Operand2::Imm(imm) = op2 {
3479 // Only use 16-bit encoding for non-negative immediates 0-255
3480 // Negative immediates must use 32-bit encoding
3481 if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3482 // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3483 let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3484 Ok(instr.to_le_bytes().to_vec())
3485 } else {
3486 self.encode_thumb32_cmp_imm(rn, *imm as u32)
3487 }
3488 } else if let Operand2::Reg(rm) = op2 {
3489 let rm_bits = reg_to_bits(rm) as u16;
3490 if rn_bits < 8 && rm_bits < 8 {
3491 // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3492 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3493 Ok(instr.to_le_bytes().to_vec())
3494 } else {
3495 // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3496 let n_bit = (rn_bits >> 3) & 1;
3497 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3498 Ok(instr.to_le_bytes().to_vec())
3499 }
3500 } else {
3501 let instr: u16 = 0xBF00;
3502 Ok(instr.to_le_bytes().to_vec())
3503 }
3504 }
3505
3506 // CMN (Compare Negative) - computes Rn + op2 and sets flags
3507 // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3508 ArmOp::Cmn { rn, op2 } => {
3509 let rn_bits = reg_to_bits(rn) as u16;
3510
3511 if let Operand2::Imm(imm) = op2 {
3512 // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3513 // modified immediate (the field sits in imm3=hw2[14:12],
3514 // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3515 // an un-encodable value — replacing the old silent `0xBF00`
3516 // NOP (the last of the silent-miscompile data-proc encoders).
3517 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3518 synth_core::Error::synthesis(
3519 "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3520 )
3521 })?;
3522 let i_bit = (field >> 11) & 1;
3523 let imm3 = (field >> 8) & 0x7;
3524 let imm8 = field & 0xFF;
3525 let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3526 let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3527 let mut bytes = hw1.to_le_bytes().to_vec();
3528 bytes.extend_from_slice(&hw2.to_le_bytes());
3529 Ok(bytes)
3530 } else if let Operand2::Reg(rm) = op2 {
3531 let rm_bits = reg_to_bits(rm) as u16;
3532 // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3533 // the 3-bit fields and corrupt the operands (#184, the #180
3534 // class). CMN has no high-register 16-bit form, so fall back
3535 // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3536 // Rd discarded as PC/1111).
3537 if rn_bits < 8 && rm_bits < 8 {
3538 // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3539 let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3540 Ok(instr.to_le_bytes().to_vec())
3541 } else {
3542 let hw1: u16 = 0xEB10 | rn_bits;
3543 let hw2: u16 = 0x0F00 | rm_bits;
3544 let mut bytes = hw1.to_le_bytes().to_vec();
3545 bytes.extend_from_slice(&hw2.to_le_bytes());
3546 Ok(bytes)
3547 }
3548 } else {
3549 Ok(vec![0xBF, 0x00])
3550 }
3551 }
3552
3553 // LDR (can be 16-bit for simple cases)
3554 ArmOp::Ldr { rd, addr } => {
3555 let rd_bits = reg_to_bits(rd);
3556 let base_bits = reg_to_bits(&addr.base);
3557
3558 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3559 if let Some(offset_reg) = &addr.offset_reg {
3560 let rm_bits = reg_to_bits(offset_reg);
3561
3562 // If there's also an immediate offset, we need to ADD it first
3563 if addr.offset != 0 {
3564 // Use R12 (IP) as scratch to avoid clobbering the address register
3565 // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3566 let scratch = Reg::R12;
3567 let mut bytes =
3568 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3569 bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3570 return Ok(bytes);
3571 }
3572
3573 // Simple register offset: LDR Rd, [Rn, Rm]
3574 // 16-bit: only if Rd, Rn, Rm < R8
3575 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3576 // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3577 let instr: u16 = 0x5800
3578 | ((rm_bits as u16) << 6)
3579 | ((base_bits as u16) << 3)
3580 | (rd_bits as u16);
3581 return Ok(instr.to_le_bytes().to_vec());
3582 }
3583
3584 // 32-bit register offset
3585 return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3586 }
3587
3588 // Immediate offset mode [base, #imm]
3589 let offset = addr.offset as u32;
3590
3591 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3592 // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3593 let imm5 = (offset >> 2) as u16;
3594 let instr: u16 =
3595 0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3596 Ok(instr.to_le_bytes().to_vec())
3597 } else {
3598 self.encode_thumb32_ldr(rd, &addr.base, offset)
3599 }
3600 }
3601
3602 // STR (can be 16-bit for simple cases)
3603 ArmOp::Str { rd, addr } => {
3604 let rd_bits = reg_to_bits(rd);
3605 let base_bits = reg_to_bits(&addr.base);
3606
3607 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3608 if let Some(offset_reg) = &addr.offset_reg {
3609 let rm_bits = reg_to_bits(offset_reg);
3610
3611 // If there's also an immediate offset, we need to ADD it first
3612 if addr.offset != 0 {
3613 // Use R12 (IP) as scratch to avoid clobbering the address register
3614 // ADD R12, Rm, #offset; STR Rd, [base, R12]
3615 let scratch = Reg::R12;
3616 let mut bytes =
3617 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3618 bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3619 return Ok(bytes);
3620 }
3621
3622 // Simple register offset: STR Rd, [Rn, Rm]
3623 // 16-bit: only if Rd, Rn, Rm < R8
3624 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3625 // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3626 let instr: u16 = 0x5000
3627 | ((rm_bits as u16) << 6)
3628 | ((base_bits as u16) << 3)
3629 | (rd_bits as u16);
3630 return Ok(instr.to_le_bytes().to_vec());
3631 }
3632
3633 // 32-bit register offset
3634 return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3635 }
3636
3637 // Immediate offset mode [base, #imm]
3638 let offset = addr.offset as u32;
3639
3640 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3641 // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3642 let imm5 = (offset >> 2) as u16;
3643 let instr: u16 =
3644 0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3645 Ok(instr.to_le_bytes().to_vec())
3646 } else {
3647 self.encode_thumb32_str(rd, &addr.base, offset)
3648 }
3649 }
3650
3651 // LDRB (Thumb-2)
3652 ArmOp::Ldrb { rd, addr } => {
3653 let rd_bits = reg_to_bits(rd);
3654 let base_bits = reg_to_bits(&addr.base);
3655
3656 if let Some(offset_reg) = &addr.offset_reg {
3657 if addr.offset != 0 {
3658 let scratch = Reg::R12;
3659 let mut bytes =
3660 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3661 bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3662 return Ok(bytes);
3663 }
3664 return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3665 }
3666
3667 let offset = addr.offset as u32;
3668 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3669 // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3670 let instr: u16 = 0x7800
3671 | ((offset as u16) << 6)
3672 | ((base_bits as u16) << 3)
3673 | (rd_bits as u16);
3674 Ok(instr.to_le_bytes().to_vec())
3675 } else {
3676 self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3677 }
3678 }
3679
3680 // LDRSB (Thumb-2)
3681 ArmOp::Ldrsb { rd, addr } => {
3682 let rd_bits = reg_to_bits(rd);
3683 let base_bits = reg_to_bits(&addr.base);
3684
3685 if let Some(offset_reg) = &addr.offset_reg {
3686 if addr.offset != 0 {
3687 let scratch = Reg::R12;
3688 let mut bytes =
3689 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3690 bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3691 return Ok(bytes);
3692 }
3693 return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3694 }
3695
3696 let offset = addr.offset as u32;
3697 // LDRSB has no 16-bit immediate form (only register)
3698 // For 16-bit reg form: only if Rd, Rn, Rm < R8
3699 if rd_bits < 8 && base_bits < 8 && offset == 0 {
3700 // No immediate 16-bit encoding for LDRSB; use 32-bit
3701 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3702 } else {
3703 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3704 }
3705 }
3706
3707 // LDRH (Thumb-2)
3708 ArmOp::Ldrh { rd, addr } => {
3709 let rd_bits = reg_to_bits(rd);
3710 let base_bits = reg_to_bits(&addr.base);
3711
3712 if let Some(offset_reg) = &addr.offset_reg {
3713 if addr.offset != 0 {
3714 let scratch = Reg::R12;
3715 let mut bytes =
3716 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3717 bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3718 return Ok(bytes);
3719 }
3720 return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3721 }
3722
3723 let offset = addr.offset as u32;
3724 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3725 // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3726 let imm5 = (offset >> 1) as u16;
3727 let instr: u16 =
3728 0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3729 Ok(instr.to_le_bytes().to_vec())
3730 } else {
3731 self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3732 }
3733 }
3734
3735 // LDRSH (Thumb-2)
3736 ArmOp::Ldrsh { rd, addr } => {
3737 if let Some(offset_reg) = &addr.offset_reg {
3738 if addr.offset != 0 {
3739 let scratch = Reg::R12;
3740 let mut bytes =
3741 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3742 bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3743 return Ok(bytes);
3744 }
3745 return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3746 }
3747
3748 let offset = addr.offset as u32;
3749 self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3750 }
3751
3752 // STRB (Thumb-2)
3753 ArmOp::Strb { rd, addr } => {
3754 let rd_bits = reg_to_bits(rd);
3755 let base_bits = reg_to_bits(&addr.base);
3756
3757 if let Some(offset_reg) = &addr.offset_reg {
3758 if addr.offset != 0 {
3759 let scratch = Reg::R12;
3760 let mut bytes =
3761 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3762 bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3763 return Ok(bytes);
3764 }
3765 return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3766 }
3767
3768 let offset = addr.offset as u32;
3769 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3770 // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3771 let instr: u16 = 0x7000
3772 | ((offset as u16) << 6)
3773 | ((base_bits as u16) << 3)
3774 | (rd_bits as u16);
3775 Ok(instr.to_le_bytes().to_vec())
3776 } else {
3777 self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3778 }
3779 }
3780
3781 // STRH (Thumb-2)
3782 ArmOp::Strh { rd, addr } => {
3783 let rd_bits = reg_to_bits(rd);
3784 let base_bits = reg_to_bits(&addr.base);
3785
3786 if let Some(offset_reg) = &addr.offset_reg {
3787 if addr.offset != 0 {
3788 let scratch = Reg::R12;
3789 let mut bytes =
3790 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3791 bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3792 return Ok(bytes);
3793 }
3794 return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3795 }
3796
3797 let offset = addr.offset as u32;
3798 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3799 // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3800 let imm5 = (offset >> 1) as u16;
3801 let instr: u16 =
3802 0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3803 Ok(instr.to_le_bytes().to_vec())
3804 } else {
3805 self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3806 }
3807 }
3808
3809 // MemorySize (Thumb-2)
3810 ArmOp::MemorySize { rd } => {
3811 // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3812 // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3813 let rd_bits = reg_to_bits(rd);
3814 let r10_bits = reg_to_bits(&Reg::R10);
3815 if rd_bits < 8 && r10_bits < 8 {
3816 let instr: u16 =
3817 0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3818 Ok(instr.to_le_bytes().to_vec())
3819 } else {
3820 // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3821 let imm5: u32 = 16;
3822 let imm3 = (imm5 >> 2) & 0x7;
3823 let imm2 = imm5 & 0x3;
3824 let hw1: u16 = 0xEA4F;
3825 let hw2: u16 =
3826 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3827 let mut bytes = hw1.to_le_bytes().to_vec();
3828 bytes.extend_from_slice(&hw2.to_le_bytes());
3829 Ok(bytes)
3830 }
3831 }
3832
3833 // MemoryGrow (Thumb-2)
3834 ArmOp::MemoryGrow { rd, .. } => {
3835 // On embedded with fixed memory, always return -1 (failure)
3836 // MVN rd, #0 → MOV rd, #-1
3837 // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3838 let rd_bits = reg_to_bits(rd);
3839 let hw1: u16 = 0xF06F; // MVN with i=0
3840 let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3841 let mut bytes = hw1.to_le_bytes().to_vec();
3842 bytes.extend_from_slice(&hw2.to_le_bytes());
3843 Ok(bytes)
3844 }
3845
3846 // BX (16-bit)
3847 ArmOp::Bx { rm } => {
3848 let rm_bits = reg_to_bits(rm) as u16;
3849 // BX Rm (16-bit): 0100 0111 0 Rm 000
3850 let instr: u16 = 0x4700 | (rm_bits << 3);
3851 Ok(instr.to_le_bytes().to_vec())
3852 }
3853
3854 // BLX (16-bit) - Branch with Link and Exchange
3855 // BLX Rm: 0100 0111 1 Rm 000
3856 ArmOp::Blx { rm } => {
3857 let rm_bits = reg_to_bits(rm) as u16;
3858 let instr: u16 = 0x4780 | (rm_bits << 3);
3859 Ok(instr.to_le_bytes().to_vec())
3860 }
3861
3862 // CallIndirect - indirect function call via table lookup
3863 // table_index_reg contains the table index
3864 // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3865 // UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3866 // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3867 // R11 region): the pointer load becomes
3868 // ADD R12,R11,R12; LDR R12,[R12,#offset]
3869 // #664, null_check (the table has null slots, linked as ZERO
3870 // words): the loaded pointer is null-checked before the BLX —
3871 // CMP.W R12,#0; BNE +1; UDF #0
3872 // #676, type_check (heterogeneous table — runtime §4.4.8 type
3873 // check against the type-id sidecar at R11+off): after the
3874 // bounds guard —
3875 // LSL R12,idx,#2; ADD R12,R11,R12;
3876 // LDR R12,[R12,#type_off]; CMP.W R12,#id;
3877 // BEQ +1; UDF #0
3878 // (the dispatch tail then recomputes idx*4 — idx stays live).
3879 ArmOp::CallIndirect {
3880 rd: _,
3881 type_idx: _,
3882 table_index_reg,
3883 table_size,
3884 table_byte_offset,
3885 null_check,
3886 type_check,
3887 } => {
3888 let idx_reg = reg_to_bits(table_index_reg);
3889 let mut bytes = Vec::new();
3890
3891 // The expansion:
3892 // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3893 // index >= table size. Without it an out-of-bounds index
3894 // reads past the table and BLXes whatever word lies there —
3895 // an uncontrolled indirect branch instead of a trap.
3896 // 2. Multiplies index by 4 (function pointer size)
3897 // 3. Loads function pointer from table (table base in R11)
3898 // 4. Calls the function via BLX
3899 //
3900 // Table base setup must be done by caller/runtime. The type
3901 // check §4.4.8 also requires is discharged at COMPILE time:
3902 // the selector only emits this op after verifying the closed-
3903 // world property that every table entry's signature equals the
3904 // expected type (the raw code-pointer table carries no runtime
3905 // type ids to compare) — see the #642 selector guard.
3906
3907 // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3908 // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3909 let size_lo = *table_size & 0xFFFF;
3910 let hw1: u16 =
3911 (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3912 let hw2: u16 =
3913 ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3914 bytes.extend_from_slice(&hw1.to_le_bytes());
3915 bytes.extend_from_slice(&hw2.to_le_bytes());
3916 // MOVT R12, #(size >> 16) — only when the table size exceeds
3917 // 16 bits (never in practice, but the guard must not compare
3918 // against a truncated size).
3919 let size_hi = *table_size >> 16;
3920 if size_hi != 0 {
3921 let hw1: u16 =
3922 (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3923 let hw2: u16 =
3924 ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3925 bytes.extend_from_slice(&hw1.to_le_bytes());
3926 bytes.extend_from_slice(&hw2.to_le_bytes());
3927 }
3928 // CMP idx, R12 — 16-bit T2 (high-register capable):
3929 // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3930 let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3931 bytes.extend_from_slice(&cmp.to_le_bytes());
3932 // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3933 // imm8=0: target = branch + 4. LO = unsigned lower.
3934 bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3935 // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3936 // the div-by-zero guards).
3937 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3938
3939 // #676: runtime type check — ONLY for a heterogeneous table
3940 // (mixed signatures, closed-world verdict impossible). Load
3941 // the indexed slot's structural class id from the type-id
3942 // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3943 // base + this table's base offset, a compile-time constant)
3944 // and compare it against the expected type's class id — a
3945 // mismatch is the WASM Core §4.4.8 type trap. Null slots
3946 // carry the reserved id 0, so this compare subsumes the
3947 // #664 null trap (the selector passes `null_check: false`).
3948 // `None` emits NOTHING: every homogeneous table keeps the
3949 // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3950 // only scratch (#212); the dispatch tail below recomputes
3951 // idx*4 — the index register is never clobbered here.
3952 if let Some((expected_id, type_off)) = type_check {
3953 // RQ-61-IMMRANGE (#1072): compiled out in release, where
3954 // the masks below silently TRUNCATE (id 256 compares as
3955 // 0 — a NULL slot would pass the §4.4.8 check). The
3956 // enforcement claim is DEMONSTRATED: the sole `Some`
3957 // producer, `resolve_runtime_type_check`, loud-declines
3958 // id > 255 / offset > 4095, tripped by
3959 // `test_676_call_indirect_runtime_check_range_declines`
3960 // (mutation-checked — see the A32 twin above).
3961 debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3962 debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3963 // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3964 // tail's index scale below).
3965 bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3966 bytes.extend_from_slice(
3967 &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3968 );
3969 // ADD.W R12, R11, R12 (the #650 base-add form).
3970 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3971 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3972 // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3973 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3974 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3975 bytes.extend_from_slice(
3976 &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3977 );
3978 // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3979 // (same form as the #664 null check's CMP.W R12, #0).
3980 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3981 bytes.extend_from_slice(
3982 &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
3983 );
3984 // BEQ +1 insn (skip the UDF when the class id matches) —
3985 // B<cond>.N imm8=0: target = branch + 4. EQ.
3986 bytes.extend_from_slice(&0xD000u16.to_le_bytes());
3987 // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
3988 // the bounds guard above).
3989 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3990 }
3991
3992 // LSL R12, idx_reg, #2 (multiply index by 4)
3993 // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3994 // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3995 // #597: the shift amount was previously shifted into bits 5:4 —
3996 // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3997 // destroyed the index and dispatched table entry 0 for every
3998 // call. imm2 lives at bits 7:6.
3999 let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
4000 let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
4001 bytes.extend_from_slice(&hw1.to_le_bytes());
4002 bytes.extend_from_slice(&hw2.to_le_bytes());
4003
4004 if *table_byte_offset == 0 {
4005 // Table 0 (base = R11 itself): the pre-#650 single-load
4006 // form — a single-table module's bytes stay identical BY
4007 // CONSTRUCTION.
4008 // LDR R12, [R11, R12] - load function pointer
4009 // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
4010 // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
4011 let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
4012 let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
4013 bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
4014 bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
4015 } else {
4016 // #650: table N of the contiguous R11 region — fold the
4017 // compile-time base offset into the pointer load via the
4018 // LDR imm12 form (R12 stays the only scratch, per the
4019 // #212 convention).
4020 assert!(
4021 *table_byte_offset <= 4095,
4022 "call_indirect table base offset {table_byte_offset} exceeds \
4023 LDR imm12 — the selector must have declined this (#650)"
4024 );
4025 // ADD.W R12, R11, R12 — T3 ADD (register):
4026 // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
4027 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
4028 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4029 // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4030 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4031 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4032 bytes.extend_from_slice(
4033 &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4034 );
4035 }
4036
4037 // #664: null-slot trap — ONLY when the table image carries
4038 // null (uninitialized) slots, which the layout contract
4039 // requires to be linked as ZERO words. A fully-initialized
4040 // table skips this branch entirely, keeping the pre-#664
4041 // expansion byte-identical BY CONSTRUCTION (the #650
4042 // offset-0 trick).
4043 if *null_check {
4044 // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4045 // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4046 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4047 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4048 // BNE +1 insn (skip the UDF when the pointer is non-null)
4049 // — B<cond>.N imm8=0: target = branch + 4. NE.
4050 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4051 // UDF #0 — call_indirect null-funcref trap (WASM Core
4052 // §4.4.8: calling an uninitialized element traps; same
4053 // trap idiom as the bounds guard above).
4054 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4055 }
4056
4057 // BLX R12 (call function indirectly)
4058 // BLX Rm (16-bit): 0100 0111 1 Rm 000
4059 let blx: u16 = 0x47E0; // BLX R12
4060 bytes.extend_from_slice(&blx.to_le_bytes());
4061
4062 Ok(bytes)
4063 }
4064
4065 // Label pseudo-instruction: emits no machine code
4066 ArmOp::Label { .. } => Ok(Vec::new()),
4067
4068 // Conditional branch to label (generic) - offset 0, will be patched
4069 ArmOp::Bcc { cond, label: _ } => {
4070 use synth_synthesis::Condition;
4071 let cond_bits: u16 = match cond {
4072 Condition::EQ => 0x0,
4073 Condition::NE => 0x1,
4074 Condition::HS => 0x2,
4075 Condition::LO => 0x3,
4076 Condition::HI => 0x8,
4077 Condition::LS => 0x9,
4078 Condition::GE => 0xA,
4079 Condition::LT => 0xB,
4080 Condition::GT => 0xC,
4081 Condition::LE => 0xD,
4082 };
4083 // 16-bit B<cond> with offset 0: 1101 cond imm8
4084 let instr: u16 = 0xD000 | (cond_bits << 8);
4085 Ok(instr.to_le_bytes().to_vec())
4086 }
4087
4088 // Branch instructions
4089 ArmOp::B { label: _ } => {
4090 // Simplified: B.N with offset 0
4091 // For real usage, would need label resolution
4092 let instr: u16 = 0xE000; // B.N #0
4093 Ok(instr.to_le_bytes().to_vec())
4094 }
4095
4096 // BHS (Branch if Higher or Same) - used for bounds checking
4097 // Condition code: 0x2 (C set)
4098 ArmOp::Bhs { label: _ } => {
4099 // 16-bit B<cond> with offset 0: 1101 cond imm8
4100 // cond = 0x2 (HS)
4101 let instr: u16 = 0xD200; // BHS.N #0
4102 Ok(instr.to_le_bytes().to_vec())
4103 }
4104
4105 // BLO (Branch if Lower) - complementary to BHS
4106 // Condition code: 0x3 (C clear)
4107 ArmOp::Blo { label: _ } => {
4108 // 16-bit B<cond> with offset 0: 1101 cond imm8
4109 // cond = 0x3 (LO)
4110 let instr: u16 = 0xD300; // BLO.N #0
4111 Ok(instr.to_le_bytes().to_vec())
4112 }
4113
4114 // Branch with numeric offset (Thumb-2)
4115 // Thumb-2 B.W instruction: 32-bit with +-16MB range
4116 ArmOp::BOffset { offset } => {
4117 // offset is already the halfword displacement: (target - branch - 4) / 2
4118 // This is the raw encoded value, accounting for variable-length instructions
4119 let halfword_offset = *offset;
4120
4121 // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4122 // Range: -1024 to +1022 halfwords
4123 if (-1024..=1022).contains(&halfword_offset) {
4124 // 16-bit B.N encoding: 1110 0 imm11
4125 let imm11 = (halfword_offset as u16) & 0x7FF;
4126 let instr: u16 = 0xE000 | imm11;
4127 Ok(instr.to_le_bytes().to_vec())
4128 } else {
4129 // 32-bit B.W encoding for larger offsets
4130 // First halfword: 1111 0 S imm10
4131 // Second halfword: 10 J1 0 J2 imm11
4132 // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4133 // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4134
4135 // The B.W (T4) encoding packs the signed offset as:
4136 // S:I1:I2:imm10:imm11:0 (25-bit signed, halfword-aligned)
4137 // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4138 // Input halfword_offset already equals (target - PC - 4) / 2,
4139 // so the full byte offset = halfword_offset << 1.
4140 // The encoding fields split that 25-bit signed value (including the
4141 // implicit trailing zero) as: S | imm10 | imm11
4142 // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4143 let signed_offset = halfword_offset << 1; // byte offset
4144 let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4145 let uoffset = signed_offset as u32;
4146 let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4147 let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4148 let i1 = (uoffset >> 23) & 1; // bit 23
4149 let i2 = (uoffset >> 22) & 1; // bit 22
4150 let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4151 let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4152
4153 let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4154 let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4155
4156 let mut bytes = hw1.to_le_bytes().to_vec();
4157 bytes.extend_from_slice(&hw2.to_le_bytes());
4158 Ok(bytes)
4159 }
4160 }
4161
4162 // Conditional branch with numeric offset (Thumb-2)
4163 ArmOp::BCondOffset { cond, offset } => {
4164 use synth_synthesis::Condition;
4165 let cond_bits: u16 = match cond {
4166 Condition::EQ => 0x0,
4167 Condition::NE => 0x1,
4168 Condition::HS => 0x2,
4169 Condition::LO => 0x3,
4170 Condition::HI => 0x8,
4171 Condition::LS => 0x9,
4172 Condition::GE => 0xA,
4173 Condition::LT => 0xB,
4174 Condition::GT => 0xC,
4175 Condition::LE => 0xD,
4176 };
4177
4178 // offset is already the halfword displacement: (target - branch - 4) / 2
4179 // This is the raw imm8 value for 16-bit B<cond> encoding
4180 let halfword_offset = *offset;
4181
4182 // 16-bit B<cond> encoding: 1101 cond imm8
4183 // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4184 if (-128..=127).contains(&halfword_offset) {
4185 let imm8 = (halfword_offset as u16) & 0xFF;
4186 let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4187 Ok(instr.to_le_bytes().to_vec())
4188 } else {
4189 // 32-bit B<cond>.W (encoding T3) for larger offsets
4190 // First halfword: 1111 0 S cond(4) imm6
4191 // Second halfword: 10 J1 0 J2 imm11
4192 //
4193 // Per ARMv7-M, the branch BYTE offset is
4194 // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4195 // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4196 // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4197 // [16:11], 17, 18 and 19 directly (mirroring the T4
4198 // unconditional arm above).
4199 //
4200 // #740: this arm previously packed `halfword_offset >> 1`
4201 // into imm6:imm11 — HALVING the displacement — so every
4202 // wide conditional branch (span > 254 bytes) landed at half
4203 // its intended offset: gust_poll's loop-head `br_if` to an
4204 // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4205 // encodings were unaffected, which is why short-range CF
4206 // fixtures never caught it.
4207 if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4208 return Err(synth_core::Error::synthesis(format!(
4209 "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4210 the signed 20-bit encoding range (±1 MB) — refusing to \
4211 emit a truncated branch"
4212 )));
4213 }
4214 let u = halfword_offset as u32;
4215 let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4216 let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4217 let j1 = (u >> 17) & 1; // bit 17
4218 let j2 = (u >> 18) & 1; // bit 18
4219 let s = (u >> 19) & 1; // sign (range-checked above)
4220
4221 let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4222 let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4223
4224 let mut bytes = hw1.to_le_bytes().to_vec();
4225 bytes.extend_from_slice(&hw2.to_le_bytes());
4226 Ok(bytes)
4227 }
4228 }
4229
4230 ArmOp::Bl { label: _ } => {
4231 // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4232 // placeholder; an R_ARM_THM_CALL relocation patches the target
4233 // (see arm_backend.rs). The placeholder must carry an embedded
4234 // addend of -4 so the relocation nets to exactly the symbol S.
4235 //
4236 // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4237 // R_ARM_THM_CALL the linker resolves using the in-place addend;
4238 // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4239 // instruction past the callee entry (#174). The correct
4240 // placeholder is what `gas` emits for `bl <extern>`:
4241 // f7ff fffe -> `bl <self>` (S=1, J1=J2=1, imm = -4 addend),
4242 // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4243 // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4244 // the garbage `bl c0000c` and "truncated to fit" of #167.)
4245 let hw1: u16 = 0xF7FF;
4246 let hw2: u16 = 0xFFFE;
4247 let mut bytes = hw1.to_le_bytes().to_vec();
4248 bytes.extend_from_slice(&hw2.to_le_bytes());
4249 Ok(bytes)
4250 }
4251
4252 // MVN
4253 ArmOp::Mvn { rd, op2 } => {
4254 if let Operand2::Reg(rm) = op2 {
4255 let rd_bits = reg_to_bits(rd) as u16;
4256 let rm_bits = reg_to_bits(rm) as u16;
4257
4258 if rd_bits < 8 && rm_bits < 8 {
4259 // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4260 let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4261 Ok(instr.to_le_bytes().to_vec())
4262 } else {
4263 // 32-bit MVN
4264 let hw1: u16 = 0xEA6F_u16;
4265 let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4266 let mut bytes = hw1.to_le_bytes().to_vec();
4267 bytes.extend_from_slice(&hw2.to_le_bytes());
4268 Ok(bytes)
4269 }
4270 } else {
4271 let instr: u16 = 0xBF00;
4272 Ok(instr.to_le_bytes().to_vec())
4273 }
4274 }
4275
4276 // MOVW - Move Wide (Thumb-2 32-bit)
4277 ArmOp::Movw { rd, imm16 } => {
4278 self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4279 }
4280
4281 // MOVT - Move Top (Thumb-2 32-bit)
4282 ArmOp::Movt { rd, imm16 } => {
4283 self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4284 }
4285
4286 // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4287 // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4288 // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4289 // symbol's final address to the in-place addend (REL semantics).
4290 ArmOp::MovwSym { rd, addend, .. } => {
4291 self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4292 }
4293 ArmOp::MovtSym { rd, addend, .. } => {
4294 self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4295 }
4296
4297 // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4298 // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4299 // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4300 // relocation against `symbol+addend`, and patches the imm12 with the
4301 // real PC-relative distance once the pool offset is known.
4302 // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4303 // base = Align(PC,4) and PC = address of this instruction + 4.
4304 ArmOp::LdrSym { rd, .. } => {
4305 let rt = reg_to_bits(rd) as u16;
4306 let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4307 let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4308 let mut bytes = Vec::with_capacity(4);
4309 bytes.extend_from_slice(&hw1.to_le_bytes());
4310 bytes.extend_from_slice(&hw2.to_le_bytes());
4311 Ok(bytes)
4312 }
4313
4314 // SetCond: Materialize condition flag into register (0 or 1)
4315 // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4316 // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4317 // always sets flags (MOVS). We need to evaluate the condition BEFORE
4318 // any MOV instruction clobbers the flags from CMP.
4319 ArmOp::SetCond { rd, cond } => {
4320 let rd_bits = reg_to_bits(rd) as u16;
4321
4322 // Condition code encoding for IT block
4323 use synth_synthesis::Condition;
4324 let cond_bits: u16 = match cond {
4325 Condition::EQ => 0x0,
4326 Condition::NE => 0x1,
4327 Condition::LT => 0xB,
4328 Condition::LE => 0xD,
4329 Condition::GT => 0xC,
4330 Condition::GE => 0xA,
4331 Condition::LO => 0x3, // CC/LO (unsigned <)
4332 Condition::LS => 0x9, // LS (unsigned <=)
4333 Condition::HI => 0x8, // HI (unsigned >)
4334 Condition::HS => 0x2, // CS/HS (unsigned >=)
4335 };
4336
4337 // ITE <cond>: encodes If-Then-Else block
4338 // The mask field depends on firstcond[0]:
4339 // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4340 // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4341 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4342 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4343
4344 // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4345 // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4346 // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4347 // turns MOVS into CMP (00100 → 00101), corrupting the result
4348 // (this mis-materialized gale's `has_waiter`, so its `local.set`
4349 // stored a stale register → the binary-sem WAKE dispatch read
4350 // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4351 // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4352 // is fine inside the ITE (the materialized value is the result;
4353 // the flags are not consumed afterwards).
4354 let mut bytes = ite_instr.to_le_bytes().to_vec();
4355 let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4356 if rd_bits <= 7 {
4357 let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4358 bytes.extend_from_slice(&m.to_le_bytes());
4359 } else {
4360 // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4361 let hw1: u16 = 0xF04F;
4362 let hw2: u16 = (rd_bits << 8) | imm;
4363 bytes.extend_from_slice(&hw1.to_le_bytes());
4364 bytes.extend_from_slice(&hw2.to_le_bytes());
4365 }
4366 };
4367 push_mov(&mut bytes, 1); // Then branch (condition true) → 1
4368 push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4369 Ok(bytes)
4370 }
4371
4372 // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4373 // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4374 // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4375 // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4376 ArmOp::I64SetCond {
4377 rd,
4378 rn_lo,
4379 rn_hi,
4380 rm_lo,
4381 rm_hi,
4382 cond,
4383 } => {
4384 use synth_synthesis::Condition;
4385 let rd_bits = reg_to_bits(rd) as u16;
4386 let mut bytes = Vec::new();
4387
4388 // Helper: encode CMP Rn, Rm (16-bit)
4389 let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4390 rm: &synth_synthesis::Reg|
4391 -> Vec<u8> {
4392 let rn_bits = reg_to_bits(rn) as u16;
4393 let rm_bits = reg_to_bits(rm) as u16;
4394 if rn_bits < 8 && rm_bits < 8 {
4395 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4396 instr.to_le_bytes().to_vec()
4397 } else {
4398 let n_bit = (rn_bits >> 3) & 1;
4399 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4400 instr.to_le_bytes().to_vec()
4401 }
4402 };
4403
4404 // Helper: encode ITE <cond> (2 bytes)
4405 let encode_ite = |cond_bits: u16| -> Vec<u8> {
4406 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4407 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4408 ite_instr.to_le_bytes().to_vec()
4409 };
4410
4411 // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4412 let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4413 let mut b = encode_ite(cond_bits);
4414 if rd_bits < 8 {
4415 let mov_one: u16 = 0x2001 | (rd_bits << 8);
4416 let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4417 b.extend_from_slice(&mov_one.to_le_bytes());
4418 b.extend_from_slice(&mov_zero.to_le_bytes());
4419 } else {
4420 // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4421 // rd field; rd_bits<<8 overflows into bit 11 and
4422 // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4423 // CMP r0,#1): the boolean dies in the flags and the
4424 // consumer reads a stale register. Use the 32-bit
4425 // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4426 // flag-preserving. Same class as H-CODE-9 / #180.
4427 for imm in [1u16, 0u16] {
4428 let hw1: u16 = 0xF04F;
4429 let hw2: u16 = (rd_bits << 8) | imm;
4430 b.extend_from_slice(&hw1.to_le_bytes());
4431 b.extend_from_slice(&hw2.to_le_bytes());
4432 }
4433 }
4434 b
4435 };
4436
4437 match cond {
4438 Condition::EQ | Condition::NE => {
4439 // CMP rn_lo, rm_lo (compare low words)
4440 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4441
4442 // IT EQ (execute next instruction only if Z=1)
4443 let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4444 bytes.extend_from_slice(&it_eq.to_le_bytes());
4445
4446 // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4447 bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4448
4449 // ITE <cond>; MOV rd, #1; MOV rd, #0
4450 let cond_bits: u16 = match cond {
4451 Condition::EQ => 0x0,
4452 Condition::NE => 0x1,
4453 _ => unreachable!(),
4454 };
4455 bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4456 }
4457
4458 Condition::LT => {
4459 // CMP rn_lo, rm_lo (sets C flag for borrow)
4460 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4461
4462 // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4463 // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4464 let rn_hi_bits = reg_to_bits(rn_hi);
4465 let rm_hi_bits = reg_to_bits(rm_hi);
4466 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4467 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4468 bytes.extend_from_slice(&hw1.to_le_bytes());
4469 bytes.extend_from_slice(&hw2.to_le_bytes());
4470
4471 // ITE LT; MOV rd, #1; MOV rd, #0
4472 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4473 }
4474
4475 Condition::GT => {
4476 // GT(a,b) = LT(b,a): swap operands
4477 // CMP rm_lo, rn_lo (swapped)
4478 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4479
4480 // SBCS rd, rm_hi, rn_hi (swapped)
4481 let rm_hi_bits = reg_to_bits(rm_hi);
4482 let rn_hi_bits = reg_to_bits(rn_hi);
4483 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4484 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4485 bytes.extend_from_slice(&hw1.to_le_bytes());
4486 bytes.extend_from_slice(&hw2.to_le_bytes());
4487
4488 // ITE LT; MOV rd, #1; MOV rd, #0
4489 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4490 }
4491
4492 Condition::LE => {
4493 // LE(a,b) = !GT(a,b): use GT logic but invert result
4494 // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4495 // CMP rm_lo, rn_lo (swapped, same as GT)
4496 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4497
4498 // SBCS rd, rm_hi, rn_hi (swapped)
4499 let rm_hi_bits = reg_to_bits(rm_hi);
4500 let rn_hi_bits = reg_to_bits(rn_hi);
4501 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4502 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4503 bytes.extend_from_slice(&hw1.to_le_bytes());
4504 bytes.extend_from_slice(&hw2.to_le_bytes());
4505
4506 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4507 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4508 }
4509
4510 Condition::GE => {
4511 // GE(a,b) = !LT(a,b): use LT logic but invert result
4512 // CMP rn_lo, rm_lo (same as LT)
4513 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4514
4515 // SBCS rd, rn_hi, rm_hi (same as LT)
4516 let rn_hi_bits = reg_to_bits(rn_hi);
4517 let rm_hi_bits = reg_to_bits(rm_hi);
4518 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4519 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4520 bytes.extend_from_slice(&hw1.to_le_bytes());
4521 bytes.extend_from_slice(&hw2.to_le_bytes());
4522
4523 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4524 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4525 }
4526
4527 // Unsigned comparisons - same instruction sequence, different conditions
4528 Condition::LO => {
4529 // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4530 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4531 let rn_hi_bits = reg_to_bits(rn_hi);
4532 let rm_hi_bits = reg_to_bits(rm_hi);
4533 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4534 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4535 bytes.extend_from_slice(&hw1.to_le_bytes());
4536 bytes.extend_from_slice(&hw2.to_le_bytes());
4537 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4538 }
4539
4540 Condition::HI => {
4541 // HI (unsigned GT): swap operands and check LO
4542 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4543 let rm_hi_bits = reg_to_bits(rm_hi);
4544 let rn_hi_bits = reg_to_bits(rn_hi);
4545 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4546 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4547 bytes.extend_from_slice(&hw1.to_le_bytes());
4548 bytes.extend_from_slice(&hw2.to_le_bytes());
4549 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4550 }
4551
4552 Condition::LS => {
4553 // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4554 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4555 let rm_hi_bits = reg_to_bits(rm_hi);
4556 let rn_hi_bits = reg_to_bits(rn_hi);
4557 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4558 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4559 bytes.extend_from_slice(&hw1.to_le_bytes());
4560 bytes.extend_from_slice(&hw2.to_le_bytes());
4561 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4562 }
4563
4564 Condition::HS => {
4565 // HS (unsigned GE): !(a < b) = !(LO)
4566 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4567 let rn_hi_bits = reg_to_bits(rn_hi);
4568 let rm_hi_bits = reg_to_bits(rm_hi);
4569 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4570 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4571 bytes.extend_from_slice(&hw1.to_le_bytes());
4572 bytes.extend_from_slice(&hw2.to_le_bytes());
4573 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4574 }
4575 }
4576
4577 Ok(bytes)
4578 }
4579
4580 // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4581 // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4582 ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4583 let rd_bits = reg_to_bits(rd);
4584 let rn_lo_bits = reg_to_bits(rn_lo);
4585 let rn_hi_bits = reg_to_bits(rn_hi);
4586 let mut bytes = Vec::new();
4587
4588 // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4589 let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4590 let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4591 bytes.extend_from_slice(&hw1.to_le_bytes());
4592 bytes.extend_from_slice(&hw2.to_le_bytes());
4593
4594 // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4595 // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4596 // H-CODE-9: rd_bits<<8 overflowing the field compared the
4597 // WRONG register. Same hardening as the #311 SetCond fix.
4598 if rd_bits < 8 {
4599 let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4600 bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4601 } else {
4602 let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4603 let hw2: u16 = 0x0F00;
4604 bytes.extend_from_slice(&hw1.to_le_bytes());
4605 bytes.extend_from_slice(&hw2.to_le_bytes());
4606 }
4607
4608 // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4609 // #311 — see I64SetCond)
4610 let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4611 let ite_instr: u16 = 0xBF00 | mask;
4612 bytes.extend_from_slice(&ite_instr.to_le_bytes());
4613 if rd_bits < 8 {
4614 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4615 let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4616 bytes.extend_from_slice(&mov_one.to_le_bytes());
4617 bytes.extend_from_slice(&mov_zero.to_le_bytes());
4618 } else {
4619 for imm in [1u16, 0u16] {
4620 let hw1: u16 = 0xF04F;
4621 let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4622 bytes.extend_from_slice(&hw1.to_le_bytes());
4623 bytes.extend_from_slice(&hw2.to_le_bytes());
4624 }
4625 }
4626
4627 Ok(bytes)
4628 }
4629
4630 // I64Mul: 64-bit multiply using UMULL + MLA cross products
4631 // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4632 // Uses R12 as scratch register
4633 ArmOp::I64Mul {
4634 rd_lo,
4635 rd_hi,
4636 rn_lo,
4637 rn_hi,
4638 rm_lo,
4639 rm_hi,
4640 } => {
4641 let rd_lo_bits = reg_to_bits(rd_lo);
4642 let rd_hi_bits = reg_to_bits(rd_hi);
4643 let rn_lo_bits = reg_to_bits(rn_lo);
4644 let rn_hi_bits = reg_to_bits(rn_hi);
4645 let rm_lo_bits = reg_to_bits(rm_lo);
4646 let rm_hi_bits = reg_to_bits(rm_hi);
4647 let r12: u32 = 12; // IP scratch register
4648 let mut bytes = Vec::new();
4649
4650 // 1. MUL R12, rn_lo, rm_hi (R12 = a_lo * b_hi)
4651 // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4652 let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4653 let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4654 bytes.extend_from_slice(&hw1.to_le_bytes());
4655 bytes.extend_from_slice(&hw2.to_le_bytes());
4656
4657 // 2. MLA R12, rn_hi, rm_lo, R12 (R12 += a_hi * b_lo)
4658 // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4659 let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4660 let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4661 bytes.extend_from_slice(&hw1.to_le_bytes());
4662 bytes.extend_from_slice(&hw2.to_le_bytes());
4663
4664 // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo (rd_lo:rd_hi = a_lo * b_lo)
4665 // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4666 let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4667 let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4668 bytes.extend_from_slice(&hw1.to_le_bytes());
4669 bytes.extend_from_slice(&hw2.to_le_bytes());
4670
4671 // 4. ADD rd_hi, R12 (rd_hi += cross products)
4672 // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4673 let d_bit = (rd_hi_bits >> 3) & 1;
4674 let add_instr: u16 =
4675 (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4676 bytes.extend_from_slice(&add_instr.to_le_bytes());
4677
4678 Ok(bytes)
4679 }
4680
4681 // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4682 //
4683 // #1048: the expansion must NEVER write its own input operands.
4684 // The pre-#1048 sequence masked the amount IN PLACE
4685 // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4686 // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4687 // LSR) — so re-reading the amount after the shift returned a
4688 // mangled value (amt=64 read back 0, amt=67 read back 3). The
4689 // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4690 // as the ONLY temporary, re-deriving the masked amount from the
4691 // untouched rm_lo whenever a second live temp would otherwise be
4692 // needed. This matches the Rocq/SMT pseudo-op models
4693 // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4694 // exactly this non-clobbering contract all along.
4695 ArmOp::I64Shl {
4696 rd_lo,
4697 rd_hi,
4698 rn_lo,
4699 rn_hi,
4700 rm_lo,
4701 rm_hi: _,
4702 } => {
4703 let rd_lo_bits = reg_to_bits(rd_lo);
4704 let rd_hi_bits = reg_to_bits(rd_hi);
4705 let rn_lo_bits = reg_to_bits(rn_lo);
4706 let rn_hi_bits = reg_to_bits(rn_hi);
4707 let rm_lo_bits = reg_to_bits(rm_lo);
4708 let r12: u32 = 12; // the only scratch — never allocatable
4709 let mut bytes = Vec::new();
4710
4711 // #1039 house style: refuse a destination that would collide
4712 // with an input still needed after the destination is first
4713 // written, loudly — never misassemble. rd_hi is written before
4714 // rn_lo and rm_lo are last read; the in-place form
4715 // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4716 if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4717 return Err(synth_core::Error::synthesis(format!(
4718 "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4719 )));
4720 }
4721
4722 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4723 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4724 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4725 bytes.extend_from_slice(&hw1.to_le_bytes());
4726 bytes.extend_from_slice(&hw2.to_le_bytes());
4727
4728 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4729 let hw1: u16 = (0xF1B0 | r12) as u16;
4730 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4731 bytes.extend_from_slice(&hw1.to_le_bytes());
4732 bytes.extend_from_slice(&hw2.to_le_bytes());
4733
4734 // BPL .large (branch if n >= 32, offset = +14 halfwords)
4735 let bpl: u16 = 0xD50E;
4736 bytes.extend_from_slice(&bpl.to_le_bytes());
4737
4738 // --- Small shift (n < 32) ---
4739 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4740 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4741 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4742 bytes.extend_from_slice(&hw1.to_le_bytes());
4743 bytes.extend_from_slice(&hw2.to_le_bytes());
4744
4745 // LSL.W rd_hi, rn_hi, R12 (hi << n; rn_hi's last read)
4746 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4747 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4748 bytes.extend_from_slice(&hw1.to_le_bytes());
4749 bytes.extend_from_slice(&hw2.to_le_bytes());
4750
4751 // RSB.W R12, R12, #32 (R12 = 32-n)
4752 let hw1: u16 = (0xF1C0 | r12) as u16;
4753 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4754 bytes.extend_from_slice(&hw1.to_le_bytes());
4755 bytes.extend_from_slice(&hw2.to_le_bytes());
4756
4757 // LSR.W R12, rn_lo, R12 (overflow = lo >> (32-n); n=0 gives
4758 // a register shift by 32 which yields 0 — exact)
4759 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4760 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4761 bytes.extend_from_slice(&hw1.to_le_bytes());
4762 bytes.extend_from_slice(&hw2.to_le_bytes());
4763
4764 // ORR.W rd_hi, rd_hi, R12 (hi |= overflow bits from lo)
4765 let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4766 let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4767 bytes.extend_from_slice(&hw1.to_le_bytes());
4768 bytes.extend_from_slice(&hw2.to_le_bytes());
4769
4770 // AND.W R12, rm_lo, #63 (n once more for the low half)
4771 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4772 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4773 bytes.extend_from_slice(&hw1.to_le_bytes());
4774 bytes.extend_from_slice(&hw2.to_le_bytes());
4775
4776 // LSL.W rd_lo, rn_lo, R12 (lo << n)
4777 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4778 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4779 bytes.extend_from_slice(&hw1.to_le_bytes());
4780 bytes.extend_from_slice(&hw2.to_le_bytes());
4781
4782 // B .done — `.done` is the END of the expansion, i.e. PAST the
4783 // large-shift arm's trailing zero-fill. #916: that zero-fill is
4784 // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4785 // displacement is DERIVED from its real width instead of the
4786 // hard-coded 0xE002 — widening the MOV without this would
4787 // overshoot `.done` and turn a data miscompile into a
4788 // control-flow one. Thumb `B` reads PC as its own address + 4
4789 // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4790 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4791 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4792 bytes.extend_from_slice(&b_done.to_le_bytes());
4793
4794 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4795 // LSL.W rd_hi, rn_lo, R12 (hi = lo << (n-32))
4796 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4797 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4798 bytes.extend_from_slice(&hw1.to_le_bytes());
4799 bytes.extend_from_slice(&hw2.to_le_bytes());
4800
4801 // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4802 // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4803 // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4804 // cannot be reordered to dodge the displacement change.
4805 emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4806
4807 Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4808 }
4809
4810 // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4811 // n>=32. #1048: R12-only scratch, operands never written — see
4812 // the I64Shl comment for the full rationale.
4813 ArmOp::I64ShrU {
4814 rd_lo,
4815 rd_hi,
4816 rn_lo,
4817 rn_hi,
4818 rm_lo,
4819 rm_hi: _,
4820 } => {
4821 let rd_lo_bits = reg_to_bits(rd_lo);
4822 let rd_hi_bits = reg_to_bits(rd_hi);
4823 let rn_lo_bits = reg_to_bits(rn_lo);
4824 let rn_hi_bits = reg_to_bits(rn_hi);
4825 let rm_lo_bits = reg_to_bits(rm_lo);
4826 let r12: u32 = 12; // the only scratch — never allocatable
4827 let mut bytes = Vec::new();
4828
4829 // #1039 house style: rd_lo is written before rn_hi and rm_lo
4830 // are last read — refuse the collision loudly. The in-place
4831 // form rd == rn (select_default) has rd_lo == rn_lo and stays
4832 // legal.
4833 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4834 return Err(synth_core::Error::synthesis(format!(
4835 "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4836 )));
4837 }
4838
4839 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4840 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4841 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4842 bytes.extend_from_slice(&hw1.to_le_bytes());
4843 bytes.extend_from_slice(&hw2.to_le_bytes());
4844
4845 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4846 let hw1: u16 = (0xF1B0 | r12) as u16;
4847 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4848 bytes.extend_from_slice(&hw1.to_le_bytes());
4849 bytes.extend_from_slice(&hw2.to_le_bytes());
4850
4851 // BPL .large (+14 halfwords)
4852 let bpl: u16 = 0xD50E;
4853 bytes.extend_from_slice(&bpl.to_le_bytes());
4854
4855 // --- Small shift (n < 32) ---
4856 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4857 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4858 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4859 bytes.extend_from_slice(&hw1.to_le_bytes());
4860 bytes.extend_from_slice(&hw2.to_le_bytes());
4861
4862 // LSR.W rd_lo, rn_lo, R12 (lo >> n; rn_lo's last read)
4863 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4864 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4865 bytes.extend_from_slice(&hw1.to_le_bytes());
4866 bytes.extend_from_slice(&hw2.to_le_bytes());
4867
4868 // RSB.W R12, R12, #32 (R12 = 32-n)
4869 let hw1: u16 = (0xF1C0 | r12) as u16;
4870 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4871 bytes.extend_from_slice(&hw1.to_le_bytes());
4872 bytes.extend_from_slice(&hw2.to_le_bytes());
4873
4874 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
4875 // a register shift by 32 which yields 0 — exact)
4876 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4877 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4878 bytes.extend_from_slice(&hw1.to_le_bytes());
4879 bytes.extend_from_slice(&hw2.to_le_bytes());
4880
4881 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
4882 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4883 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4884 bytes.extend_from_slice(&hw1.to_le_bytes());
4885 bytes.extend_from_slice(&hw2.to_le_bytes());
4886
4887 // AND.W R12, rm_lo, #63 (n once more for the high half)
4888 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4889 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4890 bytes.extend_from_slice(&hw1.to_le_bytes());
4891 bytes.extend_from_slice(&hw2.to_le_bytes());
4892
4893 // LSR.W rd_hi, rn_hi, R12 (hi >> n, logical)
4894 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4895 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4896 bytes.extend_from_slice(&hw1.to_le_bytes());
4897 bytes.extend_from_slice(&hw2.to_le_bytes());
4898
4899 // B .done — see I64Shl: `.done` is the END of the expansion,
4900 // past the trailing zero-fill, so the displacement is derived
4901 // from that zero-fill's real width (#916).
4902 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4903 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4904 bytes.extend_from_slice(&b_done.to_le_bytes());
4905
4906 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4907 // LSR.W rd_lo, rn_hi, R12 (lo = hi >> (n-32))
4908 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4909 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4910 bytes.extend_from_slice(&hw1.to_le_bytes());
4911 bytes.extend_from_slice(&hw2.to_le_bytes());
4912
4913 // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4914 // load-bearing: the LSR.W above reads rn_hi, which may BE
4915 // rd_hi in the in-place case.
4916 emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4917
4918 Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4919 }
4920
4921 // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4922 // n>=32. #1048: R12-only scratch, operands never written — see
4923 // the I64Shl comment for the full rationale.
4924 ArmOp::I64ShrS {
4925 rd_lo,
4926 rd_hi,
4927 rn_lo,
4928 rn_hi,
4929 rm_lo,
4930 rm_hi: _,
4931 } => {
4932 let rd_lo_bits = reg_to_bits(rd_lo);
4933 let rd_hi_bits = reg_to_bits(rd_hi);
4934 let rn_lo_bits = reg_to_bits(rn_lo);
4935 let rn_hi_bits = reg_to_bits(rn_hi);
4936 let rm_lo_bits = reg_to_bits(rm_lo);
4937 let r12: u32 = 12; // the only scratch — never allocatable
4938 let mut bytes = Vec::new();
4939
4940 // #1039 house style: rd_lo is written before rn_hi and rm_lo
4941 // are last read (on BOTH arms of the diamond — the large arm's
4942 // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
4943 // rd_lo is written). The in-place form rd == rn stays legal.
4944 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4945 return Err(synth_core::Error::synthesis(format!(
4946 "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4947 )));
4948 }
4949
4950 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4951 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4952 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4953 bytes.extend_from_slice(&hw1.to_le_bytes());
4954 bytes.extend_from_slice(&hw2.to_le_bytes());
4955
4956 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4957 let hw1: u16 = (0xF1B0 | r12) as u16;
4958 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4959 bytes.extend_from_slice(&hw1.to_le_bytes());
4960 bytes.extend_from_slice(&hw2.to_le_bytes());
4961
4962 // BPL .large (+14 halfwords)
4963 let bpl: u16 = 0xD50E;
4964 bytes.extend_from_slice(&bpl.to_le_bytes());
4965
4966 // --- Small shift (n < 32) ---
4967 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4968 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4969 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4970 bytes.extend_from_slice(&hw1.to_le_bytes());
4971 bytes.extend_from_slice(&hw2.to_le_bytes());
4972
4973 // LSR.W rd_lo, rn_lo, R12 (lo >> n, logical for lo word)
4974 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4975 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4976 bytes.extend_from_slice(&hw1.to_le_bytes());
4977 bytes.extend_from_slice(&hw2.to_le_bytes());
4978
4979 // RSB.W R12, R12, #32 (R12 = 32-n)
4980 let hw1: u16 = (0xF1C0 | r12) as u16;
4981 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4982 bytes.extend_from_slice(&hw1.to_le_bytes());
4983 bytes.extend_from_slice(&hw2.to_le_bytes());
4984
4985 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
4986 // a register shift by 32 which yields 0 — exact)
4987 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4988 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4989 bytes.extend_from_slice(&hw1.to_le_bytes());
4990 bytes.extend_from_slice(&hw2.to_le_bytes());
4991
4992 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
4993 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4994 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4995 bytes.extend_from_slice(&hw1.to_le_bytes());
4996 bytes.extend_from_slice(&hw2.to_le_bytes());
4997
4998 // AND.W R12, rm_lo, #63 (n once more for the high half)
4999 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
5000 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
5001 bytes.extend_from_slice(&hw1.to_le_bytes());
5002 bytes.extend_from_slice(&hw2.to_le_bytes());
5003
5004 // ASR.W rd_hi, rn_hi, R12 (hi >> n, arithmetic/sign-extending)
5005 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5006 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
5007 bytes.extend_from_slice(&hw1.to_le_bytes());
5008 bytes.extend_from_slice(&hw2.to_le_bytes());
5009
5010 // B .done (+3 halfwords, large shift is 8 bytes)
5011 let b_done: u16 = 0xE003;
5012 bytes.extend_from_slice(&b_done.to_le_bytes());
5013
5014 // --- Large shift (n >= 32) --- (R12 still holds n-32)
5015 // ASR.W rd_lo, rn_hi, R12 (lo = hi >>> (n-32))
5016 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
5017 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
5018 bytes.extend_from_slice(&hw1.to_le_bytes());
5019 bytes.extend_from_slice(&hw2.to_le_bytes());
5020
5021 // ASR.W rd_hi, rn_hi, #31 (hi = sign extension, all 0s or all 1s)
5022 // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
5023 // imm5=31=11111 → imm3=111, imm2=11
5024 let hw1: u16 = 0xEA4F;
5025 let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
5026 bytes.extend_from_slice(&hw1.to_le_bytes());
5027 bytes.extend_from_slice(&hw2.to_le_bytes());
5028
5029 Ok(bytes) // Total: 48 bytes
5030 }
5031
5032 // I64Rotl: 64-bit rotate left (#610 rewrite).
5033 // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5034 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5035 //
5036 // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5037 // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5038 // pre-#610 expansion wrote through the selector's registers with
5039 // colliding R3/R4 scratch and restored the saved R4 OVER the
5040 // result). Relies on ARM register-shift semantics: amounts >= 32
5041 // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5042 ArmOp::I64Rotl {
5043 rdlo,
5044 rdhi,
5045 rnlo,
5046 rnhi,
5047 shift,
5048 } => {
5049 let mut bytes = Vec::new();
5050 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5051
5052 let core: [u16; 35] = [
5053 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5054 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5055 0xD50E, // BPL .large (n >= 32)
5056 // --- small rotation (n < 32) ---
5057 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5058 0xFA20, 0xFC03, // LSR.W R12, R0, R3 (lo >> (32-n))
5059 0xFA21, 0xF303, // LSR.W R3, R1, R3 (hi >> (32-n))
5060 0xFA01, 0xF102, // LSL.W R1, R1, R2 (hi << n)
5061 0xEA41, 0x010C, // ORR.W R1, R1, R12 (new_hi)
5062 0xFA00, 0xF002, // LSL.W R0, R0, R2 (lo << n)
5063 0xEA40, 0x0003, // ORR.W R0, R0, R3 (new_lo)
5064 0xE00E, // B .done
5065 // --- large rotation (n >= 32), R3 = m = n-32 ---
5066 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5067 0xFA21, 0xFC02, // LSR.W R12, R1, R2 (hi >> (64-n))
5068 0xFA20, 0xF202, // LSR.W R2, R0, R2 (lo >> (64-n))
5069 0xFA00, 0xF003, // LSL.W R0, R0, R3 (lo << m)
5070 0xFA01, 0xF103, // LSL.W R1, R1, R3 (hi << m)
5071 0xEA40, 0x0C0C, // ORR.W R12, R0, R12 (new_hi = (lo<<m)|(hi>>(64-n)))
5072 0xEA41, 0x0002, // ORR.W R0, R1, R2 (new_lo = (hi<<m)|(lo>>(64-n)))
5073 0x4661, // MOV R1, R12 (new_hi into place)
5074 // .done: result in R0:R1
5075 ];
5076 for hw in core {
5077 bytes.extend_from_slice(&hw.to_le_bytes());
5078 }
5079
5080 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5081 Ok(bytes) // Total: 102 bytes
5082 }
5083
5084 // I64Rotr: 64-bit rotate right (#610 rewrite).
5085 // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5086 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5087 //
5088 // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5089 // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5090 ArmOp::I64Rotr {
5091 rdlo,
5092 rdhi,
5093 rnlo,
5094 rnhi,
5095 shift,
5096 } => {
5097 let mut bytes = Vec::new();
5098 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5099
5100 let core: [u16; 35] = [
5101 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5102 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5103 0xD50E, // BPL .large (n >= 32)
5104 // --- small rotation (n < 32) ---
5105 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5106 0xFA01, 0xFC03, // LSL.W R12, R1, R3 (hi << (32-n))
5107 0xFA00, 0xF303, // LSL.W R3, R0, R3 (lo << (32-n))
5108 0xFA20, 0xF002, // LSR.W R0, R0, R2 (lo >> n)
5109 0xEA40, 0x000C, // ORR.W R0, R0, R12 (new_lo)
5110 0xFA21, 0xF102, // LSR.W R1, R1, R2 (hi >> n)
5111 0xEA41, 0x0103, // ORR.W R1, R1, R3 (new_hi)
5112 0xE00E, // B .done
5113 // --- large rotation (n >= 32), R3 = m = n-32 ---
5114 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5115 0xFA00, 0xFC02, // LSL.W R12, R0, R2 (lo << (64-n))
5116 0xFA01, 0xF202, // LSL.W R2, R1, R2 (hi << (64-n))
5117 0xFA21, 0xF103, // LSR.W R1, R1, R3 (hi >> m)
5118 0xEA41, 0x0C0C, // ORR.W R12, R1, R12 (new_lo = (hi>>m)|(lo<<(64-n)))
5119 0xFA20, 0xF103, // LSR.W R1, R0, R3 (lo >> m)
5120 0xEA41, 0x0102, // ORR.W R1, R1, R2 (new_hi = (lo>>m)|(hi<<(64-n)))
5121 0x4660, // MOV R0, R12 (new_lo into place)
5122 // .done: result in R0:R1
5123 ];
5124 for hw in core {
5125 bytes.extend_from_slice(&hw.to_le_bytes());
5126 }
5127
5128 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5129 Ok(bytes) // Total: 102 bytes
5130 }
5131
5132 // I64Clz: Count leading zeros in 64-bit value
5133 // If hi != 0: result = CLZ(hi)
5134 // If hi == 0: result = 32 + CLZ(lo)
5135 //
5136 // Layout (using CMP+BNE approach for consistency):
5137 // 0: CMP.W rnhi, #0 (4 bytes)
5138 // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5139 // 6: CLZ.W rd, rnhi (4 bytes)
5140 // 10: B .done (2 bytes) - branch forward to offset 22
5141 // 12: NOP (2 bytes) - padding for alignment
5142 // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5143 // 18: ADD.W rd, rd, #32 (4 bytes)
5144 // 22: .done
5145 ArmOp::I64Clz { rd, rnlo, rnhi } => {
5146 let rd_bits = reg_to_bits(rd);
5147 let rn_lo_bits = reg_to_bits(rnlo);
5148 let rn_hi_bits = reg_to_bits(rnhi);
5149 let mut bytes = Vec::new();
5150
5151 // CMP.W rnhi, #0 (4 bytes at offset 0)
5152 let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5153 let hw2: u16 = 0x0F00;
5154 bytes.extend_from_slice(&hw1.to_le_bytes());
5155 bytes.extend_from_slice(&hw2.to_le_bytes());
5156
5157 // BEQ .hi_zero (2 bytes at offset 4)
5158 // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5159 let beq: u16 = 0xD003;
5160 bytes.extend_from_slice(&beq.to_le_bytes());
5161
5162 // CLZ.W rd, rnhi (4 bytes at offset 6)
5163 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5164 let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5165 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5166 bytes.extend_from_slice(&hw1.to_le_bytes());
5167 bytes.extend_from_slice(&hw2.to_le_bytes());
5168
5169 // B .done (2 bytes at offset 10)
5170 // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5171 let b_done: u16 = 0xE004;
5172 bytes.extend_from_slice(&b_done.to_le_bytes());
5173
5174 // NOP (2 bytes at offset 12) - padding
5175 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5176
5177 // .hi_zero: (offset 14)
5178 // CLZ.W rd, rnlo (4 bytes)
5179 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5180 let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5181 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5182 bytes.extend_from_slice(&hw1.to_le_bytes());
5183 bytes.extend_from_slice(&hw2.to_le_bytes());
5184
5185 // ADD.W rd, rd, #32 (4 bytes at offset 18)
5186 let hw1: u16 = (0xF100 | rd_bits) as u16;
5187 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5188 bytes.extend_from_slice(&hw1.to_le_bytes());
5189 bytes.extend_from_slice(&hw2.to_le_bytes());
5190
5191 // .done: (offset 22 — the end of the expansion)
5192 //
5193 // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5194 // is GONE. It was aimed at the RESULT's high half but wrote
5195 // the OPERAND's home high register — a real executed
5196 // miscompile on the direct selector, which allocates a fresh
5197 // destination pair and zeroes its own dst_hi, leaving the
5198 // operand's hi limb destroyed for any later re-read. The
5199 // Rocq/SMT models of I64ClzPseudo always said "writes rd
5200 // ONLY"; the callers that relied on the implicit clear
5201 // (select_default, optimizer_bridge) now emit their own
5202 // explicit hi-zero op. `B .done` above targets offset 22 =
5203 // past-the-end, `BEQ` targets offset 14 — no displacement
5204 // moves.
5205
5206 Ok(bytes) // 22 bytes, register-independent
5207 }
5208
5209 // I64Ctz: Count trailing zeros in 64-bit value
5210 // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5211 // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5212 //
5213 // Layout:
5214 // 0: CMP.W rnlo, #0 (4 bytes)
5215 // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5216 // 6: RBIT.W rd, rnlo (4 bytes)
5217 // 10: CLZ.W rd, rd (4 bytes)
5218 // 14: B .done (2 bytes) - branch to offset 30
5219 // 16: NOP (2 bytes) - padding
5220 // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5221 // 22: CLZ.W rd, rd (4 bytes)
5222 // 26: ADD.W rd, rd, #32 (4 bytes)
5223 // 30: .done
5224 ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5225 let rd_bits = reg_to_bits(rd);
5226 let rn_lo_bits = reg_to_bits(rnlo);
5227 let rn_hi_bits = reg_to_bits(rnhi);
5228 let mut bytes = Vec::new();
5229
5230 // CMP.W rnlo, #0 (4 bytes at offset 0)
5231 let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5232 let hw2: u16 = 0x0F00;
5233 bytes.extend_from_slice(&hw1.to_le_bytes());
5234 bytes.extend_from_slice(&hw2.to_le_bytes());
5235
5236 // BEQ .lo_zero (2 bytes at offset 4)
5237 // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5238 let beq: u16 = 0xD005;
5239 bytes.extend_from_slice(&beq.to_le_bytes());
5240
5241 // RBIT.W rd, rnlo (4 bytes at offset 6)
5242 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5243 let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5244 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5245 bytes.extend_from_slice(&hw1.to_le_bytes());
5246 bytes.extend_from_slice(&hw2.to_le_bytes());
5247
5248 // CLZ.W rd, rd (4 bytes at offset 10)
5249 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5250 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5251 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5252 bytes.extend_from_slice(&hw1.to_le_bytes());
5253 bytes.extend_from_slice(&hw2.to_le_bytes());
5254
5255 // B .done (2 bytes at offset 14)
5256 // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5257 let b_done: u16 = 0xE006;
5258 bytes.extend_from_slice(&b_done.to_le_bytes());
5259
5260 // NOP (2 bytes at offset 16) - padding
5261 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5262
5263 // .lo_zero: (offset 18)
5264 // RBIT.W rd, rnhi (4 bytes)
5265 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5266 let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5267 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5268 bytes.extend_from_slice(&hw1.to_le_bytes());
5269 bytes.extend_from_slice(&hw2.to_le_bytes());
5270
5271 // CLZ.W rd, rd (4 bytes at offset 22)
5272 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5273 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5274 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5275 bytes.extend_from_slice(&hw1.to_le_bytes());
5276 bytes.extend_from_slice(&hw2.to_le_bytes());
5277
5278 // ADD.W rd, rd, #32 (4 bytes at offset 26)
5279 let hw1: u16 = (0xF100 | rd_bits) as u16;
5280 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5281 bytes.extend_from_slice(&hw1.to_le_bytes());
5282 bytes.extend_from_slice(&hw2.to_le_bytes());
5283
5284 // .done: (offset 30 — the end of the expansion)
5285 // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5286 // wrote the OPERAND's home high register (see the I64Clz
5287 // comment above). `B .done` targets offset 30 = past-the-end,
5288 // `BEQ` targets offset 18 — no displacement moves.
5289
5290 Ok(bytes) // 30 bytes, register-independent
5291 }
5292
5293 // I64Popcnt: Population count of 64-bit value
5294 // result = POPCNT(lo) + POPCNT(hi)
5295 // Using SIMD-style parallel bit counting algorithm
5296 ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5297 let rd_bits = reg_to_bits(rd);
5298 let rn_lo_bits = reg_to_bits(rnlo);
5299 let rn_hi_bits = reg_to_bits(rnhi);
5300 let r12: u32 = 12; // IP scratch
5301 let r3: u32 = 3; // Scratch for hi popcnt result
5302 let mut bytes = Vec::new();
5303
5304 // PUSH {R3, R4, R5} - save scratch registers
5305 bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5306
5307 // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5308 // Using lookup table approach for each byte would be too large
5309 // Using shift-and-add approach instead
5310
5311 // For simplicity and correctness, use the efficient parallel algorithm
5312 // but implement it as a series of inline operations
5313
5314 // Marshal the operand pair into the fixed scratch regs, routing
5315 // rnlo through R12 (#632 audit): writing R4 first corrupted the
5316 // rnhi read for a pair living at (R3,R4) — every source is read
5317 // before any scratch register it could occupy is written.
5318 // MOV R12, rnlo
5319 let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5320 bytes.extend_from_slice(&mov.to_le_bytes());
5321 // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5322 let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5323 bytes.extend_from_slice(&mov.to_le_bytes());
5324 // MOV R4, R12
5325 bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5326
5327 // --- POPCNT for R4 (lo word) ---
5328 // Step 1: x = x - ((x >> 1) & 0x55555555)
5329 // LSR.W R12, R4, #1
5330 let hw1: u16 = 0xEA4F;
5331 let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5332 bytes.extend_from_slice(&hw1.to_le_bytes());
5333 bytes.extend_from_slice(&hw2.to_le_bytes());
5334
5335 // Load 0x55555555 into R3 using MOVW/MOVT
5336 // MOVW R3, #0x5555
5337 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5338 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5339 // MOVT R3, #0x5555
5340 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5341 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5342
5343 // AND.W R12, R12, R3
5344 let hw1: u16 = (0xEA00 | r12) as u16;
5345 let hw2: u16 = ((r12 << 8) | r3) as u16;
5346 bytes.extend_from_slice(&hw1.to_le_bytes());
5347 bytes.extend_from_slice(&hw2.to_le_bytes());
5348
5349 // SUB.W R4, R4, R12
5350 let hw1: u16 = (0xEBA0 | 4) as u16;
5351 let hw2: u16 = ((4 << 8) | r12) as u16;
5352 bytes.extend_from_slice(&hw1.to_le_bytes());
5353 bytes.extend_from_slice(&hw2.to_le_bytes());
5354
5355 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5356 // Load 0x33333333 into R3
5357 // MOVW R3, #0x3333
5358 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5359 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5360 // MOVT R3, #0x3333
5361 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5362 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5363
5364 // AND.W R12, R4, R3
5365 let hw1: u16 = (0xEA00 | 4) as u16;
5366 let hw2: u16 = ((r12 << 8) | r3) as u16;
5367 bytes.extend_from_slice(&hw1.to_le_bytes());
5368 bytes.extend_from_slice(&hw2.to_le_bytes());
5369
5370 // LSR.W R4, R4, #2
5371 let hw1: u16 = 0xEA4F;
5372 let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5373 bytes.extend_from_slice(&hw1.to_le_bytes());
5374 bytes.extend_from_slice(&hw2.to_le_bytes());
5375
5376 // AND.W R4, R4, R3
5377 let hw1: u16 = (0xEA00 | 4) as u16;
5378 let hw2: u16 = ((4 << 8) | r3) as u16;
5379 bytes.extend_from_slice(&hw1.to_le_bytes());
5380 bytes.extend_from_slice(&hw2.to_le_bytes());
5381
5382 // ADD.W R4, R4, R12
5383 let hw1: u16 = (0xEB00 | 4) as u16;
5384 let hw2: u16 = ((4 << 8) | r12) as u16;
5385 bytes.extend_from_slice(&hw1.to_le_bytes());
5386 bytes.extend_from_slice(&hw2.to_le_bytes());
5387
5388 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5389 // LSR.W R12, R4, #4
5390 // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5391 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5392 let hw1: u16 = 0xEA4F;
5393 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5394 bytes.extend_from_slice(&hw1.to_le_bytes());
5395 bytes.extend_from_slice(&hw2.to_le_bytes());
5396
5397 // ADD.W R4, R4, R12
5398 let hw1: u16 = (0xEB00 | 4) as u16;
5399 let hw2: u16 = ((4 << 8) | r12) as u16;
5400 bytes.extend_from_slice(&hw1.to_le_bytes());
5401 bytes.extend_from_slice(&hw2.to_le_bytes());
5402
5403 // Load 0x0F0F0F0F into R3
5404 // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5405 // hw1 = 11110 1 10 0100 0000 = 0xF640
5406 // hw2 = 0 111 0011 00001111 = 0x730F
5407 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5408 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5409 // MOVT R3, #0x0F0F
5410 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5411 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5412
5413 // AND.W R4, R4, R3
5414 let hw1: u16 = (0xEA00 | 4) as u16;
5415 let hw2: u16 = ((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 // Step 4: x = x * 0x01010101 >> 24
5420 // Load 0x01010101 into R3
5421 // MOVW R3, #0x0101
5422 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5423 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5424 // MOVT R3, #0x0101
5425 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5426 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5427
5428 // MUL R4, R4, R3
5429 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5430 let hw1: u16 = (0xFB00 | 4) as u16;
5431 let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5432 bytes.extend_from_slice(&hw1.to_le_bytes());
5433 bytes.extend_from_slice(&hw2.to_le_bytes());
5434
5435 // LSR.W R4, R4, #24
5436 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5437 let hw1: u16 = 0xEA4F;
5438 let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5439 bytes.extend_from_slice(&hw1.to_le_bytes());
5440 bytes.extend_from_slice(&hw2.to_le_bytes());
5441
5442 // --- POPCNT for R5 (hi word) - same algorithm ---
5443 // Step 1
5444 let hw1: u16 = 0xEA4F;
5445 let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5446 bytes.extend_from_slice(&hw1.to_le_bytes());
5447 bytes.extend_from_slice(&hw2.to_le_bytes());
5448
5449 // Load 0x55555555 into R3
5450 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5451 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5452 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5453 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5454
5455 let hw1: u16 = (0xEA00 | r12) 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 = (0xEBA0 | 5) as u16;
5461 let hw2: u16 = ((5 << 8) | r12) as u16;
5462 bytes.extend_from_slice(&hw1.to_le_bytes());
5463 bytes.extend_from_slice(&hw2.to_le_bytes());
5464
5465 // Step 2
5466 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5467 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5468 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5469 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5470
5471 let hw1: u16 = (0xEA00 | 5) as u16;
5472 let hw2: u16 = ((r12 << 8) | r3) as u16;
5473 bytes.extend_from_slice(&hw1.to_le_bytes());
5474 bytes.extend_from_slice(&hw2.to_le_bytes());
5475
5476 let hw1: u16 = 0xEA4F;
5477 let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5478 bytes.extend_from_slice(&hw1.to_le_bytes());
5479 bytes.extend_from_slice(&hw2.to_le_bytes());
5480
5481 let hw1: u16 = (0xEA00 | 5) as u16;
5482 let hw2: u16 = ((5 << 8) | r3) as u16;
5483 bytes.extend_from_slice(&hw1.to_le_bytes());
5484 bytes.extend_from_slice(&hw2.to_le_bytes());
5485
5486 let hw1: u16 = (0xEB00 | 5) as u16;
5487 let hw2: u16 = ((5 << 8) | r12) as u16;
5488 bytes.extend_from_slice(&hw1.to_le_bytes());
5489 bytes.extend_from_slice(&hw2.to_le_bytes());
5490
5491 // Step 3: LSR.W R12, R5, #4
5492 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5493 let hw1: u16 = 0xEA4F;
5494 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5495 bytes.extend_from_slice(&hw1.to_le_bytes());
5496 bytes.extend_from_slice(&hw2.to_le_bytes());
5497
5498 let hw1: u16 = (0xEB00 | 5) as u16;
5499 let hw2: u16 = ((5 << 8) | r12) as u16;
5500 bytes.extend_from_slice(&hw1.to_le_bytes());
5501 bytes.extend_from_slice(&hw2.to_le_bytes());
5502
5503 // Load 0x0F0F0F0F into R3 (for hi-word)
5504 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5505 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5506 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5507 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5508
5509 let hw1: u16 = (0xEA00 | 5) as u16;
5510 let hw2: u16 = ((5 << 8) | r3) as u16;
5511 bytes.extend_from_slice(&hw1.to_le_bytes());
5512 bytes.extend_from_slice(&hw2.to_le_bytes());
5513
5514 // Step 4
5515 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5516 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5517 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5518 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5519
5520 // MUL R5, R5, R3
5521 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5522 let hw1: u16 = (0xFB00 | 5) as u16;
5523 let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5524 bytes.extend_from_slice(&hw1.to_le_bytes());
5525 bytes.extend_from_slice(&hw2.to_le_bytes());
5526
5527 // LSR.W R5, R5, #24
5528 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5529 let hw1: u16 = 0xEA4F;
5530 let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5531 bytes.extend_from_slice(&hw1.to_le_bytes());
5532 bytes.extend_from_slice(&hw2.to_le_bytes());
5533
5534 // #632: the count must be carried ACROSS the scratch restore
5535 // in a register the POP cannot touch. rd is allocator-assigned
5536 // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5537 // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5538 // result one instruction after computing it (0 for every input
5539 // under qemu). R12 is encoder scratch: never allocatable (#212)
5540 // and never in a restore set, so no choice of rd can collide.
5541 // ADD.W R12, R4, R5
5542 bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5543 bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5544
5545 // POP {R3, R4, R5}
5546 bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5547
5548 // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5549 // also total over rd = R8, where the old ADDS T1 3-bit field
5550 // silently corrupted the encoding (#178/#180 class).
5551 let mov: u16 =
5552 (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5553 bytes.extend_from_slice(&mov.to_le_bytes());
5554
5555 // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5556 // is GONE — it wrote the OPERAND's home high register (see
5557 // the I64Clz comment). Callers that relied on the implicit
5558 // clear emit their own explicit hi-zero op.
5559
5560 Ok(bytes)
5561 }
5562
5563 // I64Extend8S: Sign-extend low 8 bits to 64 bits
5564 // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5565 ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5566 let rdlo_bits = reg_to_bits(rdlo);
5567 let rdhi_bits = reg_to_bits(rdhi);
5568 let rnlo_bits = reg_to_bits(rnlo);
5569 let mut bytes = Vec::new();
5570
5571 // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5572 // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5573 let hw1: u16 = 0xFA4F_u16;
5574 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5575 bytes.extend_from_slice(&hw1.to_le_bytes());
5576 bytes.extend_from_slice(&hw2.to_le_bytes());
5577
5578 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5579 // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5580 // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5581 // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5582 let hw1: u16 = 0xEA4F;
5583 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5584 bytes.extend_from_slice(&hw1.to_le_bytes());
5585 bytes.extend_from_slice(&hw2.to_le_bytes());
5586
5587 Ok(bytes)
5588 }
5589
5590 // I64Extend16S: Sign-extend low 16 bits to 64 bits
5591 // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5592 ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5593 let rdlo_bits = reg_to_bits(rdlo);
5594 let rdhi_bits = reg_to_bits(rdhi);
5595 let rnlo_bits = reg_to_bits(rnlo);
5596 let mut bytes = Vec::new();
5597
5598 // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5599 // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5600 let hw1: u16 = 0xFA0F_u16;
5601 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5602 bytes.extend_from_slice(&hw1.to_le_bytes());
5603 bytes.extend_from_slice(&hw2.to_le_bytes());
5604
5605 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5606 let hw1: u16 = 0xEA4F;
5607 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5608 bytes.extend_from_slice(&hw1.to_le_bytes());
5609 bytes.extend_from_slice(&hw2.to_le_bytes());
5610
5611 Ok(bytes)
5612 }
5613
5614 // I64Extend32S: Sign-extend low 32 bits to 64 bits
5615 // Result: rdlo = rnlo, rdhi = rnlo >> 31
5616 ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5617 let rdlo_bits = reg_to_bits(rdlo);
5618 let rdhi_bits = reg_to_bits(rdhi);
5619 let rnlo_bits = reg_to_bits(rnlo);
5620 let mut bytes = Vec::new();
5621
5622 // MOV rdlo, rnlo (if different)
5623 if rdlo_bits != rnlo_bits {
5624 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5625 let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5626 let mov: u16 = 0x4600
5627 | (d_bit << 7)
5628 | ((rnlo_bits as u16) << 3)
5629 | ((rdlo_bits & 0x7) as u16);
5630 bytes.extend_from_slice(&mov.to_le_bytes());
5631 }
5632
5633 // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5634 let hw1: u16 = 0xEA4F;
5635 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5636 bytes.extend_from_slice(&hw1.to_le_bytes());
5637 bytes.extend_from_slice(&hw2.to_le_bytes());
5638
5639 Ok(bytes)
5640 }
5641
5642 // SelectMove: IT <cond>; MOV{cond} rd, rm
5643 // Conditional move: only execute MOV if condition is true
5644 ArmOp::SelectMove { rd, rm, cond } => {
5645 let rd_bits = reg_to_bits(rd) as u16;
5646 let rm_bits = reg_to_bits(rm) as u16;
5647
5648 // Condition code encoding for IT block
5649 use synth_synthesis::Condition;
5650 let cond_bits: u16 = match cond {
5651 Condition::EQ => 0x0, // Equal
5652 Condition::NE => 0x1, // Not equal
5653 Condition::HS => 0x2, // Higher or same (unsigned >=)
5654 Condition::LO => 0x3, // Lower (unsigned <)
5655 Condition::HI => 0x8, // Higher (unsigned >)
5656 Condition::LS => 0x9, // Lower or same (unsigned <=)
5657 Condition::GE => 0xA, // Greater or equal (signed)
5658 Condition::LT => 0xB, // Less than (signed)
5659 Condition::GT => 0xC, // Greater than (signed)
5660 Condition::LE => 0xD, // Less or equal (signed)
5661 };
5662
5663 // IT <cond>: single Then block (mask = 0x8 for T only)
5664 // IT instruction: 1011 1111 firstcond mask
5665 let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5666
5667 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5668 // This MOV will only execute if condition is true due to IT block
5669 let d_bit = (rd_bits >> 3) & 1;
5670 let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5671
5672 // Emit: IT <cond>, MOV rd, rm
5673 let mut bytes = it_instr.to_le_bytes().to_vec();
5674 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5675 Ok(bytes)
5676 }
5677
5678 // Popcnt: Population count (count set bits)
5679 // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5680 // x = x - ((x >> 1) & 0x55555555);
5681 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5682 // x = (x + (x >> 4)) & 0x0F0F0F0F;
5683 // x = x + (x >> 8);
5684 // x = x + (x >> 16);
5685 // return x & 0x3F;
5686 //
5687 // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5688 // previous expansion borrowed R11 as a second temp — but R11 is
5689 // the WASM linear-memory base, materialized at entry and read by
5690 // every later LDR/STR, and it is NOT in the pushed set, so the
5691 // clobber leaked to the CALLER too (a live memory-safety
5692 // miscompile: loads through `base = x >> 16`). The second temp is
5693 // eliminated the way the healthy i64.popcnt discipline implies —
5694 // never touch an unsaved register — but without its PUSH/POP
5695 // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5696 // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5697 // each AND takes its mask from the instruction itself and R12
5698 // alone carries every intermediate. Straight-line, no branches,
5699 // no stack traffic — nothing to skip on a trap edge.
5700 ArmOp::Popcnt { rd, rm } => {
5701 let rd_bits = reg_to_bits(rd);
5702 // Defensive (#1021): rd = R11/R12/SP/PC would silently
5703 // corrupt the linear-memory base, the expansion's own
5704 // scratch, or the stack. The selector never assigns them
5705 // (pool R0-R8); refuse loudly if that ever changes.
5706 if rd_bits >= 11 {
5707 return Err(synth_core::Error::synthesis(
5708 "Popcnt destination must be R0-R10: R11 is the linear-memory \
5709 base and R12 is the expansion's scratch (#1021)",
5710 ));
5711 }
5712 let mut bytes = Vec::new();
5713
5714 // First, move rm to rd if they're different
5715 if rd != rm {
5716 let rm_bits = reg_to_bits(rm) as u16;
5717 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5718 let d_bit = ((rd_bits as u16) >> 3) & 1;
5719 let mov_instr: u16 =
5720 0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5721 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5722 }
5723
5724 // Step 1: x = x - ((x >> 1) & 0x55555555)
5725 // R12 = rd >> 1
5726 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5727 // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5728 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5729 // rd = rd - R12
5730 bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5731
5732 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5733 // R12 = rd & 0x33333333
5734 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5735 12,
5736 rd_bits,
5737 0x3333_3333,
5738 )?);
5739 // rd = rd >> 2
5740 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5741 // rd = rd & 0x33333333
5742 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5743 rd_bits,
5744 rd_bits,
5745 0x3333_3333,
5746 )?);
5747 // rd = rd + R12
5748 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5749
5750 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5751 // R12 = rd >> 4
5752 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5753 // rd = rd + R12
5754 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5755 // rd = rd & 0x0F0F0F0F
5756 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5757 rd_bits,
5758 rd_bits,
5759 0x0F0F_0F0F,
5760 )?);
5761
5762 // Step 4: x = x + (x >> 8)
5763 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5764 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5765
5766 // Step 5: x = x + (x >> 16)
5767 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5768 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5769
5770 // Step 6: return x & 0x3F
5771 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5772
5773 Ok(bytes)
5774 }
5775
5776 // I64DivU: 64-bit unsigned division using binary long division
5777 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5778 // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5779 //
5780 // #610: the fixed-ABI wrapper marshals the selector-assigned
5781 // operand registers into the core's fixed regs and lands the
5782 // result in rd — pre-#610 this arm IGNORED its register fields,
5783 // so the selector read its rd pair (e.g. R4:R5) after the core's
5784 // own POP restored the stale caller values over it: 0 for every
5785 // input. A zero divisor now traps (UDF #0), per WASM semantics.
5786 ArmOp::I64DivU {
5787 rdlo,
5788 rdhi,
5789 rnlo,
5790 rnhi,
5791 rmlo,
5792 rmhi,
5793 elide_zero_guard,
5794 } => {
5795 let mut bytes = Vec::new();
5796 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5797 // #494 phase 2b: elided only under a certificate-discharged
5798 // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5799 if !elide_zero_guard {
5800 emit_i64_divisor_zero_trap(&mut bytes);
5801 }
5802
5803 // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5804 // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5805 // Encoding: 1011 0100 1111 0000 = 0xB4F0
5806 bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5807
5808 // Initialize quotient (R4:R5) = 0
5809 bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5810 bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5811
5812 // Initialize remainder (R6:R7) = 0
5813 bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5814 bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5815
5816 // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5817 // MOV.W R12, #64: F04F 0C40
5818 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5819 bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5820
5821 // Loop start
5822 let loop_start = bytes.len();
5823
5824 // === Loop body: process one bit ===
5825
5826 // 1. Shift quotient R4:R5 left by 1
5827 // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5828 // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5829 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5830 // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5831 // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5832 // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5833 // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5834 bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5835 bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5836 // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5837 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5838
5839 // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5840 // LSLS R7, R7, #1
5841 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5842 // ORR.W R7, R7, R6, LSR #31
5843 bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5844 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5845 // LSLS R6, R6, #1
5846 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5847 // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5848 bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5849 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5850
5851 // 3. Shift dividend R0:R1 left by 1
5852 // LSLS R1, R1, #1
5853 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5854 // ORR.W R1, R1, R0, LSR #31
5855 bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5856 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5857 // LSLS R0, R0, #1
5858 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5859
5860 // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5861 // Compare high words first: CMP R7, R3
5862 // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5863 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5864 // BHI means R7 > R3 (unsigned) - definitely subtract
5865 // BLO means R7 < R3 - definitely don't subtract
5866 // BEQ means need to check low words
5867
5868 // If high > divisor high: branch to subtract (forward +offset)
5869 // BHI.N +6 (skip CMP, skip BLO, do subtract)
5870 // BHI: 1101 1000 offset8 where cond=1000 (HI)
5871 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5872
5873 // If high < divisor high: branch past subtract
5874 // BLO.N +10 (skip to decrement)
5875 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5876
5877 // High words equal, compare low: CMP R6, R2
5878 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5879 // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5880 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5881
5882 // === Subtract block: remainder -= divisor, quotient |= 1 ===
5883 // SUBS R6, R6, R2
5884 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5885 // SBC R7, R7, R3 (with borrow)
5886 // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5887 bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5888 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5889 // ORR R4, R4, #1 (set bit 0 of quotient low)
5890 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5891 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5892
5893 // === Decrement counter and loop ===
5894 // SUBS.W R12, R12, #1 (decrement loop counter)
5895 // SUBS.W R12, R12, #1: F1BC 0C01
5896 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5897 bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5898
5899 // BNE back to loop_start
5900 let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5901 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5902 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5903 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5904
5905 // === Loop done, move quotient to R0:R1 ===
5906 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5907 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5908
5909 // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5910 // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5911 // Encoding: 1011 1100 1111 0000 = 0xBCF0
5912 bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5913
5914 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5915 Ok(bytes)
5916 }
5917
5918 // I64DivS: 64-bit signed division
5919 // Converts to unsigned, divides, then applies sign
5920 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5921 // -> R0:R1 = quotient (signed)
5922 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5923 ArmOp::I64DivS {
5924 rdlo,
5925 rdhi,
5926 rnlo,
5927 rnhi,
5928 rmlo,
5929 rmhi,
5930 elide_zero_guard,
5931 elide_overflow_guard,
5932 } => {
5933 let mut bytes = Vec::new();
5934 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5935 // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5936 // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5937 // the #633 overflow guard falls ONLY to
5938 // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5939 // divisor-nonzero fact alone must keep it.
5940 if !elide_zero_guard {
5941 emit_i64_divisor_zero_trap(&mut bytes);
5942 }
5943 if !elide_overflow_guard {
5944 // #633: INT64_MIN / -1 overflows — trap like the i32 path
5945 // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5946 emit_i64_divs_overflow_trap(&mut bytes);
5947 }
5948
5949 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5950 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5951 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5952
5953 // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5954 // EOR.W R9, R1, R3
5955 bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5956 bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5957
5958 // If dividend negative (R1 MSB set), negate it
5959 // TST R1, R1 (check sign)
5960 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5961 // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5962 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5963
5964 // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5965 // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5966 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5967 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5968 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5969 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5970 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5971
5972 // If divisor negative (R3 MSB set), negate it
5973 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5974 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5975
5976 // Negate R2:R3
5977 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5978 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5979 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5980 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5981 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5982
5983 // === Now do unsigned division (same as I64DivU) ===
5984 // Initialize quotient (R4:R5) = 0
5985 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5986 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5987 // Initialize remainder (R6:R7) = 0
5988 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5989 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5990 // Initialize loop counter R8 = 64
5991 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5992 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5993
5994 let loop_start = bytes.len();
5995
5996 // Shift quotient left
5997 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5998 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5999 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6000 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6001
6002 // Shift remainder left, OR in MSB of dividend
6003 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6004 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6005 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6006 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6007 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6008 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6009
6010 // Shift dividend left
6011 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6012 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6013 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6014 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6015
6016 // Compare and conditionally subtract
6017 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6018 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6019 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6020 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6021 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6022
6023 // Subtract and set quotient bit
6024 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6025 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6026 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6027 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6028 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6029
6030 // Decrement and loop
6031 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6032 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6033
6034 let branch_offset_bytes = bytes.len() - loop_start + 4;
6035 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6036 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6037 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6038
6039 // Move quotient to R0:R1
6040 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6041 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6042
6043 // If result should be negative (R9 MSB set), negate R0:R1
6044 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6045 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6046 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6047
6048 // Negate result R0:R1
6049 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6050 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6051 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6052 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6053 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6054
6055 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6056 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6057 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6058
6059 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6060 Ok(bytes)
6061 }
6062
6063 // I64RemU: 64-bit unsigned remainder using binary long division
6064 // Same algorithm as I64DivU but returns remainder instead of quotient
6065 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6066 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6067 ArmOp::I64RemU {
6068 rdlo,
6069 rdhi,
6070 rnlo,
6071 rnhi,
6072 rmlo,
6073 rmhi,
6074 elide_zero_guard,
6075 } => {
6076 let mut bytes = Vec::new();
6077 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6078 if !elide_zero_guard {
6079 emit_i64_divisor_zero_trap(&mut bytes);
6080 }
6081
6082 // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6083 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6084 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6085
6086 // Initialize quotient (R4:R5) = 0 (computed but not returned)
6087 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6088 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6089 // Initialize remainder (R6:R7) = 0
6090 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6091 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6092 // Initialize loop counter R8 = 64
6093 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6094 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6095
6096 let loop_start = bytes.len();
6097
6098 // Shift quotient left (not needed for result, but keeps algorithm same)
6099 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6100 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6101 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6102 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6103
6104 // Shift remainder left, OR in MSB of dividend
6105 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6106 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6107 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6108 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6109 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6110 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6111
6112 // Shift dividend left
6113 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6114 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6115 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6116 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6117
6118 // Compare and conditionally subtract
6119 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6120 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6121 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6122 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6123 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6124
6125 // Subtract and set quotient bit
6126 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6127 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6128 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6129 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6130 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6131
6132 // Decrement and loop
6133 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6134 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6135
6136 let branch_offset_bytes = bytes.len() - loop_start + 4;
6137 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6138 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6139 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6140
6141 // Move REMAINDER to R0:R1 (difference from I64DivU)
6142 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6143 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6144
6145 // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6146 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6147 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6148
6149 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6150 Ok(bytes)
6151 }
6152
6153 // I64RemS: 64-bit signed remainder
6154 // Remainder sign follows dividend sign (not quotient rule)
6155 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6156 // -> R0:R1 = remainder (signed, same sign as dividend)
6157 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6158 ArmOp::I64RemS {
6159 rdlo,
6160 rdhi,
6161 rnlo,
6162 rnhi,
6163 rmlo,
6164 rmhi,
6165 elide_zero_guard,
6166 } => {
6167 let mut bytes = Vec::new();
6168 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6169 if !elide_zero_guard {
6170 emit_i64_divisor_zero_trap(&mut bytes);
6171 }
6172
6173 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6174 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6175 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6176
6177 // Save dividend sign in R9 (remainder sign = dividend sign)
6178 // MOV R9, R1 (just need the sign bit)
6179 bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6180
6181 // If dividend negative (R1 MSB set), negate it
6182 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6183 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6184
6185 // Negate R0:R1
6186 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6187 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6188 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6189 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6190 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6191
6192 // If divisor negative (R3 MSB set), negate it
6193 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6194 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6195
6196 // Negate R2:R3
6197 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6198 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6199 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6200 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6201 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6202
6203 // === Unsigned division algorithm ===
6204 // Initialize quotient (R4:R5) = 0
6205 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6206 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6207 // Initialize remainder (R6:R7) = 0
6208 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6209 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6210 // Initialize loop counter R8 = 64
6211 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6212 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6213
6214 let loop_start = bytes.len();
6215
6216 // Shift quotient left
6217 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6218 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6219 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6220 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6221
6222 // Shift remainder left, OR in MSB of dividend
6223 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6224 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6225 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6226 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6227 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6228 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6229
6230 // Shift dividend left
6231 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6232 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6233 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6234 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6235
6236 // Compare and conditionally subtract
6237 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6238 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6239 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6240 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6241 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6242
6243 // Subtract and set quotient bit
6244 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6245 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6246 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6247 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6248 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6249
6250 // Decrement and loop
6251 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6252 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6253
6254 let branch_offset_bytes = bytes.len() - loop_start + 4;
6255 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6256 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6257 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6258
6259 // Move remainder to R0:R1
6260 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6261 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6262
6263 // If original dividend was negative (R9 MSB set), negate remainder
6264 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6265 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6266 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6267
6268 // Negate result R0:R1
6269 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6270 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6271 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6272 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6273 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6274
6275 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6276 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6277 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6278
6279 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6280 Ok(bytes)
6281 }
6282
6283 // === F32 VFP single-precision Thumb-2 encodings ===
6284 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6285 ArmOp::F32Add { sd, sn, sm } => {
6286 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6287 }
6288 ArmOp::F32Sub { sd, sn, sm } => {
6289 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6290 }
6291 ArmOp::F32Mul { sd, sn, sm } => {
6292 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6293 }
6294 ArmOp::F32Div { sd, sn, sm } => {
6295 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6296 }
6297 ArmOp::F32Abs { sd, sm } => {
6298 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6299 }
6300 ArmOp::F32Neg { sd, sm } => {
6301 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6302 }
6303 ArmOp::F32Sqrt { sd, sm } => {
6304 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6305 }
6306
6307 // f32 pseudo-ops — multi-instruction sequences
6308 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6309 ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6310 ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6311 ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6312 ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6313 ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6314 ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6315 ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6316
6317 // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6318 ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6319 ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6320 ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6321 ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6322 ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6323 ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6324
6325 ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6326
6327 ArmOp::F32Load { sd, addr } => {
6328 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6329 }
6330 ArmOp::F32Store { sd, addr } => {
6331 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6332 }
6333
6334 ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6335 ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6336 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6337 Err(synth_core::Error::synthesis(
6338 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6339 ))
6340 }
6341 ArmOp::F32ReinterpretI32 { sd, rm } => {
6342 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6343 }
6344 ArmOp::I32ReinterpretF32 { rd, sm } => {
6345 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6346 }
6347 ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6348 ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6349
6350 // === F64 VFP double-precision Thumb-2 encodings ===
6351 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6352 ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6353 0xEE300B00, dd, dn, dm,
6354 )?)),
6355 ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6356 0xEE300B40, dd, dn, dm,
6357 )?)),
6358 ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6359 0xEE200B00, dd, dn, dm,
6360 )?)),
6361 ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6362 0xEE800B00, dd, dn, dm,
6363 )?)),
6364 ArmOp::F64Abs { dd, dm } => {
6365 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6366 }
6367 ArmOp::F64Neg { dd, dm } => {
6368 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6369 }
6370 ArmOp::F64Sqrt { dd, dm } => {
6371 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6372 }
6373
6374 // f64 pseudo-ops
6375 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6376 ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6377 ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6378 ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6379 ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6380 ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6381 ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6382 ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6383
6384 // f64 comparisons
6385 ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6386 ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6387 ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6388 ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6389 ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6390 ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6391
6392 ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6393
6394 ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6395 0xED900B00, dd, addr,
6396 )?)),
6397 ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6398 0xED800B00, dd, addr,
6399 )?)),
6400
6401 ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6402 ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6403 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6404 Err(synth_core::Error::synthesis(
6405 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6406 ))
6407 }
6408 ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6409 ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6410 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6411 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6412 )),
6413 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6414 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6415 )),
6416 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6417 Err(synth_core::Error::synthesis(
6418 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6419 ))
6420 }
6421 ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6422 ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6423
6424 // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6425
6426 // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6427 ArmOp::I64Add {
6428 rdlo,
6429 rdhi,
6430 rnlo,
6431 rnhi,
6432 rmlo,
6433 rmhi,
6434 } => {
6435 let mut bytes = Vec::new();
6436 // ADDS rdlo, rnlo, rmlo (16-bit)
6437 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6438 rd: *rdlo,
6439 rn: *rnlo,
6440 op2: Operand2::Reg(*rmlo),
6441 })?);
6442 // ADC.W rdhi, rnhi, rmhi (32-bit)
6443 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6444 rd: *rdhi,
6445 rn: *rnhi,
6446 op2: Operand2::Reg(*rmhi),
6447 })?);
6448 Ok(bytes)
6449 }
6450
6451 // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6452 ArmOp::I64Sub {
6453 rdlo,
6454 rdhi,
6455 rnlo,
6456 rnhi,
6457 rmlo,
6458 rmhi,
6459 } => {
6460 let mut bytes = Vec::new();
6461 // SUBS rdlo, rnlo, rmlo (16-bit)
6462 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6463 rd: *rdlo,
6464 rn: *rnlo,
6465 op2: Operand2::Reg(*rmlo),
6466 })?);
6467 // SBC.W rdhi, rnhi, rmhi (32-bit)
6468 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6469 rd: *rdhi,
6470 rn: *rnhi,
6471 op2: Operand2::Reg(*rmhi),
6472 })?);
6473 Ok(bytes)
6474 }
6475
6476 // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6477 ArmOp::I64And {
6478 rdlo,
6479 rdhi,
6480 rnlo,
6481 rnhi,
6482 rmlo,
6483 rmhi,
6484 } => {
6485 let mut bytes = Vec::new();
6486 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6487 rd: *rdlo,
6488 rn: *rnlo,
6489 op2: Operand2::Reg(*rmlo),
6490 })?);
6491 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6492 rd: *rdhi,
6493 rn: *rnhi,
6494 op2: Operand2::Reg(*rmhi),
6495 })?);
6496 Ok(bytes)
6497 }
6498
6499 // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6500 ArmOp::I64Or {
6501 rdlo,
6502 rdhi,
6503 rnlo,
6504 rnhi,
6505 rmlo,
6506 rmhi,
6507 } => {
6508 let mut bytes = Vec::new();
6509 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6510 rd: *rdlo,
6511 rn: *rnlo,
6512 op2: Operand2::Reg(*rmlo),
6513 })?);
6514 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6515 rd: *rdhi,
6516 rn: *rnhi,
6517 op2: Operand2::Reg(*rmhi),
6518 })?);
6519 Ok(bytes)
6520 }
6521
6522 // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6523 ArmOp::I64Xor {
6524 rdlo,
6525 rdhi,
6526 rnlo,
6527 rnhi,
6528 rmlo,
6529 rmhi,
6530 } => {
6531 let mut bytes = Vec::new();
6532 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6533 rd: *rdlo,
6534 rn: *rnlo,
6535 op2: Operand2::Reg(*rmlo),
6536 })?);
6537 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6538 rd: *rdhi,
6539 rn: *rnhi,
6540 op2: Operand2::Reg(*rmhi),
6541 })?);
6542 Ok(bytes)
6543 }
6544
6545 // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6546 ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6547 rd: *rd,
6548 rn_lo: *rnlo,
6549 rn_hi: *rnhi,
6550 }),
6551
6552 // I64 comparisons: delegate to I64SetCond
6553 ArmOp::I64Eq {
6554 rd,
6555 rnlo,
6556 rnhi,
6557 rmlo,
6558 rmhi,
6559 } => self.encode_thumb(&ArmOp::I64SetCond {
6560 rd: *rd,
6561 rn_lo: *rnlo,
6562 rn_hi: *rnhi,
6563 rm_lo: *rmlo,
6564 rm_hi: *rmhi,
6565 cond: synth_synthesis::Condition::EQ,
6566 }),
6567
6568 ArmOp::I64Ne {
6569 rd,
6570 rnlo,
6571 rnhi,
6572 rmlo,
6573 rmhi,
6574 } => self.encode_thumb(&ArmOp::I64SetCond {
6575 rd: *rd,
6576 rn_lo: *rnlo,
6577 rn_hi: *rnhi,
6578 rm_lo: *rmlo,
6579 rm_hi: *rmhi,
6580 cond: synth_synthesis::Condition::NE,
6581 }),
6582
6583 ArmOp::I64LtS {
6584 rd,
6585 rnlo,
6586 rnhi,
6587 rmlo,
6588 rmhi,
6589 } => self.encode_thumb(&ArmOp::I64SetCond {
6590 rd: *rd,
6591 rn_lo: *rnlo,
6592 rn_hi: *rnhi,
6593 rm_lo: *rmlo,
6594 rm_hi: *rmhi,
6595 cond: synth_synthesis::Condition::LT,
6596 }),
6597
6598 ArmOp::I64LtU {
6599 rd,
6600 rnlo,
6601 rnhi,
6602 rmlo,
6603 rmhi,
6604 } => self.encode_thumb(&ArmOp::I64SetCond {
6605 rd: *rd,
6606 rn_lo: *rnlo,
6607 rn_hi: *rnhi,
6608 rm_lo: *rmlo,
6609 rm_hi: *rmhi,
6610 cond: synth_synthesis::Condition::LO,
6611 }),
6612
6613 ArmOp::I64LeS {
6614 rd,
6615 rnlo,
6616 rnhi,
6617 rmlo,
6618 rmhi,
6619 } => self.encode_thumb(&ArmOp::I64SetCond {
6620 rd: *rd,
6621 rn_lo: *rnlo,
6622 rn_hi: *rnhi,
6623 rm_lo: *rmlo,
6624 rm_hi: *rmhi,
6625 cond: synth_synthesis::Condition::LE,
6626 }),
6627
6628 ArmOp::I64LeU {
6629 rd,
6630 rnlo,
6631 rnhi,
6632 rmlo,
6633 rmhi,
6634 } => self.encode_thumb(&ArmOp::I64SetCond {
6635 rd: *rd,
6636 rn_lo: *rnlo,
6637 rn_hi: *rnhi,
6638 rm_lo: *rmlo,
6639 rm_hi: *rmhi,
6640 cond: synth_synthesis::Condition::LS,
6641 }),
6642
6643 ArmOp::I64GtS {
6644 rd,
6645 rnlo,
6646 rnhi,
6647 rmlo,
6648 rmhi,
6649 } => self.encode_thumb(&ArmOp::I64SetCond {
6650 rd: *rd,
6651 rn_lo: *rnlo,
6652 rn_hi: *rnhi,
6653 rm_lo: *rmlo,
6654 rm_hi: *rmhi,
6655 cond: synth_synthesis::Condition::GT,
6656 }),
6657
6658 ArmOp::I64GtU {
6659 rd,
6660 rnlo,
6661 rnhi,
6662 rmlo,
6663 rmhi,
6664 } => self.encode_thumb(&ArmOp::I64SetCond {
6665 rd: *rd,
6666 rn_lo: *rnlo,
6667 rn_hi: *rnhi,
6668 rm_lo: *rmlo,
6669 rm_hi: *rmhi,
6670 cond: synth_synthesis::Condition::HI,
6671 }),
6672
6673 ArmOp::I64GeS {
6674 rd,
6675 rnlo,
6676 rnhi,
6677 rmlo,
6678 rmhi,
6679 } => self.encode_thumb(&ArmOp::I64SetCond {
6680 rd: *rd,
6681 rn_lo: *rnlo,
6682 rn_hi: *rnhi,
6683 rm_lo: *rmlo,
6684 rm_hi: *rmhi,
6685 cond: synth_synthesis::Condition::GE,
6686 }),
6687
6688 ArmOp::I64GeU {
6689 rd,
6690 rnlo,
6691 rnhi,
6692 rmlo,
6693 rmhi,
6694 } => self.encode_thumb(&ArmOp::I64SetCond {
6695 rd: *rd,
6696 rn_lo: *rnlo,
6697 rn_hi: *rnhi,
6698 rm_lo: *rmlo,
6699 rm_hi: *rmhi,
6700 cond: synth_synthesis::Condition::HS,
6701 }),
6702
6703 // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6704 ArmOp::I64Const { rdlo, rdhi, value } => {
6705 let lo32 = *value as u32;
6706 let hi32 = (*value >> 32) as u32;
6707 let mut bytes = Vec::new();
6708 // Load low 32 bits into rdlo
6709 bytes.extend_from_slice(
6710 &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6711 );
6712 if lo32 > 0xFFFF {
6713 bytes.extend_from_slice(
6714 &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6715 );
6716 }
6717 // Load high 32 bits into rdhi
6718 bytes.extend_from_slice(
6719 &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6720 );
6721 if hi32 > 0xFFFF {
6722 bytes.extend_from_slice(
6723 &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6724 );
6725 }
6726 Ok(bytes)
6727 }
6728
6729 // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6730 ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6731 let mut bytes = Vec::new();
6732 // #372/#382: a memory `i64.load` carries an index register
6733 // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6734 // immediate `encode_thumb32_ldr` below uses only base+offset and
6735 // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6736 // i64. `i64_effective_base` materializes the effective base into
6737 // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6738 // the function is NOT skipped — #382), returning the residual
6739 // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6740 // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6741 // form unchanged — so existing output is byte-identical.
6742 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6743 bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6744 bytes.extend_from_slice(&self.encode_thumb32_ldr(
6745 rdhi,
6746 &base,
6747 offset.wrapping_add(4),
6748 )?);
6749 Ok(bytes)
6750 }
6751
6752 // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6753 ArmOp::I64Str { rdlo, rdhi, addr } => {
6754 let mut bytes = Vec::new();
6755 // #372/#382: same index-materialization + large-offset fold as
6756 // I64Ldr (see above).
6757 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6758 bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6759 bytes.extend_from_slice(&self.encode_thumb32_str(
6760 rdhi,
6761 &base,
6762 offset.wrapping_add(4),
6763 )?);
6764 Ok(bytes)
6765 }
6766
6767 // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6768 ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6769 let mut bytes = Vec::new();
6770 if rdlo != rn {
6771 // MOV rdlo, rn (16-bit)
6772 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6773 rd: *rdlo,
6774 op2: Operand2::Reg(*rn),
6775 })?);
6776 }
6777 // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6778 bytes.extend_from_slice(
6779 &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6780 );
6781 Ok(bytes)
6782 }
6783
6784 // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6785 ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6786 let mut bytes = Vec::new();
6787 if rdlo != rn {
6788 // MOV rdlo, rn
6789 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6790 rd: *rdlo,
6791 op2: Operand2::Reg(*rn),
6792 })?);
6793 }
6794 // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6795 // site with no branches in the expansion — before the fix this
6796 // emitted the literal two-instruction stream [4608, 2800], half
6797 // of which was `CMP r0,#0` rather than the high-word clear, so
6798 // every i64.extend_i32_u into a high pair leaked stale bits.
6799 emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6800 Ok(bytes)
6801 }
6802
6803 // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6804 ArmOp::I32WrapI64 { rd, rnlo } => {
6805 if rd == rnlo {
6806 // No-op: already in the right register
6807 let instr: u16 = 0xBF00; // NOP
6808 Ok(instr.to_le_bytes().to_vec())
6809 } else {
6810 // MOV rd, rnlo
6811 self.encode_thumb(&ArmOp::Mov {
6812 rd: *rd,
6813 op2: Operand2::Reg(*rnlo),
6814 })
6815 }
6816 }
6817
6818 // ===== Helium MVE operations (Thumb-2 encoding) =====
6819 ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6820 ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6821 ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6822 ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6823 0xEF000150, qd, qn, qm,
6824 ))),
6825 ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6826 0xEF200150, qd, qn, qm,
6827 ))),
6828 ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6829 0xFF000150, qd, qn, qm,
6830 ))),
6831 ArmOp::MveMvn { qd, qm } => {
6832 // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6833 let qd_enc = qreg_to_num(qd);
6834 let qm_enc = qreg_to_num(qm);
6835 let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6836 Ok(vfp_to_thumb_bytes(instr))
6837 }
6838 ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6839 0xEF100150, qd, qn, qm,
6840 ))),
6841 ArmOp::MveAddI { qd, qn, qm, size } => {
6842 let sz = mve_size_bits(size);
6843 let base: u32 = 0xEF000840 | (sz << 20);
6844 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6845 }
6846 ArmOp::MveSubI { qd, qn, qm, size } => {
6847 let sz = mve_size_bits(size);
6848 let base: u32 = 0xFF000840 | (sz << 20);
6849 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6850 }
6851 ArmOp::MveMulI { qd, qn, qm, size } => {
6852 let sz = mve_size_bits(size);
6853 let base: u32 = 0xEF000950 | (sz << 20);
6854 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6855 }
6856 ArmOp::MveNegI { qd, qm, size } => {
6857 let sz = mve_size_bits(size);
6858 // VNEG.Sx Qd, Qm
6859 let qd_enc = qreg_to_num(qd);
6860 let qm_enc = qreg_to_num(qm);
6861 let base: u32 = 0xFFB103C0 | (sz << 18);
6862 let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6863 Ok(vfp_to_thumb_bytes(instr))
6864 }
6865 ArmOp::MveDup { qd, rn, size } => {
6866 let sz = mve_size_bits(size);
6867 let qd_enc = qreg_to_num(qd);
6868 let rn_bits = reg_to_bits(rn);
6869 // VDUP.sz Qd, Rn: EEA0 0B10 variant
6870 // size encoding: 00=32, 01=16, 10=8
6871 let be = match sz {
6872 0 => 0b00u32, // 8-bit
6873 1 => 0b01, // 16-bit
6874 _ => 0b00, // 32-bit (default)
6875 };
6876 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6877 Ok(vfp_to_thumb_bytes(instr))
6878 }
6879 ArmOp::MveExtractLane { rd, qn, lane, size } => {
6880 let qn_enc = qreg_to_num(qn);
6881 let rd_bits = reg_to_bits(rd);
6882 // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6883 // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6884 let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6885 let lane_in_d = (*lane as u32) & 1;
6886 let _sz = mve_size_bits(size);
6887 // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6888 let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6889 Ok(vfp_to_thumb_bytes(instr))
6890 }
6891 ArmOp::MveInsertLane { qd, rn, lane, size } => {
6892 let qd_enc = qreg_to_num(qd);
6893 let rn_bits = reg_to_bits(rn);
6894 let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6895 let lane_in_d = (*lane as u32) & 1;
6896 let _sz = mve_size_bits(size);
6897 // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6898 let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6899 Ok(vfp_to_thumb_bytes(instr))
6900 }
6901
6902 // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6903 ArmOp::MveCmpEqI { qd, qn, qm, size }
6904 | ArmOp::MveCmpNeI { qd, qn, qm, size }
6905 | ArmOp::MveCmpLtS { qd, qn, qm, size }
6906 | ArmOp::MveCmpLtU { qd, qn, qm, size }
6907 | ArmOp::MveCmpGtS { qd, qn, qm, size }
6908 | ArmOp::MveCmpGtU { qd, qn, qm, size }
6909 | ArmOp::MveCmpLeS { qd, qn, qm, size }
6910 | ArmOp::MveCmpLeU { qd, qn, qm, size }
6911 | ArmOp::MveCmpGeS { qd, qn, qm, size }
6912 | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6913 // Encode as VADD (placeholder encoding — real implementation
6914 // would use VCMP + VPSEL pair)
6915 let sz = mve_size_bits(size);
6916 let base: u32 = 0xEF000840 | (sz << 20);
6917 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6918 }
6919
6920 // f32x4 MVE arithmetic
6921 ArmOp::MveAddF32 { qd, qn, qm } => {
6922 // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6923 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6924 }
6925 ArmOp::MveSubF32 { qd, qn, qm } => {
6926 // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6927 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6928 }
6929 ArmOp::MveMulF32 { qd, qn, qm } => {
6930 // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6931 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6932 }
6933 ArmOp::MveNegF32 { qd, qm } => {
6934 let qd_enc = qreg_to_num(qd);
6935 let qm_enc = qreg_to_num(qm);
6936 // VNEG.F32 Qd, Qm: FFB907C0
6937 let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6938 Ok(vfp_to_thumb_bytes(instr))
6939 }
6940 ArmOp::MveAbsF32 { qd, qm } => {
6941 let qd_enc = qreg_to_num(qd);
6942 let qm_enc = qreg_to_num(qm);
6943 // VABS.F32 Qd, Qm: FFB90740
6944 let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6945 Ok(vfp_to_thumb_bytes(instr))
6946 }
6947 ArmOp::MveCmpEqF32 { qd, qn, qm }
6948 | ArmOp::MveCmpNeF32 { qd, qn, qm }
6949 | ArmOp::MveCmpLtF32 { qd, qn, qm }
6950 | ArmOp::MveCmpLeF32 { qd, qn, qm }
6951 | ArmOp::MveCmpGtF32 { qd, qn, qm }
6952 | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6953 // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6954 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6955 }
6956 ArmOp::MveDupF32 { qd, rn } => {
6957 let qd_enc = qreg_to_num(qd);
6958 let rn_bits = reg_to_bits(rn);
6959 // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6960 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6961 Ok(vfp_to_thumb_bytes(instr))
6962 }
6963 ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6964 let qn_enc = qreg_to_num(qn);
6965 let rd_bits = reg_to_bits(rd);
6966 // VMOV Rd, Sn where Sn = Q*4 + lane
6967 let s_num = qn_enc * 4 + (*lane as u32);
6968 let (vn, n) = encode_sreg(s_num);
6969 let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6970 Ok(vfp_to_thumb_bytes(instr))
6971 }
6972 ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6973 let qd_enc = qreg_to_num(qd);
6974 let rn_bits = reg_to_bits(rn);
6975 // VMOV Sn, Rn where Sn = Q*4 + lane
6976 let s_num = qd_enc * 4 + (*lane as u32);
6977 let (vn, n) = encode_sreg(s_num);
6978 let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6979 Ok(vfp_to_thumb_bytes(instr))
6980 }
6981 ArmOp::MveDivF32 { qd, qn, qm } => {
6982 // Lane-wise: extract 4 S-regs, VDIV, insert back
6983 self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6984 }
6985 ArmOp::MveSqrtF32 { qd, qm } => {
6986 // Lane-wise: extract 4 S-regs, VSQRT, insert back
6987 self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6988 }
6989
6990 // Catch-all for any remaining ops
6991 _ => {
6992 let instr: u16 = 0xBF00; // NOP
6993 Ok(instr.to_le_bytes().to_vec())
6994 }
6995 }
6996 }
6997
6998 // === Thumb-2 VFP multi-instruction helpers ===
6999
7000 /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
7001 fn encode_thumb_f32_compare(
7002 &self,
7003 rd: &Reg,
7004 sn: &VfpReg,
7005 sm: &VfpReg,
7006 cond_code: u32,
7007 ) -> Result<Vec<u8>> {
7008 let mut bytes = Vec::new();
7009 let rd_bits = reg_to_bits(rd);
7010
7011 // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
7012 // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
7013 // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
7014 // transferred from the VFP compare, so the following `IT<cond>` read
7015 // stale flags and every f32 comparison silently returned 0 (verified:
7016 // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
7017 // because it deliberately skipped compare EXECUTION on a false premise
7018 // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
7019 // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
7020 // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
7021 // untouched — only the byte ORDER differs.
7022
7023 // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
7024 // is immediately overwritten by the VMRS below.
7025 if rd_bits < 8 {
7026 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7027 bytes.extend_from_slice(&movs_zero.to_le_bytes());
7028 } else {
7029 // MOV.W Rd, #0 (32-bit Thumb-2)
7030 let hw1: u16 = 0xF04F;
7031 let hw2: u16 = (rd_bits as u16) << 8;
7032 bytes.extend_from_slice(&hw1.to_le_bytes());
7033 bytes.extend_from_slice(&hw2.to_le_bytes());
7034 }
7035
7036 // VCMP.F32 Sn, Sm
7037 let sn_num = vfp_sreg_to_num(sn)?;
7038 let sm_num = vfp_sreg_to_num(sm)?;
7039 let (vd, d) = encode_sreg(sn_num);
7040 let (vm, m) = encode_sreg(sm_num);
7041 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7042 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7043
7044 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7045 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7046
7047 // IT<cond> — If-Then for conditional MOV
7048 // IT encoding: 1011 1111 cond(4) mask(4)
7049 // mask = 0x8 for single "then" (IT)
7050 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7051 bytes.extend_from_slice(&it.to_le_bytes());
7052
7053 // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7054 if rd_bits < 8 {
7055 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7056 bytes.extend_from_slice(&mov_one.to_le_bytes());
7057 } else {
7058 // MOV.W Rd, #1 (32-bit)
7059 let hw1: u16 = 0xF04F;
7060 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7061 bytes.extend_from_slice(&hw1.to_le_bytes());
7062 bytes.extend_from_slice(&hw2.to_le_bytes());
7063 }
7064
7065 Ok(bytes)
7066 }
7067
7068 /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7069 fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7070 let mut bytes = Vec::new();
7071 let bits = value.to_bits();
7072 let rt: u32 = 12; // R12/IP as temp
7073
7074 // MOVW R12, #lo16
7075 // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7076 let lo16 = bits & 0xFFFF;
7077 let imm4 = (lo16 >> 12) & 0xF;
7078 let i_bit = (lo16 >> 11) & 1;
7079 let imm3 = (lo16 >> 8) & 0x7;
7080 let imm8 = lo16 & 0xFF;
7081 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7082 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7083 bytes.extend_from_slice(&hw1.to_le_bytes());
7084 bytes.extend_from_slice(&hw2.to_le_bytes());
7085
7086 // MOVT R12, #hi16
7087 let hi16 = (bits >> 16) & 0xFFFF;
7088 let imm4 = (hi16 >> 12) & 0xF;
7089 let i_bit = (hi16 >> 11) & 1;
7090 let imm3 = (hi16 >> 8) & 0x7;
7091 let imm8 = hi16 & 0xFF;
7092 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7093 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7094 bytes.extend_from_slice(&hw1.to_le_bytes());
7095 bytes.extend_from_slice(&hw2.to_le_bytes());
7096
7097 // VMOV Sd, R12
7098 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7099 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7100
7101 Ok(bytes)
7102 }
7103
7104 /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7105 fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7106 let mut bytes = Vec::new();
7107
7108 // VMOV Sd, Rm
7109 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7110 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7111
7112 // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7113 // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7114 // (GI-FPU-002: previously swapped — see the ARM32 twin).
7115 let sd_num = vfp_sreg_to_num(sd)?;
7116 let (vd, d) = encode_sreg(sd_num);
7117 let (vm, m) = encode_sreg(sd_num);
7118 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7119 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7120 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7121
7122 Ok(bytes)
7123 }
7124
7125 /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7126 /// Encode F32 rounding as Thumb-2.
7127 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7128 ///
7129 /// For trunc: uses VCVTR.S32.F32 (always truncates).
7130 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7131 /// then restores FPSCR.
7132 fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7133 let mut bytes = Vec::new();
7134 let sm_num = vfp_sreg_to_num(sm)?;
7135 let sd_num = vfp_sreg_to_num(sd)?;
7136 let (vd_s, d_s) = encode_sreg(sd_num);
7137 let (vm_s, m_s) = encode_sreg(sm_num);
7138
7139 if mode == 0b11 {
7140 // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7141 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7142 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7143 } else {
7144 // ceil/floor/nearest: manipulate FPSCR rounding mode
7145 let rt: u32 = 12; // R12/IP as temp
7146
7147 // VMRS R12, FPSCR
7148 let vmrs = 0xEEF10A10 | (rt << 12);
7149 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7150
7151 // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7152 // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7153 // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7154 // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7155 let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7156 let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7157 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7158 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7159
7160 // ORR.W R12, R12, #(mode << 22)
7161 if mode != 0 {
7162 let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7163 let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7164 bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7165 bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7166 }
7167
7168 // VMSR FPSCR, R12
7169 let vmsr = 0xEEE10A10 | (rt << 12);
7170 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7171
7172 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7173 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7174 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7175
7176 // Restore FPSCR: clear rmode bits back to nearest (default)
7177 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7178 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7179 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7180 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7181 }
7182
7183 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7184 let (vd2, d2) = encode_sreg(sd_num);
7185 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7186 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7187
7188 Ok(bytes)
7189 }
7190
7191 /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7192 fn encode_thumb_f32_minmax(
7193 &self,
7194 sd: &VfpReg,
7195 sn: &VfpReg,
7196 sm: &VfpReg,
7197 is_min: bool,
7198 ) -> Result<Vec<u8>> {
7199 let mut bytes = Vec::new();
7200 let sn_num = vfp_sreg_to_num(sn)?;
7201 let sm_num = vfp_sreg_to_num(sm)?;
7202 let sd_num = vfp_sreg_to_num(sd)?;
7203
7204 // VMOV.F32 Sd, Sn
7205 let (vd, d) = encode_sreg(sd_num);
7206 let (vn, n) = encode_sreg(sn_num);
7207 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7208 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7209
7210 // VCMP.F32 Sn, Sm
7211 let (vm, m) = encode_sreg(sm_num);
7212 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7213 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7214
7215 // VMRS APSR_nzcv, FPSCR
7216 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7217
7218 // IT GT (for min) or IT MI (for max)
7219 let cond: u16 = if is_min { 0xC } else { 0x4 };
7220 let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7221 bytes.extend_from_slice(&it.to_le_bytes());
7222
7223 // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7224 let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7225 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7226
7227 Ok(bytes)
7228 }
7229
7230 /// Encode F32 copysign as Thumb-2
7231 /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7232 /// encoder scratch, #212), the flags, and Sd:
7233 ///
7234 /// VMOV R12, Sm ; CMP R12, #0 (N flag = the sign bit)
7235 /// VABS.F32 Sd, Sn (magnitude, sign cleared)
7236 /// IT MI ; VNEG.F32(MI) Sd, Sd
7237 ///
7238 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7239 /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7240 /// is safe. (The previous sequence staged the magnitude through R0 —
7241 /// clobbering a live allocator-owned value, the #615 class; caught while
7242 /// composing the F64 twin for #369.)
7243 fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7244 let mut bytes = Vec::new();
7245
7246 // VMOV R12, Sm (sign source bits)
7247 bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7248 false,
7249 sm,
7250 &Reg::R12,
7251 )?));
7252 // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7253 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7254 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7255 // VABS.F32 Sd, Sn
7256 let sd_num = vfp_sreg_to_num(sd)?;
7257 let sn_num = vfp_sreg_to_num(sn)?;
7258 let (vd, d) = encode_sreg(sd_num);
7259 let (vn, n) = encode_sreg(sn_num);
7260 let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7261 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7262 // IT MI ; VNEG.F32(MI) Sd, Sd
7263 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7264 let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7265 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7266
7267 Ok(bytes)
7268 }
7269
7270 /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7271 fn encode_thumb_f64_compare(
7272 &self,
7273 rd: &Reg,
7274 dn: &VfpReg,
7275 dm: &VfpReg,
7276 cond_code: u32,
7277 ) -> Result<Vec<u8>> {
7278 let mut bytes = Vec::new();
7279 let rd_bits = reg_to_bits(rd);
7280
7281 // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7282 // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7283 // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7284 // transferred, so the following `IT<cond>` read stale flags and every
7285 // f64 comparison silently returned 0 — the exact bug the f32 compare
7286 // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7287 // FIRST (its flag side effect is overwritten by the VMRS), then
7288 // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7289 // unchanged.
7290
7291 // MOVS Rd, #0
7292 if rd_bits < 8 {
7293 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7294 bytes.extend_from_slice(&movs_zero.to_le_bytes());
7295 } else {
7296 let hw1: u16 = 0xF04F;
7297 let hw2: u16 = (rd_bits as u16) << 8;
7298 bytes.extend_from_slice(&hw1.to_le_bytes());
7299 bytes.extend_from_slice(&hw2.to_le_bytes());
7300 }
7301
7302 // VCMP.F64 Dn, Dm
7303 let dn_num = vfp_dreg_to_num(dn)?;
7304 let dm_num = vfp_dreg_to_num(dm)?;
7305 let (vd, d) = encode_dreg(dn_num);
7306 let (vm, m) = encode_dreg(dm_num);
7307 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7308 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7309
7310 // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7311 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7312
7313 // IT<cond>
7314 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7315 bytes.extend_from_slice(&it.to_le_bytes());
7316
7317 // MOV Rd, #1
7318 if rd_bits < 8 {
7319 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7320 bytes.extend_from_slice(&mov_one.to_le_bytes());
7321 } else {
7322 let hw1: u16 = 0xF04F;
7323 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7324 bytes.extend_from_slice(&hw1.to_le_bytes());
7325 bytes.extend_from_slice(&hw2.to_le_bytes());
7326 }
7327
7328 Ok(bytes)
7329 }
7330
7331 /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7332 fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7333 let mut bytes = Vec::new();
7334 let bits = value.to_bits();
7335 let lo32 = bits as u32;
7336 let hi32 = (bits >> 32) as u32;
7337
7338 // MOVW R0, #lo16(lo32)
7339 let lo16 = lo32 & 0xFFFF;
7340 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7341
7342 // MOVT R0, #hi16(lo32)
7343 let hi16 = (lo32 >> 16) & 0xFFFF;
7344 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7345
7346 // MOVW R12, #lo16(hi32)
7347 let lo16 = hi32 & 0xFFFF;
7348 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7349
7350 // MOVT R12, #hi16(hi32)
7351 let hi16 = (hi32 >> 16) & 0xFFFF;
7352 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7353
7354 // VMOV Dd, R0, R12
7355 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7356 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7357
7358 Ok(bytes)
7359 }
7360
7361 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7362 /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7363 /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7364 /// definition — never S0 (which may hold a live value; the previous
7365 /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7366 /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7367 /// twin had; latent here because f64.convert_i32_* was decode-dropped
7368 /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7369 /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7370 fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7371 let dd_num = vfp_dreg_to_num(dd)?;
7372 if dd_num > 7 {
7373 return Err(synth_core::Error::synthesis(format!(
7374 "F64ConvertI32: destination {dd:?} has no S-register alias \
7375 (D8..D15) — the selector allocates only D0..D7"
7376 )));
7377 }
7378 let mut bytes = Vec::new();
7379
7380 // VMOV S(2d), Rm — stage the integer in the destination's low word.
7381 let (vn_s, n_s) = encode_sreg(2 * dd_num);
7382 let rt = reg_to_bits(rm);
7383 let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7384 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7385
7386 // VCVT.F64.S32/U32 Dd, S(2d)
7387 let (vd, d) = encode_dreg(dd_num);
7388 let (vm, m) = encode_sreg(2 * dd_num);
7389 let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7390 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7391 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7392
7393 Ok(bytes)
7394 }
7395
7396 /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7397 fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7398 let dd_num = vfp_dreg_to_num(dd)?;
7399 let sm_num = vfp_sreg_to_num(sm)?;
7400 let (vd, d) = encode_dreg(dd_num);
7401 let (vm, m) = encode_sreg(sm_num);
7402
7403 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7404 Ok(vfp_to_thumb_bytes(vcvt))
7405 }
7406
7407 /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7408 /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7409 /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7410 fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7411 let sd_num = vfp_sreg_to_num(sd)?;
7412 let dm_num = vfp_dreg_to_num(dm)?;
7413 let (vd, d) = encode_sreg(sd_num);
7414 let (vm, m) = encode_dreg(dm_num);
7415
7416 let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7417 Ok(vfp_to_thumb_bytes(vcvt))
7418 }
7419
7420 /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7421 ///
7422 /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7423 /// clobbering half of an operand the selector has already popped. The
7424 /// overlapping write is well-defined: VCVT reads its source operand before
7425 /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7426 ///
7427 /// # The one precondition, and who actually provides it
7428 ///
7429 /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7430 /// whole safety argument, and it is worth naming the guarantor precisely
7431 /// (#946): **`select_with_stack`** provides it, by copying a home into a
7432 /// fresh D-temp first. Visible in the shipped output for
7433 /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7434 ///
7435 /// ```text
7436 /// vmov r1, r2, d0 ; read the param out of its AAPCS-VFP home D0
7437 /// vmov d1, r1, r2 ; ...into a fresh D-temp
7438 /// vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7439 /// ```
7440 ///
7441 /// `InstructionSelector::select` / `select_default` do NOT provide it —
7442 /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7443 /// or home test. That path is not reachable from `synth compile`
7444 /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7445 /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7446 /// not a live miscompile — but a caller reaching that `pub` API directly
7447 /// gets no such guarantee.
7448 ///
7449 /// # What this deliberately does NOT claim
7450 ///
7451 /// An earlier version of this comment said the staging register is "never
7452 /// S0, which may hold an unrelated live value (the #615 class)". **That is
7453 /// false**, and measurably so: for
7454 /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7455 /// compiler emits `vcvt.s32.f64 s0, d0`.
7456 ///
7457 /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7458 /// here it is always the low half of `dm` itself, which the precondition
7459 /// above already makes dead. Naming a guard the code does not have — and
7460 /// does not need — invites a future reader to lean on it. The dead-temp
7461 /// precondition is the only thing holding this up.
7462 fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7463 let dm_num = vfp_dreg_to_num(dm)?;
7464 if dm_num > 7 {
7465 return Err(synth_core::Error::synthesis(format!(
7466 "I32TruncF64: source {dm:?} has no S-register alias \
7467 (D8..D15) — the selector allocates only D0..D7"
7468 )));
7469 }
7470 let mut bytes = Vec::new();
7471
7472 // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7473 // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7474 let (vm, m) = encode_dreg(dm_num);
7475 let (vd_s, d_s) = encode_sreg(2 * dm_num);
7476 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7477 let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7478 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7479
7480 // VMOV Rd, S(2m)
7481 let rt = reg_to_bits(rd);
7482 let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7483 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7484
7485 Ok(bytes)
7486 }
7487
7488 /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7489 /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7490 /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7491 /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7492 /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7493 /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7494 /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7495 /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7496 /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7497 /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7498 /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7499 /// IT-conditional; none of these sequences emits them inside an IT block).
7500 fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7501 let dd_num = vfp_dreg_to_num(dd)?;
7502 let dm_num = vfp_dreg_to_num(dm)?;
7503 let (vd, d) = encode_dreg(dd_num);
7504 let (vm, m) = encode_dreg(dm_num);
7505 // clang-verified bases (thumbv7em, fpv5-d16):
7506 // vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7507 // vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7508 let base: u32 = match mode {
7509 0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7510 0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7511 0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7512 _ => 0xEEB60BC0, // VRINTZ.F64 (round toward zero)
7513 };
7514 Ok(vfp_to_thumb_bytes(
7515 base | (d << 22) | (vd << 12) | (m << 5) | vm,
7516 ))
7517 }
7518
7519 /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7520 ///
7521 /// VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7522 /// VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm (FPv5; -0.0 < +0.0 ordered)
7523 /// IT VS ; VADD.F64(VS) Dd, Dn, Dm (unordered ⇒ NaN-propagating)
7524 ///
7525 /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7526 /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7527 /// VADD overwrites the result with a quiet NaN whenever the compare was
7528 /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7529 /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7530 /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7531 /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7532 ///
7533 /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7534 /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7535 /// destination while both sources are still marked live).
7536 fn encode_thumb_f64_minmax(
7537 &self,
7538 dd: &VfpReg,
7539 dn: &VfpReg,
7540 dm: &VfpReg,
7541 is_min: bool,
7542 ) -> Result<Vec<u8>> {
7543 if dd == dn || dd == dm {
7544 return Err(synth_core::Error::synthesis(format!(
7545 "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7546 the unordered NaN fix-up would read a clobbered operand \
7547 (compiler bug: the selector must allocate a fresh D-temp)",
7548 if is_min { "Min" } else { "Max" },
7549 )));
7550 }
7551 let mut bytes = Vec::new();
7552 let dd_num = vfp_dreg_to_num(dd)?;
7553 let dn_num = vfp_dreg_to_num(dn)?;
7554 let dm_num = vfp_dreg_to_num(dm)?;
7555 let (vd, d) = encode_dreg(dd_num);
7556 let (vn, n) = encode_dreg(dn_num);
7557 let (vm, m) = encode_dreg(dm_num);
7558
7559 // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7560 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7561 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7562 // VMRS APSR_nzcv, FPSCR
7563 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7564 // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7565 // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7566 let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7567 let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7568 bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7569 // IT VS (unordered ⇒ at least one NaN operand)
7570 bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7571 // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7572 let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7573 bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7574
7575 Ok(bytes)
7576 }
7577
7578 /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7579 /// encoder scratch, #212), the flags, and Dd:
7580 ///
7581 /// VMOV R12, S(2m+1) (high word of the SIGN source Dm)
7582 /// CMP R12, #0 (N flag = the sign bit)
7583 /// VABS.F64 Dd, Dn (magnitude, sign cleared)
7584 /// IT MI ; VNEG.F64(MI) Dd, Dd
7585 ///
7586 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7587 /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7588 /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7589 /// allocator's back — the #615 class.)
7590 fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7591 let dm_num = vfp_dreg_to_num(dm)?;
7592 if dm_num > 7 {
7593 return Err(synth_core::Error::synthesis(format!(
7594 "F64Copysign: sign source {dm:?} has no S-register alias \
7595 (D8..D15) — the selector allocates only D0..D7"
7596 )));
7597 }
7598 let mut bytes = Vec::new();
7599 // VMOV R12, S(2m+1) — the sign source's high word.
7600 let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7601 let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7602 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7603 // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7604 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7605 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7606 // VABS.F64 Dd, Dn
7607 let dd_num = vfp_dreg_to_num(dd)?;
7608 let dn_num = vfp_dreg_to_num(dn)?;
7609 let (vd, d) = encode_dreg(dd_num);
7610 let (vn, n) = encode_dreg(dn_num);
7611 let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7612 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7613 // IT MI ; VNEG.F64(MI) Dd, Dd
7614 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7615 let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7616 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7617
7618 Ok(bytes)
7619 }
7620
7621 /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7622 fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7623 let mut bytes = Vec::new();
7624
7625 let sm_num = vfp_sreg_to_num(sm)?;
7626 let (vd, d) = encode_sreg(sm_num);
7627 let (vm, m) = encode_sreg(sm_num);
7628 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7629 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7630 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7631
7632 // VMOV Rd, Sm
7633 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7634 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7635
7636 Ok(bytes)
7637 }
7638
7639 // === Thumb-2 32-bit encoding helpers ===
7640
7641 /// Encode Thumb-2 32-bit ADD with immediate
7642 fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7643 let rd_bits = reg_to_bits(rd);
7644 let rn_bits = reg_to_bits(rn);
7645
7646 // The `i:imm3:imm8` field is split the same way for both forms.
7647 let i_bit = (imm >> 11) & 1;
7648 let imm3 = (imm >> 8) & 0x7;
7649 let imm8 = imm & 0xFF;
7650
7651 let hw1_base = if imm <= 0xFF {
7652 // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7653 // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7654 // correct — keep this form so existing encodings stay bit-identical.
7655 0xF100
7656 } else if imm <= 0xFFF {
7657 // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7658 // This is what makes `add sp, sp, #frame` correct for frame sizes
7659 // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7660 0xF200
7661 } else {
7662 return Err(synth_core::Error::synthesis(
7663 "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7664 ));
7665 };
7666
7667 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7668 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7669
7670 let mut bytes = hw1.to_le_bytes().to_vec();
7671 bytes.extend_from_slice(&hw2.to_le_bytes());
7672 Ok(bytes)
7673 }
7674
7675 /// Encode Thumb-2 32-bit SUB with immediate
7676 fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7677 let rd_bits = reg_to_bits(rd);
7678 let rn_bits = reg_to_bits(rn);
7679
7680 let i_bit = (imm >> 11) & 1;
7681 let imm3 = (imm >> 8) & 0x7;
7682 let imm8 = imm & 0xFF;
7683
7684 let hw1_base = if imm <= 0xFF {
7685 // SUB.W (T3) modified immediate — correct for the zero-extended byte
7686 // (imm <= 0xFF). Kept bit-identical for existing encodings.
7687 0xF1A0
7688 } else if imm <= 0xFFF {
7689 // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7690 // `sub sp, sp, #frame` correct for frame sizes >= 256.
7691 0xF2A0
7692 } else {
7693 return Err(synth_core::Error::synthesis(
7694 "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7695 ));
7696 };
7697
7698 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7699 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7700
7701 let mut bytes = hw1.to_le_bytes().to_vec();
7702 bytes.extend_from_slice(&hw2.to_le_bytes());
7703 Ok(bytes)
7704 }
7705
7706 /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7707 fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7708 let rd_bits = reg_to_bits(rd);
7709 let rn_bits = reg_to_bits(rn);
7710
7711 // ADDS.W (flag-setting) has only the modified-immediate form — error on
7712 // an un-encodable value rather than silently add the wrong constant.
7713 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7714 synth_core::Error::synthesis(
7715 "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7716 )
7717 })?;
7718 let i_bit = (field >> 11) & 1;
7719 let imm3 = (field >> 8) & 0x7;
7720 let imm8 = field & 0xFF;
7721
7722 // ADDS.W Rd, Rn, #imm (with S=1)
7723 // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7724 let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7725 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7726
7727 let mut bytes = hw1.to_le_bytes().to_vec();
7728 bytes.extend_from_slice(&hw2.to_le_bytes());
7729 Ok(bytes)
7730 }
7731
7732 /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7733 fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7734 let rd_bits = reg_to_bits(rd);
7735 let rn_bits = reg_to_bits(rn);
7736
7737 // SUBS.W (flag-setting) has only the modified-immediate form — error on
7738 // an un-encodable value rather than silently subtract the wrong constant.
7739 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7740 synth_core::Error::synthesis(
7741 "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7742 )
7743 })?;
7744 let i_bit = (field >> 11) & 1;
7745 let imm3 = (field >> 8) & 0x7;
7746 let imm8 = field & 0xFF;
7747
7748 // SUBS.W Rd, Rn, #imm (with S=1)
7749 // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7750 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7751 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7752
7753 let mut bytes = hw1.to_le_bytes().to_vec();
7754 bytes.extend_from_slice(&hw2.to_le_bytes());
7755 Ok(bytes)
7756 }
7757
7758 /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7759 ///
7760 /// # Contract (Verus-style)
7761 /// ```text
7762 /// requires rd <= R14
7763 /// ensures result.len() == 4
7764 /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7765 /// ```
7766 fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7767 let rd_bits = reg_to_bits(rd);
7768 reg_bits_checked(rd_bits)?;
7769 let imm16 = imm & 0xFFFF;
7770
7771 // MOVW Rd, #imm16
7772 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7773 let imm4 = (imm16 >> 12) & 0xF;
7774 let i_bit = (imm16 >> 11) & 1;
7775 let imm3 = (imm16 >> 8) & 0x7;
7776 let imm8 = imm16 & 0xFF;
7777
7778 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7779 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7780
7781 let mut bytes = hw1.to_le_bytes().to_vec();
7782 bytes.extend_from_slice(&hw2.to_le_bytes());
7783 encoding_contracts::verify_thumb32(&bytes);
7784 Ok(bytes)
7785 }
7786
7787 /// Encode Thumb-2 32-bit shift with immediate
7788 ///
7789 /// # Contract (Verus-style)
7790 /// ```text
7791 /// requires rd <= R14, rm <= R14
7792 /// ensures result.len() == 4
7793 /// ```
7794 fn encode_thumb32_shift(
7795 &self,
7796 rd: &Reg,
7797 rm: &Reg,
7798 shift: u32,
7799 shift_type: u8,
7800 ) -> Result<Vec<u8>> {
7801 let rd_bits = reg_to_bits(rd);
7802 let rm_bits = reg_to_bits(rm);
7803 reg_bits_checked(rd_bits)?;
7804 reg_bits_checked(rm_bits)?;
7805 let imm5 = shift & 0x1F;
7806 let imm2 = imm5 & 0x3;
7807 let imm3 = (imm5 >> 2) & 0x7;
7808
7809 // MOV.W Rd, Rm, <shift> #imm
7810 // EA4F 0 imm3 Rd imm2 type Rm
7811 let hw1: u16 = 0xEA4F;
7812 let hw2: u16 =
7813 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7814 as u16;
7815
7816 let mut bytes = hw1.to_le_bytes().to_vec();
7817 bytes.extend_from_slice(&hw2.to_le_bytes());
7818 Ok(bytes)
7819 }
7820
7821 /// Encode Thumb-2 32-bit shift by register
7822 /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7823 /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7824 fn encode_thumb32_shift_reg(
7825 &self,
7826 rd: &Reg,
7827 rn: &Reg,
7828 rm: &Reg,
7829 shift_type: u8,
7830 ) -> Result<Vec<u8>> {
7831 let rd_bits = reg_to_bits(rd);
7832 let rn_bits = reg_to_bits(rn);
7833 let rm_bits = reg_to_bits(rm);
7834
7835 // hw1: 1111 1010 0xx0 Rn
7836 let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7837 // hw2: 1111 Rd 0000 Rm
7838 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7839
7840 let mut bytes = hw1.to_le_bytes().to_vec();
7841 bytes.extend_from_slice(&hw2.to_le_bytes());
7842 Ok(bytes)
7843 }
7844
7845 /// Encode Thumb-2 32-bit CMP with immediate
7846 fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7847 let rn_bits = reg_to_bits(rn);
7848
7849 // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7850 // so an un-encodable immediate MUST be materialized into a register by
7851 // the selector. Error rather than silently compare the wrong constant.
7852 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7853 synth_core::Error::synthesis(
7854 "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7855 )
7856 })?;
7857 let i_bit = (field >> 11) & 1;
7858 let imm3 = (field >> 8) & 0x7;
7859 let imm8 = field & 0xFF;
7860
7861 // CMP.W Rn, #imm
7862 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7863 let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7864
7865 let mut bytes = hw1.to_le_bytes().to_vec();
7866 bytes.extend_from_slice(&hw2.to_le_bytes());
7867 Ok(bytes)
7868 }
7869
7870 /// #372/#382: resolve the base register AND residual immediate offset for an
7871 /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7872 /// `(base, low_offset)`; the caller accesses the halves at `[base,
7873 /// #low_offset]` and `[base, #low_offset + 4]`.
7874 ///
7875 /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7876 /// returns `(addr.base, off)` and emits NOTHING — byte-identical.
7877 /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7878 /// with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7879 /// `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7880 /// Byte-identical to the pre-#382 (#372) behavior.
7881 /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7882 /// high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7883 /// rightly refused it and the WHOLE function was skipped (#382). Instead
7884 /// MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7885 /// the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7886 /// `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7887 /// `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7888 ///
7889 /// The effective address is fully materialized into `ip` BEFORE the halves
7890 /// are accessed, so an `rdlo` aliasing the index register is safe.
7891 fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7892 let offset = if addr.offset < 0 {
7893 0u32
7894 } else {
7895 addr.offset as u32
7896 };
7897 match addr.offset_reg {
7898 Some(idx) => {
7899 let ip = Reg::R12;
7900 if offset.wrapping_add(4) > 0xFFF {
7901 // Large static offset (#382): fold it (and R11) into ip so the
7902 // imm12 halves stay in range instead of skipping the function.
7903 // ADD ip, index, #offset (index != ip → no add_imm alias trap)
7904 bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7905 // ADD.W ip, ip, base (+ R11)
7906 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7907 reg_to_bits(&ip),
7908 reg_to_bits(&ip),
7909 reg_to_bits(&addr.base),
7910 )?);
7911 Ok((ip, 0))
7912 } else {
7913 // ADD.W ip, addr.base, idx (Thumb-2, byte-verified vs as)
7914 let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7915 let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7916 bytes.extend_from_slice(&hw1.to_le_bytes());
7917 bytes.extend_from_slice(&hw2.to_le_bytes());
7918 Ok((ip, offset))
7919 }
7920 }
7921 None => Ok((addr.base, offset)),
7922 }
7923 }
7924
7925 /// Encode Thumb-2 32-bit LDR
7926 fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7927 let rd_bits = reg_to_bits(rd);
7928 let base_bits = reg_to_bits(base);
7929
7930 // LDR.W Rd, [Rn, #imm12]
7931 check_ldst_imm12(offset)?;
7932 let hw1: u16 = (0xF8D0 | base_bits) as u16;
7933 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7934
7935 let mut bytes = hw1.to_le_bytes().to_vec();
7936 bytes.extend_from_slice(&hw2.to_le_bytes());
7937 Ok(bytes)
7938 }
7939
7940 /// Encode Thumb-2 32-bit STR
7941 fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7942 let rd_bits = reg_to_bits(rd);
7943 let base_bits = reg_to_bits(base);
7944
7945 // STR.W Rd, [Rn, #imm12]
7946 check_ldst_imm12(offset)?;
7947 let hw1: u16 = (0xF8C0 | base_bits) as u16;
7948 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7949
7950 let mut bytes = hw1.to_le_bytes().to_vec();
7951 bytes.extend_from_slice(&hw2.to_le_bytes());
7952 Ok(bytes)
7953 }
7954
7955 /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7956 fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7957 let rd_bits = reg_to_bits(rd);
7958 let base_bits = reg_to_bits(base);
7959 let rm_bits = reg_to_bits(offset_reg);
7960
7961 // LDR.W Rd, [Rn, Rm, LSL #0]
7962 // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7963 // imm2 = 00 for no shift (LSL #0)
7964 let hw1: u16 = (0xF850 | base_bits) as u16;
7965 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7966
7967 let mut bytes = hw1.to_le_bytes().to_vec();
7968 bytes.extend_from_slice(&hw2.to_le_bytes());
7969 Ok(bytes)
7970 }
7971
7972 /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7973 fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7974 let rd_bits = reg_to_bits(rd);
7975 let base_bits = reg_to_bits(base);
7976 let rm_bits = reg_to_bits(offset_reg);
7977
7978 // STR.W Rd, [Rn, Rm, LSL #0]
7979 // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7980 // imm2 = 00 for no shift (LSL #0)
7981 let hw1: u16 = (0xF840 | base_bits) as u16;
7982 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7983
7984 let mut bytes = hw1.to_le_bytes().to_vec();
7985 bytes.extend_from_slice(&hw2.to_le_bytes());
7986 Ok(bytes)
7987 }
7988
7989 // === Sub-word load/store Thumb-2 encoding helpers ===
7990
7991 /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7992 fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7993 let rd_bits = reg_to_bits(rd);
7994 let base_bits = reg_to_bits(base);
7995 // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7996 check_ldst_imm12(offset)?;
7997 let hw1: u16 = (0xF890 | base_bits) as u16;
7998 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7999 let mut bytes = hw1.to_le_bytes().to_vec();
8000 bytes.extend_from_slice(&hw2.to_le_bytes());
8001 Ok(bytes)
8002 }
8003
8004 /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
8005 fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8006 let rd_bits = reg_to_bits(rd);
8007 let base_bits = reg_to_bits(base);
8008 let rm_bits = reg_to_bits(offset_reg);
8009 // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
8010 let hw1: u16 = (0xF810 | base_bits) as u16;
8011 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8012 let mut bytes = hw1.to_le_bytes().to_vec();
8013 bytes.extend_from_slice(&hw2.to_le_bytes());
8014 Ok(bytes)
8015 }
8016
8017 /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
8018 fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8019 let rd_bits = reg_to_bits(rd);
8020 let base_bits = reg_to_bits(base);
8021 // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
8022 check_ldst_imm12(offset)?;
8023 let hw1: u16 = (0xF990 | base_bits) as u16;
8024 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8025 let mut bytes = hw1.to_le_bytes().to_vec();
8026 bytes.extend_from_slice(&hw2.to_le_bytes());
8027 Ok(bytes)
8028 }
8029
8030 /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8031 fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8032 let rd_bits = reg_to_bits(rd);
8033 let base_bits = reg_to_bits(base);
8034 let rm_bits = reg_to_bits(offset_reg);
8035 // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8036 let hw1: u16 = (0xF910 | base_bits) as u16;
8037 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8038 let mut bytes = hw1.to_le_bytes().to_vec();
8039 bytes.extend_from_slice(&hw2.to_le_bytes());
8040 Ok(bytes)
8041 }
8042
8043 /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8044 fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8045 let rd_bits = reg_to_bits(rd);
8046 let base_bits = reg_to_bits(base);
8047 // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8048 check_ldst_imm12(offset)?;
8049 let hw1: u16 = (0xF8B0 | base_bits) as u16;
8050 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8051 let mut bytes = hw1.to_le_bytes().to_vec();
8052 bytes.extend_from_slice(&hw2.to_le_bytes());
8053 Ok(bytes)
8054 }
8055
8056 /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8057 fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8058 let rd_bits = reg_to_bits(rd);
8059 let base_bits = reg_to_bits(base);
8060 let rm_bits = reg_to_bits(offset_reg);
8061 // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8062 let hw1: u16 = (0xF830 | base_bits) as u16;
8063 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8064 let mut bytes = hw1.to_le_bytes().to_vec();
8065 bytes.extend_from_slice(&hw2.to_le_bytes());
8066 Ok(bytes)
8067 }
8068
8069 /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8070 fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8071 let rd_bits = reg_to_bits(rd);
8072 let base_bits = reg_to_bits(base);
8073 // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8074 check_ldst_imm12(offset)?;
8075 let hw1: u16 = (0xF9B0 | base_bits) as u16;
8076 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8077 let mut bytes = hw1.to_le_bytes().to_vec();
8078 bytes.extend_from_slice(&hw2.to_le_bytes());
8079 Ok(bytes)
8080 }
8081
8082 /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8083 fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8084 let rd_bits = reg_to_bits(rd);
8085 let base_bits = reg_to_bits(base);
8086 let rm_bits = reg_to_bits(offset_reg);
8087 // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8088 let hw1: u16 = (0xF930 | base_bits) as u16;
8089 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8090 let mut bytes = hw1.to_le_bytes().to_vec();
8091 bytes.extend_from_slice(&hw2.to_le_bytes());
8092 Ok(bytes)
8093 }
8094
8095 /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8096 fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8097 let rd_bits = reg_to_bits(rd);
8098 let base_bits = reg_to_bits(base);
8099 // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8100 check_ldst_imm12(offset)?;
8101 let hw1: u16 = (0xF880 | base_bits) as u16;
8102 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8103 let mut bytes = hw1.to_le_bytes().to_vec();
8104 bytes.extend_from_slice(&hw2.to_le_bytes());
8105 Ok(bytes)
8106 }
8107
8108 /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8109 fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8110 let rd_bits = reg_to_bits(rd);
8111 let base_bits = reg_to_bits(base);
8112 let rm_bits = reg_to_bits(offset_reg);
8113 // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8114 let hw1: u16 = (0xF800 | base_bits) as u16;
8115 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8116 let mut bytes = hw1.to_le_bytes().to_vec();
8117 bytes.extend_from_slice(&hw2.to_le_bytes());
8118 Ok(bytes)
8119 }
8120
8121 /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8122 fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8123 let rd_bits = reg_to_bits(rd);
8124 let base_bits = reg_to_bits(base);
8125 // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8126 check_ldst_imm12(offset)?;
8127 let hw1: u16 = (0xF8A0 | base_bits) as u16;
8128 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8129 let mut bytes = hw1.to_le_bytes().to_vec();
8130 bytes.extend_from_slice(&hw2.to_le_bytes());
8131 Ok(bytes)
8132 }
8133
8134 /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8135 fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8136 let rd_bits = reg_to_bits(rd);
8137 let base_bits = reg_to_bits(base);
8138 let rm_bits = reg_to_bits(offset_reg);
8139 // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8140 let hw1: u16 = (0xF820 | base_bits) as u16;
8141 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8142 let mut bytes = hw1.to_le_bytes().to_vec();
8143 bytes.extend_from_slice(&hw2.to_le_bytes());
8144 Ok(bytes)
8145 }
8146
8147 /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8148 fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8149 let rd_bits = reg_to_bits(rd);
8150 let rn_bits = reg_to_bits(rn);
8151
8152 // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8153 // which picks the correct form per value:
8154 // - imm <= 0xFF -> ADD.W (T3). Its `i:imm3:imm8` field is a
8155 // ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8156 // - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8157 //
8158 // #681: this function used to pack the raw value into the T3 field for
8159 // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8160 // = 0x8000_0000, so every dynamic-address load/store with a static
8161 // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8162 // --safety-bounds software the guard checked the intended address while
8163 // the access used the mis-encoded one (bounds bypass). Same
8164 // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8165 if imm <= 0xFFF {
8166 self.encode_thumb32_add(rd, rn, imm)
8167 } else {
8168 // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8169 // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8170 // "encoder must produce a legal sequence, not assert" class — see #350.
8171 //
8172 // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8173 // the ADD reads it):
8174 // - rd != rn => use rd itself (rn is untouched, since rd != rn).
8175 // - rd == rn => use R12/IP (the reserved encoder scratch). rd/rn are
8176 // never R12 (R12 is non-allocatable), so it can't alias.
8177 //
8178 // The materialized value is the same whether or not MOVT is emitted, so
8179 // the byte length depends only on `imm` (and rd==rn) — the size probe and
8180 // the final emit therefore agree (mandatory: the function is encoded twice).
8181 let scratch: u32 = if rd_bits == rn_bits {
8182 12 // R12/IP — in-place add, can't use rd because rd == rn
8183 } else {
8184 rd_bits // rn is preserved because rd != rn
8185 };
8186 // Invariant: the scratch must never alias Rn (would clobber it before
8187 // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8188 // which is reserved encoder scratch), but the encoder is also driven by
8189 // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8190 // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8191 // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8192 // instead of asserting. #350 follow-up.
8193 if scratch == rn_bits {
8194 return Err(synth_core::Error::synthesis(format!(
8195 "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8196 register (R12 is the reserved encoder scratch and aliases Rn here)"
8197 )));
8198 }
8199
8200 let lo16 = imm & 0xFFFF;
8201 let hi16 = (imm >> 16) & 0xFFFF;
8202
8203 let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8204 if hi16 != 0 {
8205 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8206 }
8207 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8208 Ok(bytes)
8209 }
8210 }
8211
8212 // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8213
8214 /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8215 ///
8216 /// # Contract (Verus-style)
8217 /// ```text
8218 /// requires rd <= 14, imm16 <= 0xFFFF
8219 /// ensures result.len() == 4
8220 /// ```
8221 fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8222 reg_bits_checked(rd)?;
8223 encoding_contracts::verify_imm16(imm16);
8224 // MOVW Rd, #imm16
8225 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8226 let imm16 = imm16 & 0xFFFF;
8227 let imm4 = (imm16 >> 12) & 0xF;
8228 let i_bit = (imm16 >> 11) & 1;
8229 let imm3 = (imm16 >> 8) & 0x7;
8230 let imm8 = imm16 & 0xFF;
8231
8232 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8233 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8234
8235 let mut bytes = hw1.to_le_bytes().to_vec();
8236 bytes.extend_from_slice(&hw2.to_le_bytes());
8237 encoding_contracts::verify_thumb32(&bytes);
8238 Ok(bytes)
8239 }
8240
8241 /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8242 ///
8243 /// # Contract (Verus-style)
8244 /// ```text
8245 /// requires rd <= 14, imm16 <= 0xFFFF
8246 /// ensures result.len() == 4
8247 /// ```
8248 fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8249 reg_bits_checked(rd)?;
8250 encoding_contracts::verify_imm16(imm16);
8251 // MOVT Rd, #imm16
8252 // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8253 let imm16 = imm16 & 0xFFFF;
8254 let imm4 = (imm16 >> 12) & 0xF;
8255 let i_bit = (imm16 >> 11) & 1;
8256 let imm3 = (imm16 >> 8) & 0x7;
8257 let imm8 = imm16 & 0xFF;
8258
8259 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8260 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8261
8262 let mut bytes = hw1.to_le_bytes().to_vec();
8263 bytes.extend_from_slice(&hw2.to_le_bytes());
8264 encoding_contracts::verify_thumb32(&bytes);
8265 Ok(bytes)
8266 }
8267
8268 /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8269 fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8270 // MOV.W Rd, Rm, LSR #imm
8271 // EA4F 0 imm3 Rd imm2 01 Rm
8272 let imm5 = shift & 0x1F;
8273 let imm2 = imm5 & 0x3;
8274 let imm3 = (imm5 >> 2) & 0x7;
8275
8276 let hw1: u16 = 0xEA4F;
8277 let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8278
8279 let mut bytes = hw1.to_le_bytes().to_vec();
8280 bytes.extend_from_slice(&hw2.to_le_bytes());
8281 Ok(bytes)
8282 }
8283
8284 /// Encode Thumb-2 32-bit AND with immediate - raw version
8285 fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8286 // AND.W Rd, Rn, #<modified_immediate>
8287 // F0 00 Rn | 0 imm3 Rd imm8
8288 //
8289 // #681 class audit: the field is a ThumbExpandImm modified immediate,
8290 // not a raw value. The only current caller (POPCNT final mask) passes
8291 // 0x3F, which expands to itself — the gate is byte-identical today and
8292 // closes the raw-packing landmine for any future caller.
8293 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8294 synth_core::Error::synthesis(
8295 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8296 )
8297 })?;
8298 let i_bit = (field >> 11) & 1;
8299 let imm3 = (field >> 8) & 0x7;
8300 let imm8 = field & 0xFF;
8301
8302 let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8303 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8304
8305 let mut bytes = hw1.to_le_bytes().to_vec();
8306 bytes.extend_from_slice(&hw2.to_le_bytes());
8307 Ok(bytes)
8308 }
8309
8310 /// Encode Thumb-2 32-bit SUB (register) - raw version
8311 fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8312 // SUB.W Rd, Rn, Rm
8313 // EBA0 Rn | 0 Rd 00 00 Rm
8314 let hw1: u16 = (0xEBA0 | rn) as u16;
8315 let hw2: u16 = ((rd << 8) | rm) as u16;
8316
8317 let mut bytes = hw1.to_le_bytes().to_vec();
8318 bytes.extend_from_slice(&hw2.to_le_bytes());
8319 Ok(bytes)
8320 }
8321
8322 /// Encode Thumb-2 32-bit ADD (register) - raw version
8323 fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8324 // ADD.W Rd, Rn, Rm
8325 // EB00 Rn | 0 Rd 00 00 Rm
8326 let hw1: u16 = (0xEB00 | rn) as u16;
8327 let hw2: u16 = ((rd << 8) | rm) as u16;
8328
8329 let mut bytes = hw1.to_le_bytes().to_vec();
8330 bytes.extend_from_slice(&hw2.to_le_bytes());
8331 Ok(bytes)
8332 }
8333
8334 /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8335 /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8336 /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8337 fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8338 // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8339 let hw1: u16 = (0xEB10 | rn) as u16;
8340 let hw2: u16 = ((rd << 8) | rm) as u16;
8341 let mut bytes = hw1.to_le_bytes().to_vec();
8342 bytes.extend_from_slice(&hw2.to_le_bytes());
8343 Ok(bytes)
8344 }
8345
8346 /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8347 /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8348 fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8349 // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8350 let hw1: u16 = (0xEBB0 | rn) as u16;
8351 let hw2: u16 = ((rd << 8) | rm) as u16;
8352 let mut bytes = hw1.to_le_bytes().to_vec();
8353 bytes.extend_from_slice(&hw2.to_le_bytes());
8354 Ok(bytes)
8355 }
8356
8357 /// Encode a sequence of ARM instructions
8358 pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8359 let mut code = Vec::new();
8360
8361 for op in ops {
8362 let encoded = self.encode(op)?;
8363 code.extend_from_slice(&encoded);
8364 }
8365
8366 Ok(code)
8367 }
8368}
8369
8370/// Convert register to bit encoding (0-15)
8371/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8372/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8373/// `None` (the caller must materialize the value into a register). This is the
8374/// shared correct path for the data-processing immediate encoders — without it
8375/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8376/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8377fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8378 // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8379 if value <= 0xFF {
8380 return Some(value);
8381 }
8382 let b0 = value & 0xFF; // byte 0
8383 let b1 = (value >> 8) & 0xFF; // byte 1
8384 // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8385 if value == (b0 << 16) | b0 {
8386 return Some(0x100 | b0);
8387 }
8388 // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8389 if value == (b1 << 24) | (b1 << 8) {
8390 return Some(0x200 | b1);
8391 }
8392 // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8393 if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8394 return Some(0x300 | b0);
8395 }
8396 // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8397 // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8398 // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8399 for rot in 8..=31u32 {
8400 let unrot = value.rotate_left(rot);
8401 if (0x80..=0xFF).contains(&unrot) {
8402 return Some((rot << 7) | (unrot & 0x7F));
8403 }
8404 }
8405 None
8406}
8407
8408/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8409/// `0..=4095`; a larger offset must be materialized into a register by the
8410/// selector (register-offset addressing). Returning `Err` rather than silently
8411/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8412/// the load/store sibling of #253/#255).
8413fn check_ldst_imm12(offset: u32) -> Result<()> {
8414 if offset > 0xFFF {
8415 Err(synth_core::Error::synthesis(
8416 "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8417 ))
8418 } else {
8419 Ok(())
8420 }
8421}
8422
8423/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8424///
8425/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8426/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8427/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8428/// not a move at all. The destination is never written (it keeps stale data)
8429/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8430/// defect #311 fixed for `I64SetCond`.
8431///
8432/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8433/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8434/// does not set flags, which is what these zero-fill sites want anyway.
8435///
8436/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8437/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8438/// PAST this instruction moves when it widens and its displacement has to be
8439/// derived rather than hard-coded. (A branch targeting this instruction's own
8440/// address is unaffected — an instruction cannot move itself.)
8441fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8442 if rd_bits < 8 {
8443 let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8444 bytes.extend_from_slice(&movs.to_le_bytes());
8445 } else {
8446 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8447 bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8448 }
8449}
8450
8451/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8452/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8453/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8454/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8455/// `estimator_encoder_agreement` oracle).
8456fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8457 if rd_bits < 8 { 1 } else { 2 }
8458}
8459
8460fn reg_to_bits(reg: &Reg) -> u32 {
8461 match reg {
8462 Reg::R0 => 0,
8463 Reg::R1 => 1,
8464 Reg::R2 => 2,
8465 Reg::R3 => 3,
8466 Reg::R4 => 4,
8467 Reg::R5 => 5,
8468 Reg::R6 => 6,
8469 Reg::R7 => 7,
8470 Reg::R8 => 8,
8471 Reg::R9 => 9,
8472 Reg::R10 => 10,
8473 Reg::R11 => 11,
8474 Reg::R12 => 12,
8475 Reg::SP => 13,
8476 Reg::LR => 14,
8477 Reg::PC => 15,
8478 }
8479}
8480
8481// ======================================================================
8482// #610 — i64 fixed-ABI expansion wrappers.
8483//
8484// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8485// shift-subtract loops) compute in FIXED low registers. Before #610 the
8486// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8487// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8488// collided with selector-assigned registers — then restored the saved
8489// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8490// returned the caller's stale register: 0 for every input under qemu.
8491//
8492// These wrappers make each core honor its register parameters:
8493// 1. save R0-R3,
8494// 2. marshal the operand registers into the core's fixed input regs via
8495// the stack (permutation-safe: every source is read before any fixed
8496// register is written),
8497// 3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8498// scratch and never allocatable, #212),
8499// 4. MOV the result pair from R0:R1 into the selector's rd pair,
8500// 5. restore R0-R3, skipping any register the result now occupies.
8501//
8502// All emitted lengths are register-independent so the optimized path's
8503// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8504// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8505// ======================================================================
8506
8507/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8508/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8509/// before any destination register is written, so arbitrary source/target
8510/// permutations (including operands living in R0-R3) are safe.
8511fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8512 debug_assert!(srcs.len() <= 4);
8513 // PUSH {R0-R3} — save the caller-visible low registers.
8514 bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8515 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8516 for src in srcs.iter().rev() {
8517 let rt = reg_to_bits(src) as u16;
8518 bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8519 bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8520 }
8521 // POP {Ri} — Ri := srcs[i].
8522 for i in 0..srcs.len() as u16 {
8523 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8524 }
8525}
8526
8527/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8528/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8529/// register the result now lives in (its saved caller word is discarded).
8530fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8531 let lo = reg_to_bits(rdlo);
8532 let hi = reg_to_bits(rdhi);
8533 if lo == 1 && hi == 0 {
8534 // A fully swapped pair would clobber one half in either MOV order.
8535 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8536 return Err(synth_core::Error::synthesis(
8537 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8538 ));
8539 }
8540 let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8541 let d = ((rd >> 3) & 1) as u16;
8542 bytes.extend_from_slice(
8543 &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8544 );
8545 };
8546 if hi == 0 {
8547 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8548 mov16(bytes, lo, 0);
8549 mov16(bytes, hi, 1);
8550 } else {
8551 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8552 mov16(bytes, hi, 1);
8553 mov16(bytes, lo, 0);
8554 }
8555 for i in 0..4u32 {
8556 if i == lo || i == hi {
8557 // The result lives here — drop the saved caller word.
8558 bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8559 } else {
8560 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8561 }
8562 }
8563 Ok(())
8564}
8565
8566/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8567/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8568/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8569fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8570 bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8571 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8572 bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8573 bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8574}
8575
8576/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8577/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8578/// guard — #633: without it the core negated INT64_MIN onto itself and
8579/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8580/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8581///
8582/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8583/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8584/// register-independent (estimator contract, #498/#511).
8585fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8586 // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8587 bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8588 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8589 // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8590 bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8591 bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8592 // BNE .no_trap
8593 bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8594 // CMP R0, #0 — dividend lo word of INT64_MIN
8595 bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8596 // BNE .no_trap
8597 bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8598 // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8599 bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8600 bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8601 // BNE .no_trap
8602 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8603 // UDF #0 — signed-division overflow
8604 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8605 // .no_trap:
8606}
8607
8608// ======================================================================
8609// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8610// Identical register contract, A32 encodings: the multi-instruction i64
8611// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8612// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8613// the selector-assigned operand registers in and the result out, saving and
8614// restoring the caller-visible R0-R3 around the core.
8615// ======================================================================
8616
8617/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8618/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8619/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8620/// written, so arbitrary source/target permutations are safe.
8621fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8622 debug_assert!(srcs.len() <= 4);
8623 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8624 // PUSH {R0-R3} — save the caller-visible low registers.
8625 w(bytes, 0xE92D_000F);
8626 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8627 for src in srcs.iter().rev() {
8628 w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8629 }
8630 // LDR Ri, [SP], #4 — Ri := srcs[i].
8631 for i in 0..srcs.len() as u32 {
8632 w(bytes, 0xE49D_0004 | (i << 12));
8633 }
8634}
8635
8636/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8637/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8638/// any register the result now lives in (its saved caller word is discarded).
8639fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8640 let lo = reg_to_bits(rdlo);
8641 let hi = reg_to_bits(rdhi);
8642 if lo == 1 && hi == 0 {
8643 // A fully swapped pair would clobber one half in either MOV order.
8644 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8645 return Err(synth_core::Error::synthesis(
8646 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8647 ));
8648 }
8649 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8650 let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8651 if hi == 0 {
8652 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8653 mov(bytes, lo, 0);
8654 mov(bytes, hi, 1);
8655 } else {
8656 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8657 mov(bytes, hi, 1);
8658 mov(bytes, lo, 0);
8659 }
8660 for i in 0..4u32 {
8661 if i == lo || i == hi {
8662 // The result lives here — drop the saved caller word.
8663 w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8664 } else {
8665 w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8666 }
8667 }
8668 Ok(())
8669}
8670
8671/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8672/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8673/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8674fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8675 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8676 w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8677 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8678 w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8679}
8680
8681/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8682/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8683/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8684/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8685fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8686 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8687 w(bytes, 0xE002_C003); // AND R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8688 w(bytes, 0xE37C_0001); // CMN R12, #1 (EQ iff divisor == -1)
8689 w(bytes, 0x0350_0000); // CMPEQ R0, #0 (EQ iff also dividend lo == 0)
8690 w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8691 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8692 w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8693}
8694
8695/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8696/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8697/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8698/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8699/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8700/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8701/// Returns a typed Err instead. See #185.
8702fn reg_bits_checked(bits: u32) -> Result<()> {
8703 if bits > 14 {
8704 return Err(synth_core::Error::synthesis(format!(
8705 "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8706 )));
8707 }
8708 Ok(())
8709}
8710
8711/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8712/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8713fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8714 if val == 0 {
8715 return Some((0, 1));
8716 }
8717 for rot in 0..16u32 {
8718 let shift = rot * 2;
8719 // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8720 let unrotated = val.rotate_left(shift);
8721 if unrotated <= 0xFF {
8722 // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8723 return Some(((rot << 8) | unrotated, 1));
8724 }
8725 }
8726 None
8727}
8728
8729/// Encode operand2 field and return (bits, immediate_flag).
8730/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8731/// Panics if an immediate value cannot be represented. Callers that need large
8732/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8733fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8734 match op2 {
8735 Operand2::Imm(val) => {
8736 let uval = *val as u32;
8737 // Attempt rotated-immediate encoding (ARM32 Operand2)
8738 if let Some(encoded) = try_encode_rotated_imm(uval) {
8739 Ok(encoded)
8740 } else {
8741 // #378-class honesty: an immediate that can't be expressed as an
8742 // ARM32 rotated immediate is an INTERNAL selector bug — large
8743 // constants must be materialized via MOVW/MOVT, not passed here.
8744 // FAIL HONESTLY with an Err rather than silently masking to
8745 // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8746 // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8747 // this is an Err and not a panic (the `encoder_no_panic` fuzz
8748 // contract — malformed/oversized input must degrade, not crash).
8749 Err(synth_core::Error::synthesis(format!(
8750 "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8751 rotated immediate — the selector must materialize large \
8752 constants via MOVW/MOVT"
8753 )))
8754 }
8755 }
8756
8757 Operand2::Reg(reg) => {
8758 let reg_bits = reg_to_bits(reg);
8759 Ok((reg_bits, 0)) // I=0 for register
8760 }
8761
8762 Operand2::RegShift {
8763 rm,
8764 shift: _,
8765 amount,
8766 } => {
8767 // Simplified encoding with shift
8768 let rm_bits = reg_to_bits(rm);
8769 let shift_bits = (*amount & 0x1F) << 7;
8770 Ok((shift_bits | rm_bits, 0))
8771 }
8772 }
8773}
8774
8775/// Encode memory address to (base_reg, offset)
8776fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8777 let base_bits = reg_to_bits(&addr.base);
8778 let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8779 (base_bits, offset_bits)
8780}
8781
8782/// S-register number: S0=0, S1=1, ..., S31=31
8783fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8784 match reg {
8785 VfpReg::S0 => Ok(0),
8786 VfpReg::S1 => Ok(1),
8787 VfpReg::S2 => Ok(2),
8788 VfpReg::S3 => Ok(3),
8789 VfpReg::S4 => Ok(4),
8790 VfpReg::S5 => Ok(5),
8791 VfpReg::S6 => Ok(6),
8792 VfpReg::S7 => Ok(7),
8793 VfpReg::S8 => Ok(8),
8794 VfpReg::S9 => Ok(9),
8795 VfpReg::S10 => Ok(10),
8796 VfpReg::S11 => Ok(11),
8797 VfpReg::S12 => Ok(12),
8798 VfpReg::S13 => Ok(13),
8799 VfpReg::S14 => Ok(14),
8800 VfpReg::S15 => Ok(15),
8801 VfpReg::S16 => Ok(16),
8802 VfpReg::S17 => Ok(17),
8803 VfpReg::S18 => Ok(18),
8804 VfpReg::S19 => Ok(19),
8805 VfpReg::S20 => Ok(20),
8806 VfpReg::S21 => Ok(21),
8807 VfpReg::S22 => Ok(22),
8808 VfpReg::S23 => Ok(23),
8809 VfpReg::S24 => Ok(24),
8810 VfpReg::S25 => Ok(25),
8811 VfpReg::S26 => Ok(26),
8812 VfpReg::S27 => Ok(27),
8813 VfpReg::S28 => Ok(28),
8814 VfpReg::S29 => Ok(29),
8815 VfpReg::S30 => Ok(30),
8816 VfpReg::S31 => Ok(31),
8817 // D-registers are not used in F32 single-precision encodings
8818 _ => Err(synth_core::Error::SynthesisError(
8819 "D-register not supported in single-precision VFP encoding".to_string(),
8820 )),
8821 }
8822}
8823
8824/// D-register number: D0=0, D1=1, ..., D15=15
8825fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8826 match reg {
8827 VfpReg::D0 => Ok(0),
8828 VfpReg::D1 => Ok(1),
8829 VfpReg::D2 => Ok(2),
8830 VfpReg::D3 => Ok(3),
8831 VfpReg::D4 => Ok(4),
8832 VfpReg::D5 => Ok(5),
8833 VfpReg::D6 => Ok(6),
8834 VfpReg::D7 => Ok(7),
8835 VfpReg::D8 => Ok(8),
8836 VfpReg::D9 => Ok(9),
8837 VfpReg::D10 => Ok(10),
8838 VfpReg::D11 => Ok(11),
8839 VfpReg::D12 => Ok(12),
8840 VfpReg::D13 => Ok(13),
8841 VfpReg::D14 => Ok(14),
8842 VfpReg::D15 => Ok(15),
8843 // S-registers are not used in F64 double-precision encodings
8844 _ => Err(synth_core::Error::SynthesisError(
8845 "S-register not supported in double-precision VFP encoding".to_string(),
8846 )),
8847 }
8848}
8849
8850/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8851/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8852/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8853fn encode_sreg(s: u32) -> (u32, u32) {
8854 (s >> 1, s & 1)
8855}
8856
8857/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8858/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8859/// For D0-D15, qualifier is always 0.
8860fn encode_dreg(d: u32) -> (u32, u32) {
8861 (d & 0xF, (d >> 4) & 1)
8862}
8863
8864/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8865/// Returns the full 32-bit instruction word.
8866///
8867/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8868/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8869fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8870 let sd_num = vfp_sreg_to_num(sd)?;
8871 let sn_num = vfp_sreg_to_num(sn)?;
8872 let sm_num = vfp_sreg_to_num(sm)?;
8873 let (vd, d) = encode_sreg(sd_num);
8874 let (vn, n) = encode_sreg(sn_num);
8875 let (vm, m) = encode_sreg(sm_num);
8876
8877 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8878}
8879
8880/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8881/// Returns the full 32-bit instruction word.
8882fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8883 let sd_num = vfp_sreg_to_num(sd)?;
8884 let sm_num = vfp_sreg_to_num(sm)?;
8885 let (vd, d) = encode_sreg(sd_num);
8886 let (vm, m) = encode_sreg(sm_num);
8887
8888 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8889}
8890
8891/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8892/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8893/// U bit (bit 23) controls add/subtract offset.
8894fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8895 let sd_num = vfp_sreg_to_num(sd)?;
8896 let (vd, d) = encode_sreg(sd_num);
8897 let rn = reg_to_bits(&addr.base);
8898
8899 let offset = addr.offset;
8900 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8901 let abs_offset = offset.unsigned_abs();
8902 let imm8 = (abs_offset / 4) & 0xFF;
8903
8904 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8905}
8906
8907/// Encode VMOV between core register and S-register.
8908/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8909/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8910fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8911 let s_num = vfp_sreg_to_num(sreg)?;
8912 let (vn, n) = encode_sreg(s_num);
8913 let rt = reg_to_bits(core);
8914
8915 let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8916 Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8917}
8918
8919/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8920/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8921/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8922fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8923 let dd_num = vfp_dreg_to_num(dd)?;
8924 let dn_num = vfp_dreg_to_num(dn)?;
8925 let dm_num = vfp_dreg_to_num(dm)?;
8926 let (vd, d) = encode_dreg(dd_num);
8927 let (vn, n) = encode_dreg(dn_num);
8928 let (vm, m) = encode_dreg(dm_num);
8929
8930 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8931}
8932
8933/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8934fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8935 let dd_num = vfp_dreg_to_num(dd)?;
8936 let dm_num = vfp_dreg_to_num(dm)?;
8937 let (vd, d) = encode_dreg(dd_num);
8938 let (vm, m) = encode_dreg(dm_num);
8939
8940 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8941}
8942
8943/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8944/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8945fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8946 let dd_num = vfp_dreg_to_num(dd)?;
8947 let (vd, d) = encode_dreg(dd_num);
8948 let rn = reg_to_bits(&addr.base);
8949
8950 let offset = addr.offset;
8951 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8952 let abs_offset = offset.unsigned_abs();
8953 let imm8 = (abs_offset / 4) & 0xFF;
8954
8955 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8956}
8957
8958/// Encode VMOV between two core registers and a D-register.
8959/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8960/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8961fn encode_vmov_core_dreg(
8962 to_dreg: bool,
8963 dreg: &VfpReg,
8964 core_lo: &Reg,
8965 core_hi: &Reg,
8966) -> Result<u32> {
8967 let d_num = vfp_dreg_to_num(dreg)?;
8968 let (vm, m) = encode_dreg(d_num);
8969 let rt = reg_to_bits(core_lo);
8970 let rt2 = reg_to_bits(core_hi);
8971
8972 let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8973 Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8974}
8975
8976/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8977fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8978 let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8979 let hw2 = (instr & 0xFFFF) as u16;
8980 let mut bytes = hw1.to_le_bytes().to_vec();
8981 bytes.extend_from_slice(&hw2.to_le_bytes());
8982 bytes
8983}
8984
8985// ============================================================================
8986// Helium MVE encoding helpers
8987// ============================================================================
8988
8989/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8990fn qreg_to_num(reg: &QReg) -> u32 {
8991 match reg {
8992 QReg::Q0 => 0,
8993 QReg::Q1 => 1,
8994 QReg::Q2 => 2,
8995 QReg::Q3 => 3,
8996 QReg::Q4 => 4,
8997 QReg::Q5 => 5,
8998 QReg::Q6 => 6,
8999 QReg::Q7 => 7,
9000 }
9001}
9002
9003/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
9004fn mve_size_bits(size: &MveSize) -> u32 {
9005 match size {
9006 MveSize::S8 => 0b00,
9007 MveSize::S16 => 0b01,
9008 MveSize::S32 => 0b10,
9009 }
9010}
9011
9012/// Encode MVE 3-register instruction.
9013/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
9014/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
9015fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9016 let d = qreg_to_num(qd) * 2;
9017 let n = qreg_to_num(qn) * 2;
9018 let m = qreg_to_num(qm) * 2;
9019
9020 // Standard NEON/MVE 3-register encoding:
9021 // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
9022 // N bit (bit 7) = Vn[4], Vn[3:0] = bits [19:16]
9023 // M bit (bit 5) = Vm[4], Vm[3:0] = bits [3:0]
9024 let vd = d & 0xF;
9025 let d_bit = (d >> 4) & 1;
9026 let vn = n & 0xF;
9027 let n_bit = (n >> 4) & 1;
9028 let vm = m & 0xF;
9029 let m_bit = (m >> 4) & 1;
9030
9031 base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9032}
9033
9034/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9035fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9036 encode_mve_3reg(base, qd, qn, qm)
9037}
9038
9039/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9040/// Format: EC9x xxxx - contiguous load, word-sized elements
9041fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9042 let qd_enc = qreg_to_num(qd) * 2;
9043 let rn = reg_to_bits(&addr.base);
9044 let offset = addr.offset;
9045 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9046 let abs_offset = offset.unsigned_abs();
9047 let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9048
9049 // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9050 0xED100E80
9051 | (u_bit << 23)
9052 | ((qd_enc >> 4) << 22)
9053 | (rn << 16)
9054 | ((qd_enc & 0xF) << 12)
9055 | (imm7 & 0x7F)
9056}
9057
9058/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9059fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9060 let qd_enc = qreg_to_num(qd) * 2;
9061 let rn = reg_to_bits(&addr.base);
9062 let offset = addr.offset;
9063 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9064 let abs_offset = offset.unsigned_abs();
9065 let imm7 = (abs_offset / 4) & 0x7F;
9066
9067 0xED000E80
9068 | (u_bit << 23)
9069 | ((qd_enc >> 4) << 22)
9070 | (rn << 16)
9071 | ((qd_enc & 0xF) << 12)
9072 | (imm7 & 0x7F)
9073}
9074
9075impl ArmEncoder {
9076 /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9077 fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9078 let mut result = Vec::new();
9079 let qd_num = qreg_to_num(qd);
9080
9081 // Load each 32-bit word into R12 (temp) then VMOV into S-register
9082 for i in 0..4 {
9083 let word = u32::from_le_bytes([
9084 bytes[i * 4],
9085 bytes[i * 4 + 1],
9086 bytes[i * 4 + 2],
9087 bytes[i * 4 + 3],
9088 ]);
9089 let lo16 = word & 0xFFFF;
9090 let hi16 = (word >> 16) & 0xFFFF;
9091
9092 // MOVW R12, #lo16
9093 result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9094 // MOVT R12, #hi16
9095 if hi16 != 0 {
9096 result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9097 }
9098
9099 // VMOV Sn, R12 where Sn = Qd*4 + i
9100 let s_num = qd_num * 4 + i as u32;
9101 let (vn, n) = encode_sreg(s_num);
9102 let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9103 result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9104 }
9105
9106 Ok(result)
9107 }
9108
9109 /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9110 fn encode_thumb_mve_lane_wise_f32_binop(
9111 &self,
9112 qd: &QReg,
9113 qn: &QReg,
9114 qm: &QReg,
9115 vfp_base: u32,
9116 ) -> Result<Vec<u8>> {
9117 let mut result = Vec::new();
9118 let qd_num = qreg_to_num(qd);
9119 let qn_num = qreg_to_num(qn);
9120 let qm_num = qreg_to_num(qm);
9121
9122 // For each lane 0..3: use S-registers directly (Q aliasing)
9123 for i in 0..4u32 {
9124 let sd = qd_num * 4 + i;
9125 let sn = qn_num * 4 + i;
9126 let sm = qm_num * 4 + i;
9127
9128 let (vd, d) = encode_sreg(sd);
9129 let (vn, n) = encode_sreg(sn);
9130 let (vm, m) = encode_sreg(sm);
9131
9132 let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9133 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9134 }
9135
9136 Ok(result)
9137 }
9138
9139 /// Encode lane-wise f32 VSQRT via S-register extraction
9140 fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9141 let mut result = Vec::new();
9142 let qd_num = qreg_to_num(qd);
9143 let qm_num = qreg_to_num(qm);
9144
9145 // VSQRT.F32 base: 0xEEB10AC0
9146 for i in 0..4u32 {
9147 let sd = qd_num * 4 + i;
9148 let sm = qm_num * 4 + i;
9149
9150 let (vd, d) = encode_sreg(sd);
9151 let (vm, m) = encode_sreg(sm);
9152
9153 let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9154 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9155 }
9156
9157 Ok(result)
9158 }
9159}
9160
9161/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9162/// expansion: the registers, beyond the op's declared RESULT registers and the
9163/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9164/// modified when it completes. Transient-but-restored traffic (push/pop through
9165/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9166///
9167/// This is the SINGLE declaration site for that contract — the one place a
9168/// future expansion that must borrow a register says so (the repo rule: one
9169/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9170/// from the expansion's observed behavior: a contract read off the bytes would
9171/// rubber-stamp any clobber. Intent is declared here; the canary gate
9172/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9173/// bytes of every variant the shipped rule table emits, on both backends, with
9174/// every non-contract register holding a distinctive canary, and fails on any
9175/// undeclared write — and on any declared register the expansion never
9176/// actually writes, so an over-broad declaration cannot hollow the gate.
9177///
9178/// The default is the STRICTEST reading — result registers only — which is
9179/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9180/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9181/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9182/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9183/// the i64 shifts and bit-counts off their own operand registers, so every
9184/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9185/// A32. Backend-independent for the same reason; if a backend's expansion ever
9186/// diverges, this signature grows a backend parameter in the same PR.
9187pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9188 // No variant currently borrows any register beyond R12. A new declaration
9189 // is added as a `match op { .. }` arm here — nowhere else.
9190 let _ = op;
9191 &[]
9192}
9193
9194#[cfg(test)]
9195mod tests {
9196 use super::*;
9197
9198 #[test]
9199 fn test_encoder_creation() {
9200 let encoder_arm = ArmEncoder::new_arm32();
9201 assert!(!encoder_arm.thumb_mode);
9202
9203 let encoder_thumb = ArmEncoder::new_thumb2();
9204 assert!(encoder_thumb.thumb_mode);
9205 }
9206
9207 /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9208 /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9209 /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9210 /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9211 /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9212 /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9213 /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9214 /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9215 /// #204 hardening. With rd=R8 the boolean died in the flags
9216 /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9217 /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9218 #[test]
9219 fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9220 use synth_synthesis::{ArmOp, Condition, Reg};
9221 let enc = ArmEncoder::new_thumb2();
9222 let bytes = enc
9223 .encode(&ArmOp::I64SetCond {
9224 rd: Reg::R8,
9225 rn_lo: Reg::R2,
9226 rn_hi: Reg::R3,
9227 rm_lo: Reg::R6,
9228 rm_hi: Reg::R7,
9229 cond: Condition::EQ,
9230 })
9231 .unwrap();
9232 // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9233 // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9234 let halfwords: Vec<u16> = bytes
9235 .chunks(2)
9236 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9237 .collect();
9238 assert!(
9239 halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9240 "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9241 );
9242 assert!(
9243 !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9244 "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9245 );
9246
9247 let bytes_z = enc
9248 .encode(&ArmOp::I64SetCondZ {
9249 rd: Reg::R8,
9250 rn_lo: Reg::R2,
9251 rn_hi: Reg::R3,
9252 })
9253 .unwrap();
9254 let hw_z: Vec<u16> = bytes_z
9255 .chunks(2)
9256 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9257 .collect();
9258 assert!(
9259 hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9260 "SetCondZ high rd MOV.W: {hw_z:04x?}"
9261 );
9262 // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9263 assert!(
9264 hw_z.contains(&(0xF1B0 | 8)),
9265 "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9266 );
9267 }
9268
9269 #[test]
9270 fn test_encode_setcond_high_reg_uses_mov_w_204() {
9271 use synth_synthesis::{ArmOp, Condition, Reg};
9272 let enc = ArmEncoder::new_thumb2();
9273 // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9274 let hi = enc
9275 .encode(&ArmOp::SetCond {
9276 rd: Reg::R12,
9277 cond: Condition::NE,
9278 })
9279 .unwrap();
9280 assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9281 // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9282 assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9283 assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9284 assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9285 assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9286 // Low Rd keeps the compact 16-bit MOVS form.
9287 let lo = enc
9288 .encode(&ArmOp::SetCond {
9289 rd: Reg::R0,
9290 cond: Condition::NE,
9291 })
9292 .unwrap();
9293 assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9294 assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9295 assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9296 }
9297
9298 /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9299 /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9300 /// A32: cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9301 #[test]
9302 fn test_encode_umull_209b() {
9303 use synth_synthesis::{ArmOp, Reg};
9304 let op = ArmOp::Umull {
9305 rdlo: Reg::R4,
9306 rdhi: Reg::R5,
9307 rn: Reg::R0,
9308 rm: Reg::R3,
9309 };
9310 // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9311 let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9312 assert_eq!(
9313 t,
9314 vec![0xA0, 0xFB, 0x03, 0x45],
9315 "umull r4,r5,r0,r3 (T2): {t:02x?}"
9316 );
9317 // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9318 let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9319 assert_eq!(
9320 a,
9321 0xE085_4390u32.to_le_bytes().to_vec(),
9322 "umull (A32): {a:02x?}"
9323 );
9324 }
9325
9326 /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9327 /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9328 /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9329 /// the access to the wrong runtime address (silent miscompile on the default
9330 /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9331 /// load from `[ip, #off]`. Verify the bytes.
9332 #[test]
9333 fn test_encode_arm32_indexed_load_keeps_index_206() {
9334 use synth_synthesis::{ArmOp, MemAddr, Reg};
9335 let enc = ArmEncoder::new_arm32();
9336 // ldr r0, [r11, r1, #8] must NOT collapse to a single immediate ldr.
9337 let bytes = enc
9338 .encode(&ArmOp::Ldr {
9339 rd: Reg::R0,
9340 addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9341 })
9342 .unwrap();
9343 assert_eq!(
9344 bytes.len(),
9345 8,
9346 "expected ADD ip + LDR (2 words): {bytes:02x?}"
9347 );
9348 let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9349 let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9350 // ADD ip, r11, r1 = 0xE08BC001
9351 assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9352 // LDR r0, [ip, #8] = 0xE59C0008
9353 assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9354 // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9355 assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9356 }
9357
9358 /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9359 /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9360 /// the function silently returned the leftover table-index value. The A32
9361 /// encoder must emit a real dispatch expansion, since #642 guarded by an
9362 /// inline bounds check:
9363 /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9364 /// MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9365 #[test]
9366 fn test_encode_arm32_call_indirect_is_real_call_594() {
9367 use synth_synthesis::{ArmOp, Reg};
9368 let enc = ArmEncoder::new_arm32();
9369 let bytes = enc
9370 .encode(&ArmOp::CallIndirect {
9371 rd: Reg::R0,
9372 type_idx: 0,
9373 table_index_reg: Reg::R0,
9374 table_size: 4,
9375 table_byte_offset: 0,
9376 null_check: false,
9377 type_check: None,
9378 })
9379 .unwrap();
9380 assert_eq!(
9381 bytes.len(),
9382 28,
9383 "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9384 );
9385 let words: Vec<u32> = bytes
9386 .as_chunks::<4>()
9387 .0
9388 .iter()
9389 .map(|&w| u32::from_le_bytes(w))
9390 .collect();
9391 // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9392 assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9393 assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9394 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9395 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9396 // MOV r12, r0, LSL #2 = 0xE1A0C100
9397 assert_eq!(
9398 words[4], 0xE1A0_C100,
9399 "MOV r12,r0,LSL#2: {:#010x}",
9400 words[4]
9401 );
9402 // LDR r12, [r11, r12] = 0xE79BC00C
9403 assert_eq!(
9404 words[5], 0xE79B_C00C,
9405 "LDR r12,[r11,r12]: {:#010x}",
9406 words[5]
9407 );
9408 // BLX r12 = 0xE12FFF3C
9409 assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9410 // The bug: a single NOP word. Must never come back.
9411 assert!(
9412 !bytes
9413 .as_chunks::<4>()
9414 .0
9415 .iter()
9416 .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9417 "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9418 );
9419
9420 // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9421 let bytes = enc
9422 .encode(&ArmOp::CallIndirect {
9423 rd: Reg::R0,
9424 type_idx: 0,
9425 table_index_reg: Reg::R4,
9426 table_size: 4,
9427 table_byte_offset: 0,
9428 null_check: false,
9429 type_check: None,
9430 })
9431 .unwrap();
9432 let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9433 assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9434 let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9435 assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9436 }
9437
9438 /// #642: a table size above 16 bits must not be silently truncated by the
9439 /// MOVW — the A32 guard adds a MOVT for the high half.
9440 #[test]
9441 fn test_encode_arm32_call_indirect_wide_table_size_642() {
9442 use synth_synthesis::{ArmOp, Reg};
9443 let enc = ArmEncoder::new_arm32();
9444 let bytes = enc
9445 .encode(&ArmOp::CallIndirect {
9446 rd: Reg::R0,
9447 type_idx: 0,
9448 table_index_reg: Reg::R0,
9449 table_size: 0x0002_0003,
9450 table_byte_offset: 0,
9451 null_check: false,
9452 type_check: None,
9453 })
9454 .unwrap();
9455 assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9456 let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9457 let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9458 assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9459 assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9460 }
9461
9462 /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9463 /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9464 /// [r11, ip]; blx ip`.
9465 ///
9466 /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9467 /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9468 /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9469 /// 7:6), so the index was destroyed and every call_indirect dispatched
9470 /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9471 /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9472 /// execution-validated under unicorn against the wasmtime oracle on a
9473 /// multi-entry table (indexes 0, 1, 3 —
9474 /// scripts/repro/call_indirect_597_differential.py) before this pin was
9475 /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9476 /// never come back).
9477 #[test]
9478 fn test_encode_thumb_call_indirect_lsl2_597() {
9479 use synth_synthesis::{ArmOp, Reg};
9480 let enc = ArmEncoder::new_thumb2();
9481 let bytes = enc
9482 .encode(&ArmOp::CallIndirect {
9483 rd: Reg::R0,
9484 type_idx: 0,
9485 table_index_reg: Reg::R0,
9486 table_size: 4,
9487 table_byte_offset: 0,
9488 null_check: false,
9489 type_check: None,
9490 })
9491 .unwrap();
9492 assert_eq!(
9493 bytes,
9494 vec![
9495 // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9496 0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9497 0x60, 0x45, // cmp r0, ip
9498 0x00, 0xD3, // blo .+4 (skip the udf)
9499 0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9500 // #597-pinned dispatch
9501 0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9502 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9503 0xE0, 0x47, // blx ip
9504 ],
9505 "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9506 );
9507 // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9508 assert!(
9509 !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9510 "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9511 );
9512
9513 // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9514 // and the cmp's Rn field.
9515 let bytes = enc
9516 .encode(&ArmOp::CallIndirect {
9517 rd: Reg::R0,
9518 type_idx: 0,
9519 table_index_reg: Reg::R4,
9520 table_size: 4,
9521 table_byte_offset: 0,
9522 null_check: false,
9523 type_check: None,
9524 })
9525 .unwrap();
9526 assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9527 assert_eq!(
9528 &bytes[10..14],
9529 &[0x4F, 0xEA, 0x84, 0x0C],
9530 "mov.w ip, r4, LSL #2: {bytes:02x?}"
9531 );
9532 }
9533
9534 /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9535 /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9536 /// the N bit set; a table size above 16 bits adds a MOVT.
9537 #[test]
9538 fn test_encode_thumb_call_indirect_guard_shapes_642() {
9539 use synth_synthesis::{ArmOp, Reg};
9540 let enc = ArmEncoder::new_thumb2();
9541 let bytes = enc
9542 .encode(&ArmOp::CallIndirect {
9543 rd: Reg::R0,
9544 type_idx: 0,
9545 table_index_reg: Reg::R8,
9546 table_size: 3,
9547 table_byte_offset: 0,
9548 null_check: false,
9549 type_check: None,
9550 })
9551 .unwrap();
9552 // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9553 assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9554
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 // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9567 assert_eq!(
9568 &bytes[0..8],
9569 &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9570 "movw ip,#3; movt ip,#2: {bytes:02x?}"
9571 );
9572 }
9573
9574 /// #650: a non-zero table base offset (table N of the contiguous R11
9575 /// region) routes the Thumb-2 pointer load through
9576 /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9577 /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9578 #[test]
9579 fn test_encode_thumb_call_indirect_table_offset_650() {
9580 use synth_synthesis::{ArmOp, Reg};
9581 let enc = ArmEncoder::new_thumb2();
9582 // falcon's fused-component shape: table 0 has 7 entries, so table 1
9583 // sits at byte offset 28.
9584 let bytes = enc
9585 .encode(&ArmOp::CallIndirect {
9586 rd: Reg::R0,
9587 type_idx: 0,
9588 table_index_reg: Reg::R1,
9589 table_size: 41,
9590 table_byte_offset: 28,
9591 null_check: false,
9592 type_check: None,
9593 })
9594 .unwrap();
9595 assert_eq!(
9596 bytes,
9597 vec![
9598 // #642 bounds guard against TABLE 1's OWN size (41)
9599 0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9600 0x61, 0x45, // cmp r1, ip
9601 0x00, 0xD3, // blo .+4 (skip the udf)
9602 0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9603 // dispatch through table 1's base (R11 + 28)
9604 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9605 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9606 0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9607 0xE0, 0x47, // blx ip
9608 ],
9609 "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9610 );
9611
9612 // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9613 // imm-form ldr) — single-table byte identity by construction.
9614 let zero = enc
9615 .encode(&ArmOp::CallIndirect {
9616 rd: Reg::R0,
9617 type_idx: 0,
9618 table_index_reg: Reg::R1,
9619 table_size: 41,
9620 table_byte_offset: 0,
9621 null_check: false,
9622 type_check: None,
9623 })
9624 .unwrap();
9625 assert_eq!(
9626 &zero[10..],
9627 &[
9628 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9629 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9630 0xE0, 0x47, // blx ip
9631 ],
9632 "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9633 );
9634 }
9635
9636 /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9637 /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9638 #[test]
9639 fn test_encode_arm32_call_indirect_table_offset_650() {
9640 use synth_synthesis::{ArmOp, Reg};
9641 let enc = ArmEncoder::new_arm32();
9642 let bytes = enc
9643 .encode(&ArmOp::CallIndirect {
9644 rd: Reg::R0,
9645 type_idx: 0,
9646 table_index_reg: Reg::R1,
9647 table_size: 41,
9648 table_byte_offset: 28,
9649 null_check: false,
9650 type_check: None,
9651 })
9652 .unwrap();
9653 let words: Vec<u32> = bytes
9654 .as_chunks::<4>()
9655 .0
9656 .iter()
9657 .map(|&w| u32::from_le_bytes(w))
9658 .collect();
9659 assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9660 assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9661 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9662 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9663 assert_eq!(
9664 words[4], 0xE1A0_C101,
9665 "MOV r12,r1,LSL#2: {:#010x}",
9666 words[4]
9667 );
9668 assert_eq!(
9669 words[5], 0xE08B_C00C,
9670 "ADD r12,r11,r12 (#650): {:#010x}",
9671 words[5]
9672 );
9673 assert_eq!(
9674 words[6], 0xE59C_C01C,
9675 "LDR r12,[r12,#28] (#650): {:#010x}",
9676 words[6]
9677 );
9678 assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9679 }
9680
9681 /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9682 /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9683 /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9684 /// branch to address 0. `null_check: false` keeps the expansion
9685 /// byte-identical to the pre-#664 form (by-construction pin).
9686 #[test]
9687 fn test_encode_thumb_call_indirect_null_check_664() {
9688 use synth_synthesis::{ArmOp, Reg};
9689 let enc = ArmEncoder::new_thumb2();
9690 let op = |null_check| ArmOp::CallIndirect {
9691 rd: Reg::R0,
9692 type_idx: 0,
9693 table_index_reg: Reg::R1,
9694 table_size: 4,
9695 table_byte_offset: 0,
9696 null_check,
9697 type_check: None,
9698 };
9699 let with = enc.encode(&op(true)).unwrap();
9700 let without = enc.encode(&op(false)).unwrap();
9701 // The checked form = the unchecked form with EXACTLY the three-insn
9702 // null check spliced in before the final BLX (byte identity of the
9703 // shared prefix/suffix — nothing else may move).
9704 assert_eq!(
9705 with.len(),
9706 without.len() + 8,
9707 "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9708 );
9709 let blx_at = without.len() - 2;
9710 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9711 assert_eq!(
9712 &with[blx_at..],
9713 &[
9714 0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9715 0x00, 0xD1, // bne .+4 (skip the udf)
9716 0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9717 0xE0, 0x47, // blx ip
9718 ],
9719 "null check precedes the BLX: {with:02x?}"
9720 );
9721 assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9722 }
9723
9724 /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9725 /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9726 #[test]
9727 fn test_encode_arm32_call_indirect_null_check_664() {
9728 use synth_synthesis::{ArmOp, Reg};
9729 let enc = ArmEncoder::new_arm32();
9730 let op = |null_check| ArmOp::CallIndirect {
9731 rd: Reg::R0,
9732 type_idx: 0,
9733 table_index_reg: Reg::R1,
9734 table_size: 4,
9735 table_byte_offset: 0,
9736 null_check,
9737 type_check: None,
9738 };
9739 let with = enc.encode(&op(true)).unwrap();
9740 let without = enc.encode(&op(false)).unwrap();
9741 assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9742 let blx_at = without.len() - 4;
9743 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9744 let words: Vec<u32> = with[blx_at..]
9745 .as_chunks::<4>()
9746 .0
9747 .iter()
9748 .map(|&w| u32::from_le_bytes(w))
9749 .collect();
9750 assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9751 assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9752 assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9753 assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9754 }
9755
9756 /// #676: `type_check` splices the runtime type check — scale the index,
9757 /// load the slot's structural class id from the type-id sidecar
9758 /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9759 /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9760 /// the dispatch tail. `type_check: None` keeps the expansion
9761 /// byte-identical to the pre-#676 form (by-construction pin, the same
9762 /// trick as #650 offset-0 / #664 `null_check: false`).
9763 #[test]
9764 fn test_encode_thumb_call_indirect_type_check_676() {
9765 use synth_synthesis::{ArmOp, Reg};
9766 let enc = ArmEncoder::new_thumb2();
9767 let op = |type_check| ArmOp::CallIndirect {
9768 rd: Reg::R0,
9769 type_idx: 1,
9770 table_index_reg: Reg::R1,
9771 table_size: 5,
9772 table_byte_offset: 0,
9773 null_check: false,
9774 type_check,
9775 };
9776 let with = enc.encode(&op(Some((2, 20)))).unwrap();
9777 let without = enc.encode(&op(None)).unwrap();
9778 // The checked form = the unchecked form with EXACTLY the six-insn
9779 // type check spliced in after the bounds guard (byte identity of
9780 // the shared prefix/suffix — nothing else may move).
9781 assert_eq!(
9782 with.len(),
9783 without.len() + 20,
9784 "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9785 );
9786 // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9787 let guard_end = 10;
9788 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9789 assert_eq!(
9790 &with[guard_end..guard_end + 20],
9791 &[
9792 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9793 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9794 0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9795 0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9796 0x00, 0xD0, // beq .+4 (skip the udf on a match)
9797 0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9798 ],
9799 "type check follows the bounds guard: {with:02x?}"
9800 );
9801 assert_eq!(
9802 &with[guard_end + 20..],
9803 &without[guard_end..],
9804 "dispatch tail unchanged (idx*4 recomputed)"
9805 );
9806 }
9807
9808 /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9809 /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9810 /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9811 /// bytes identical.
9812 #[test]
9813 fn test_encode_arm32_call_indirect_type_check_676() {
9814 use synth_synthesis::{ArmOp, Reg};
9815 let enc = ArmEncoder::new_arm32();
9816 let op = |type_check| ArmOp::CallIndirect {
9817 rd: Reg::R0,
9818 type_idx: 1,
9819 table_index_reg: Reg::R1,
9820 table_size: 5,
9821 table_byte_offset: 0,
9822 null_check: false,
9823 type_check,
9824 };
9825 let with = enc.encode(&op(Some((2, 20)))).unwrap();
9826 let without = enc.encode(&op(None)).unwrap();
9827 assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
9828 // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
9829 let guard_end = 16;
9830 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9831 let words: Vec<u32> = with[guard_end..guard_end + 24]
9832 .as_chunks::<4>()
9833 .0
9834 .iter()
9835 .map(|&w| u32::from_le_bytes(w))
9836 .collect();
9837 assert_eq!(
9838 words[0], 0xE1A0_C101,
9839 "MOV r12,r1,LSL#2: {:#010x}",
9840 words[0]
9841 );
9842 assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
9843 assert_eq!(
9844 words[2], 0xE59C_C014,
9845 "LDR r12,[r12,#20] (sidecar): {:#010x}",
9846 words[2]
9847 );
9848 assert_eq!(
9849 words[3], 0xE35C_0002,
9850 "CMP r12,#2 (expected class id): {:#010x}",
9851 words[3]
9852 );
9853 assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
9854 assert_eq!(
9855 words[5], 0xE7F0_00F0,
9856 "UDF (type-mismatch trap): {:#010x}",
9857 words[5]
9858 );
9859 assert_eq!(
9860 &with[guard_end + 24..],
9861 &without[guard_end..],
9862 "dispatch tail unchanged"
9863 );
9864 }
9865
9866 /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9867 /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9868 /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9869 /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9870 /// dropping the address operand and miscompiling every optimized memory
9871 /// access. High registers must use the 32-bit `.W` forms.
9872 #[test]
9873 fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9874 let encoder = ArmEncoder::new_thumb2();
9875
9876 // add ip, ip, r0 — the exact MemLoad/MemStore base+addr op.
9877 let code = encoder
9878 .encode(&ArmOp::Add {
9879 rd: Reg::R12,
9880 rn: Reg::R12,
9881 op2: Operand2::Reg(Reg::R0),
9882 })
9883 .unwrap();
9884 // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9885 assert_eq!(
9886 code,
9887 vec![0x0C, 0xEB, 0x00, 0x0C],
9888 "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
9889 );
9890 // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
9891 assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
9892
9893 // Low-register add stays 16-bit (no regression for the common case).
9894 let lo = encoder
9895 .encode(&ArmOp::Add {
9896 rd: Reg::R1,
9897 rn: Reg::R2,
9898 op2: Operand2::Reg(Reg::R3),
9899 })
9900 .unwrap();
9901 assert_eq!(
9902 lo.len(),
9903 2,
9904 "low-reg ADD should remain 16-bit, got {lo:02X?}"
9905 );
9906 }
9907
9908 /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
9909 /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
9910 #[test]
9911 fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
9912 let encoder = ArmEncoder::new_thumb2();
9913
9914 // adds r10, r10, r8 → ADDS.W = EB1A 0A08
9915 let adds = encoder
9916 .encode(&ArmOp::Adds {
9917 rd: Reg::R10,
9918 rn: Reg::R10,
9919 op2: Operand2::Reg(Reg::R8),
9920 })
9921 .unwrap();
9922 assert_eq!(
9923 adds,
9924 vec![0x1A, 0xEB, 0x08, 0x0A],
9925 "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
9926 );
9927
9928 // subs r10, r10, r8 → SUBS.W = EBBA 0A08
9929 let subs = encoder
9930 .encode(&ArmOp::Subs {
9931 rd: Reg::R10,
9932 rn: Reg::R10,
9933 op2: Operand2::Reg(Reg::R8),
9934 })
9935 .unwrap();
9936 assert_eq!(
9937 subs,
9938 vec![0xBA, 0xEB, 0x08, 0x0A],
9939 "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
9940 );
9941 }
9942
9943 /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
9944 /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
9945 #[test]
9946 fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
9947 let encoder = ArmEncoder::new_thumb2();
9948
9949 // cmn r10, r8 → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
9950 let cmn = encoder
9951 .encode(&ArmOp::Cmn {
9952 rn: Reg::R10,
9953 op2: Operand2::Reg(Reg::R8),
9954 })
9955 .unwrap();
9956 assert_eq!(
9957 cmn,
9958 vec![0x1A, 0xEB, 0x08, 0x0F],
9959 "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
9960 );
9961
9962 // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
9963 let lo = encoder
9964 .encode(&ArmOp::Cmn {
9965 rn: Reg::R1,
9966 op2: Operand2::Reg(Reg::R2),
9967 })
9968 .unwrap();
9969 assert_eq!(
9970 lo.len(),
9971 2,
9972 "low-reg CMN should remain 16-bit, got {lo:02X?}"
9973 );
9974 assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
9975 }
9976
9977 /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
9978 /// guards its registers must return Err, not panic under debug-assertions.
9979 /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
9980 #[test]
9981 fn test_encode_pc_operand_returns_err_not_panic_185() {
9982 let encoder = ArmEncoder::new_thumb2();
9983 for op in [
9984 ArmOp::Sdiv {
9985 rd: Reg::PC,
9986 rn: Reg::R0,
9987 rm: Reg::R1,
9988 },
9989 ArmOp::Udiv {
9990 rd: Reg::R0,
9991 rn: Reg::PC,
9992 rm: Reg::R1,
9993 },
9994 ArmOp::Sdiv {
9995 rd: Reg::R0,
9996 rn: Reg::R1,
9997 rm: Reg::PC,
9998 },
9999 ] {
10000 let r = encoder.encode(&op);
10001 assert!(
10002 r.is_err(),
10003 "encode({op:?}) must return Err for a PC operand, got {r:?}"
10004 );
10005 }
10006 // Valid registers still encode fine (no false rejection).
10007 assert!(
10008 encoder
10009 .encode(&ArmOp::Sdiv {
10010 rd: Reg::R0,
10011 rn: Reg::R1,
10012 rm: Reg::R2
10013 })
10014 .is_ok()
10015 );
10016 }
10017
10018 #[test]
10019 fn test_encode_nop_arm32() {
10020 let encoder = ArmEncoder::new_arm32();
10021 let code = encoder.encode(&ArmOp::Nop).unwrap();
10022
10023 assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
10024 assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
10025 }
10026
10027 #[test]
10028 fn test_encode_nop_thumb() {
10029 let encoder = ArmEncoder::new_thumb2();
10030 let code = encoder.encode(&ArmOp::Nop).unwrap();
10031
10032 assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10033 assert_eq!(code, vec![0x00, 0xBF]); // NOP
10034 }
10035
10036 #[test]
10037 fn test_encode_mov_immediate_arm32() {
10038 let encoder = ArmEncoder::new_arm32();
10039 let op = ArmOp::Mov {
10040 rd: Reg::R0,
10041 op2: Operand2::Imm(42),
10042 };
10043
10044 let code = encoder.encode(&op).unwrap();
10045 assert_eq!(code.len(), 4);
10046
10047 // Verify it's a MOV instruction (bits should have immediate flag set)
10048 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10049 assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10050 }
10051
10052 #[test]
10053 fn test_encode_add_registers_arm32() {
10054 let encoder = ArmEncoder::new_arm32();
10055 let op = ArmOp::Add {
10056 rd: Reg::R0,
10057 rn: Reg::R1,
10058 op2: Operand2::Reg(Reg::R2),
10059 };
10060
10061 let code = encoder.encode(&op).unwrap();
10062 assert_eq!(code.len(), 4);
10063
10064 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10065 // Verify it's an ADD instruction with correct opcode
10066 assert_eq!(instr & 0x0FE00000, 0x00800000);
10067 }
10068
10069 /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10070 /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10071 /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10072 #[test]
10073 fn test_encode_add_imm_large_350() {
10074 let enc = ArmEncoder::new_thumb2();
10075
10076 // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10077 // VALUE must be right (#681: this test used to assert only the length,
10078 // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10079 // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10080 // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10081 let small = enc
10082 .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10083 .unwrap();
10084 assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10085
10086 // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10087 fn movx_imm16(b: &[u8]) -> u32 {
10088 let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10089 let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10090 let imm4 = hw1 & 0xF;
10091 let i = (hw1 >> 10) & 1;
10092 let imm3 = (hw2 >> 12) & 0x7;
10093 let imm8 = hw2 & 0xFF;
10094 (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10095 }
10096 fn movx_rd(b: &[u8]) -> u32 {
10097 (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10098 }
10099
10100 // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10101 // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10102 let seq = enc
10103 .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10104 .unwrap();
10105 assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10106 // MOVW r12, #0x1170
10107 assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10108 assert_eq!(movx_rd(&seq[0..4]), 12);
10109 assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10110 // MOVT r12, #0x0001
10111 assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10112 assert_eq!(movx_rd(&seq[4..8]), 12);
10113 assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10114 // ADD.W r12, r0, r12 (EB00 | rn=0 ; rd=12, rm=12)
10115 let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10116 let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10117 assert_eq!(add1 & 0xFFF0, 0xEB00);
10118 assert_eq!(add1 & 0xF, 0); // rn = r0
10119 assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10120 assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10121 // The materialized scratch must reconstruct exactly 70000.
10122 assert_eq!(
10123 (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10124 70000
10125 );
10126
10127 // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10128 let seq16 = enc
10129 .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10130 .unwrap();
10131 assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10132 assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10133 assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10134
10135 // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10136 // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10137 let inplace = enc
10138 .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10139 .unwrap();
10140 assert_eq!(inplace.len(), 12);
10141 assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10142 assert_eq!(
10143 (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10144 0x12345
10145 );
10146 // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10147 let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10148 assert_eq!(ip_add2 & 0xF, 12);
10149 assert_eq!((ip_add2 >> 8) & 0xF, 5);
10150 }
10151
10152 /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10153 /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10154 /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10155 /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10156 /// computed a wrong address (and bypassed --safety-bounds software: the
10157 /// guard checked the intended address, the access used the mis-encoded
10158 /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10159 /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10160 /// `encode_thumb32_add` already uses per #253.
10161 ///
10162 /// Every expected byte sequence below is pinned against clang
10163 /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10164 #[test]
10165 fn test_encode_add_imm_thumb_expand_681() {
10166 let enc = ArmEncoder::new_thumb2();
10167 let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10168
10169 // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10170 // clang: add.w r12, r0, #0xff = f100 0cff
10171 assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10172
10173 // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10174 // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10175 // clang: addw r12, r0, #0x100 = f200 1c00
10176 assert_eq!(
10177 add(&Reg::R12, &Reg::R0, 0x100),
10178 vec![0x00, 0xF2, 0x00, 0x1C]
10179 );
10180 // clang: addw r12, r0, #0x104 = f200 1c04
10181 assert_eq!(
10182 add(&Reg::R12, &Reg::R0, 0x104),
10183 vec![0x00, 0xF2, 0x04, 0x1C]
10184 );
10185 // clang: addw r12, r0, #0x200 = f200 2c00
10186 assert_eq!(
10187 add(&Reg::R12, &Reg::R0, 0x200),
10188 vec![0x00, 0xF2, 0x00, 0x2C]
10189 );
10190 // clang: addw r12, r0, #0x3fc = f200 3cfc
10191 assert_eq!(
10192 add(&Reg::R12, &Reg::R0, 0x3FC),
10193 vec![0x00, 0xF2, 0xFC, 0x3C]
10194 );
10195 // clang: addw r12, r0, #0x400 = f200 4c00
10196 assert_eq!(
10197 add(&Reg::R12, &Reg::R0, 0x400),
10198 vec![0x00, 0xF2, 0x00, 0x4C]
10199 );
10200 // clang: addw r12, r0, #0xfff = f600 7cff
10201 assert_eq!(
10202 add(&Reg::R12, &Reg::R0, 0xFFF),
10203 vec![0x00, 0xF6, 0xFF, 0x7C]
10204 );
10205 // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10206 assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10207 }
10208
10209 /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10210 /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10211 /// (T4-style) form, so a non-representable immediate must Err loudly
10212 /// (#253/#255/#378 class: never silently encode a different constant).
10213 /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10214 /// pinned here bit-for-bit against clang.
10215 #[test]
10216 fn test_rsb_and_imm_thumb_expand_gate_681() {
10217 let enc = ArmEncoder::new_thumb2();
10218
10219 // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10220 let rsb = enc
10221 .encode(&ArmOp::Rsb {
10222 rd: Reg::R3,
10223 rn: Reg::R2,
10224 imm: 32,
10225 })
10226 .unwrap();
10227 assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10228
10229 // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10230 assert!(
10231 enc.encode(&ArmOp::Rsb {
10232 rd: Reg::R3,
10233 rn: Reg::R2,
10234 imm: 0x101,
10235 })
10236 .is_err(),
10237 "non-ThumbExpandImm RSB immediate must Err"
10238 );
10239
10240 // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10241 let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10242 assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10243 assert!(
10244 enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10245 "non-ThumbExpandImm AND immediate must Err"
10246 );
10247
10248 // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10249 // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10250 let a32 = ArmEncoder::new_arm32();
10251 assert!(
10252 a32.encode(&ArmOp::Rsb {
10253 rd: Reg::R3,
10254 rn: Reg::R2,
10255 imm: 0x120,
10256 })
10257 .is_err(),
10258 "A32 RSB immediate > 0xFF must Err, not mask"
10259 );
10260 // imm 32 (the only value real codegen emits) still encodes.
10261 assert!(
10262 a32.encode(&ArmOp::Rsb {
10263 rd: Reg::R3,
10264 rn: Reg::R2,
10265 imm: 32,
10266 })
10267 .is_ok()
10268 );
10269 }
10270
10271 /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10272 /// with ARBITRARY registers, including the one case the in-place lowering
10273 /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10274 /// register) would alias Rn and clobber it before the ADD reads it. The
10275 /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10276 /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10277 /// is non-allocatable; this guards only the fuzz/adversarial path.)
10278 #[test]
10279 fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10280 let enc = ArmEncoder::new_thumb2();
10281 // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10282 let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10283 assert!(
10284 r.is_err(),
10285 "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10286 );
10287 // Small imm with rd==rn==R12 still takes the single-instruction fast path
10288 // (no scratch needed) and must succeed — the guard is scoped to the
10289 // out-of-range lowering only.
10290 let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10291 assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10292 }
10293
10294 /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10295 /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10296 /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10297 /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10298 /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10299 /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10300 /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10301 /// harness — which drives arbitrary operands — still passes.
10302 #[test]
10303 fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10304 let enc = ArmEncoder::new_arm32();
10305 let bad = enc.encode(&ArmOp::Add {
10306 rd: Reg::R0,
10307 rn: Reg::R1,
10308 op2: Operand2::Imm(0x1FF),
10309 });
10310 assert!(
10311 bad.is_err(),
10312 "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10313 to 0xFF), got {bad:?}"
10314 );
10315 // A representable rotated immediate still encodes fine (regression guard).
10316 let ok = enc.encode(&ArmOp::Add {
10317 rd: Reg::R0,
10318 rn: Reg::R1,
10319 op2: Operand2::Imm(0xFF),
10320 });
10321 assert!(
10322 ok.is_ok(),
10323 "0xFF is a valid rotated immediate, must stay Ok"
10324 );
10325 }
10326
10327 #[test]
10328 fn test_encode_ldr_arm32() {
10329 let encoder = ArmEncoder::new_arm32();
10330 let op = ArmOp::Ldr {
10331 rd: Reg::R0,
10332 addr: MemAddr::imm(Reg::R1, 4),
10333 };
10334
10335 let code = encoder.encode(&op).unwrap();
10336 assert_eq!(code.len(), 4);
10337
10338 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10339 // Verify load bit is set
10340 assert_eq!(instr & 0x00100000, 0x00100000);
10341 }
10342
10343 #[test]
10344 fn test_encode_str_arm32() {
10345 let encoder = ArmEncoder::new_arm32();
10346 let op = ArmOp::Str {
10347 rd: Reg::R0,
10348 addr: MemAddr::imm(Reg::SP, 0),
10349 };
10350
10351 let code = encoder.encode(&op).unwrap();
10352 assert_eq!(code.len(), 4);
10353 }
10354
10355 #[test]
10356 fn test_encode_branch_arm32() {
10357 let encoder = ArmEncoder::new_arm32();
10358 let op = ArmOp::Bl {
10359 label: "main".to_string(),
10360 };
10361
10362 let code = encoder.encode(&op).unwrap();
10363 assert_eq!(code.len(), 4);
10364
10365 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10366 // Verify BL opcode
10367 assert_eq!(instr & 0x0F000000, 0x0B000000);
10368 }
10369
10370 /// #1040: the A32 BL relocatable placeholder must carry an embedded addend
10371 /// of -8 so an R_ARM_CALL nets to exactly the symbol S — the A32 twin of
10372 /// the Thumb #167/#174 test above. A32 `BL` computes
10373 /// `target = P + 8 + (imm24 << 2)`, so:
10374 /// - `eb000000` (imm24 = 0, addend 0) lands at S+8, two instructions
10375 /// past the callee entry. This is what synth emitted before #1040, and
10376 /// combined with the mislabelled R_ARM_THM_CALL a real linker
10377 /// (`arm-none-eabi-ld`) corrupted the word to `eaca0000` — opcode
10378 /// `eb` (BL) flipped to `ea` (B), so LR was never set at all.
10379 /// - `ebfffffe` (imm24 = -2, offset -8) is `bl <self>` and nets to S.
10380 /// This is exactly what `arm-none-eabi-as -march=armv7-r` emits for
10381 /// `bl <extern>`, verified directly rather than derived from the ABI.
10382 #[test]
10383 fn test_encode_arm32_bl_placeholder_addend_1040() {
10384 let encoder = ArmEncoder::new_arm32();
10385 let code = encoder
10386 .encode(&ArmOp::Bl {
10387 label: "callee".to_string(),
10388 })
10389 .unwrap();
10390 assert_eq!(code.len(), 4, "A32 BL is one 32-bit word");
10391 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10392 assert_eq!(
10393 instr, 0xEBFF_FFFE,
10394 "A32 BL placeholder must be `ebfffffe` (gas's `bl <extern>`), not `eb000000` — a 0 addend resolves two instructions past the callee entry (#1040)"
10395 );
10396 // Spell the addend out so a future edit cannot satisfy the literal by
10397 // accident: sign-extended imm24, word-scaled, plus the +8 pipeline
10398 // bias, must be exactly 0 (branch-to-self).
10399 let imm24 = instr & 0x00FF_FFFF;
10400 let signed = ((imm24 as i32) << 8) >> 8;
10401 assert_eq!(
10402 8 + (signed << 2),
10403 0,
10404 "placeholder must branch to itself so the REL addend is -8"
10405 );
10406 }
10407
10408 /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10409 /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10410 /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10411 /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10412 /// - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10413 /// to fit (#167).
10414 /// - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10415 /// entry (#174).
10416 /// - 0xFFFE (addend -4) → lands at S. Correct.
10417 #[test]
10418 fn test_encode_thumb_bl_placeholder_addend_167_174() {
10419 let encoder = ArmEncoder::new_thumb2();
10420 let op = ArmOp::Bl {
10421 label: "callee".to_string(),
10422 };
10423
10424 let code = encoder.encode(&op).unwrap();
10425 assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10426
10427 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10428 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10429 assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10430 assert_eq!(
10431 hw2, 0xFFFE,
10432 "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10433 );
10434 assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10435 assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10436 }
10437
10438 /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10439 /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10440 /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10441 /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10442 /// displacement: gust_poll's loop-head `br_if` to an outer block end
10443 /// landed mid-shape (a spurious state write + spurious calls on the
10444 /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10445 /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10446 /// disassembler (`bne.w #0x224` = f040 8112).
10447 #[test]
10448 fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10449 use synth_synthesis::Condition;
10450 let encoder = ArmEncoder::new_thumb2();
10451
10452 // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10453 let code = encoder
10454 .encode(&ArmOp::BCondOffset {
10455 cond: Condition::NE,
10456 offset: 0x112,
10457 })
10458 .unwrap();
10459 assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10460 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10461 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10462 assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10463 assert_eq!(
10464 hw2, 0x8112,
10465 "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10466 0x8089 (offset>>1) is the halved #740 miscompile"
10467 );
10468
10469 // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10470 // imm6=0b111111, imm11=0x700 → f43f af00.
10471 let code = encoder
10472 .encode(&ArmOp::BCondOffset {
10473 cond: Condition::EQ,
10474 offset: -0x100,
10475 })
10476 .unwrap();
10477 assert_eq!(code.len(), 4);
10478 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10479 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10480 assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10481 assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10482
10483 // Narrow encoding stays byte-identical (in-range offsets untouched).
10484 let code = encoder
10485 .encode(&ArmOp::BCondOffset {
10486 cond: Condition::EQ,
10487 offset: 5,
10488 })
10489 .unwrap();
10490 assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10491
10492 // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10493 assert!(
10494 encoder
10495 .encode(&ArmOp::BCondOffset {
10496 cond: Condition::NE,
10497 offset: 1 << 19,
10498 })
10499 .is_err(),
10500 "out-of-range T3 offset must be a loud decline"
10501 );
10502 }
10503
10504 #[test]
10505 fn test_encode_sequence() {
10506 let encoder = ArmEncoder::new_arm32();
10507 let ops = vec![
10508 ArmOp::Mov {
10509 rd: Reg::R0,
10510 op2: Operand2::Imm(42),
10511 },
10512 ArmOp::Mov {
10513 rd: Reg::R1,
10514 op2: Operand2::Imm(10),
10515 },
10516 ArmOp::Add {
10517 rd: Reg::R2,
10518 rn: Reg::R0,
10519 op2: Operand2::Reg(Reg::R1),
10520 },
10521 ];
10522
10523 let code = encoder.encode_sequence(&ops).unwrap();
10524 assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10525 }
10526
10527 #[test]
10528 fn test_reg_to_bits() {
10529 assert_eq!(reg_to_bits(&Reg::R0), 0);
10530 assert_eq!(reg_to_bits(&Reg::R7), 7);
10531 assert_eq!(reg_to_bits(&Reg::SP), 13);
10532 assert_eq!(reg_to_bits(&Reg::LR), 14);
10533 assert_eq!(reg_to_bits(&Reg::PC), 15);
10534 }
10535
10536 #[test]
10537 fn test_encode_bitwise_operations() {
10538 let encoder = ArmEncoder::new_arm32();
10539
10540 let and_op = ArmOp::And {
10541 rd: Reg::R0,
10542 rn: Reg::R1,
10543 op2: Operand2::Reg(Reg::R2),
10544 };
10545 let and_code = encoder.encode(&and_op).unwrap();
10546 assert_eq!(and_code.len(), 4);
10547
10548 let orr_op = ArmOp::Orr {
10549 rd: Reg::R0,
10550 rn: Reg::R1,
10551 op2: Operand2::Reg(Reg::R2),
10552 };
10553 let orr_code = encoder.encode(&orr_op).unwrap();
10554 assert_eq!(orr_code.len(), 4);
10555
10556 let eor_op = ArmOp::Eor {
10557 rd: Reg::R0,
10558 rn: Reg::R1,
10559 op2: Operand2::Reg(Reg::R2),
10560 };
10561 let eor_code = encoder.encode(&eor_op).unwrap();
10562 assert_eq!(eor_code.len(), 4);
10563 }
10564
10565 // === Thumb-2 32-bit encoding tests ===
10566
10567 #[test]
10568 fn test_encode_sdiv_thumb2() {
10569 let encoder = ArmEncoder::new_thumb2();
10570 let op = ArmOp::Sdiv {
10571 rd: Reg::R0,
10572 rn: Reg::R1,
10573 rm: Reg::R2,
10574 };
10575
10576 let code = encoder.encode(&op).unwrap();
10577 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10578
10579 // SDIV R0, R1, R2: 0xFB91 0xF0F2
10580 // First halfword: 0xFB90 | Rn(1) = 0xFB91
10581 // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10582 // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10583 assert_eq!(code[0], 0x91);
10584 assert_eq!(code[1], 0xFB);
10585 assert_eq!(code[2], 0xF2);
10586 assert_eq!(code[3], 0xF0);
10587 }
10588
10589 #[test]
10590 fn test_encode_udiv_thumb2() {
10591 let encoder = ArmEncoder::new_thumb2();
10592 let op = ArmOp::Udiv {
10593 rd: Reg::R0,
10594 rn: Reg::R1,
10595 rm: Reg::R2,
10596 };
10597
10598 let code = encoder.encode(&op).unwrap();
10599 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10600
10601 // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10602 // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10603 assert_eq!(code[0], 0xB1);
10604 assert_eq!(code[1], 0xFB);
10605 assert_eq!(code[2], 0xF2);
10606 assert_eq!(code[3], 0xF0);
10607 }
10608
10609 #[test]
10610 fn test_encode_mul_thumb2() {
10611 let encoder = ArmEncoder::new_thumb2();
10612 let op = ArmOp::Mul {
10613 rd: Reg::R0,
10614 rn: Reg::R1,
10615 rm: Reg::R2,
10616 };
10617
10618 let code = encoder.encode(&op).unwrap();
10619 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10620 }
10621
10622 #[test]
10623 fn test_encode_and_thumb2() {
10624 let encoder = ArmEncoder::new_thumb2();
10625 let op = ArmOp::And {
10626 rd: Reg::R0,
10627 rn: Reg::R1,
10628 op2: Operand2::Reg(Reg::R2),
10629 };
10630
10631 let code = encoder.encode(&op).unwrap();
10632 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10633 }
10634
10635 #[test]
10636 fn test_encode_lsl_thumb2_low_regs() {
10637 let encoder = ArmEncoder::new_thumb2();
10638 let op = ArmOp::Lsl {
10639 rd: Reg::R0,
10640 rn: Reg::R1,
10641 shift: 5,
10642 };
10643
10644 let code = encoder.encode(&op).unwrap();
10645 assert_eq!(code.len(), 2); // 16-bit for low registers
10646 }
10647
10648 #[test]
10649 fn test_encode_clz_thumb2() {
10650 let encoder = ArmEncoder::new_thumb2();
10651 let op = ArmOp::Clz {
10652 rd: Reg::R0,
10653 rm: Reg::R1,
10654 };
10655
10656 let code = encoder.encode(&op).unwrap();
10657 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10658 }
10659
10660 #[test]
10661 fn test_encode_bx_thumb2() {
10662 let encoder = ArmEncoder::new_thumb2();
10663 let op = ArmOp::Bx { rm: Reg::LR };
10664
10665 let code = encoder.encode(&op).unwrap();
10666 assert_eq!(code.len(), 2); // 16-bit instruction
10667
10668 // BX LR: 0x4770
10669 assert_eq!(code, vec![0x70, 0x47]);
10670 }
10671
10672 // ========================================================================
10673 // f32 pseudo-op encoding tests
10674 // ========================================================================
10675
10676 #[test]
10677 fn test_encode_f32_abs_arm32() {
10678 let encoder = ArmEncoder::new_arm32();
10679 let op = ArmOp::F32Abs {
10680 sd: VfpReg::S0,
10681 sm: VfpReg::S2,
10682 };
10683 let code = encoder.encode(&op).unwrap();
10684 assert_eq!(code.len(), 4); // Single VFP instruction
10685 }
10686
10687 #[test]
10688 fn test_encode_f32_neg_arm32() {
10689 let encoder = ArmEncoder::new_arm32();
10690 let op = ArmOp::F32Neg {
10691 sd: VfpReg::S0,
10692 sm: VfpReg::S2,
10693 };
10694 let code = encoder.encode(&op).unwrap();
10695 assert_eq!(code.len(), 4);
10696 }
10697
10698 #[test]
10699 fn test_encode_f32_sqrt_arm32() {
10700 let encoder = ArmEncoder::new_arm32();
10701 let op = ArmOp::F32Sqrt {
10702 sd: VfpReg::S0,
10703 sm: VfpReg::S2,
10704 };
10705 let code = encoder.encode(&op).unwrap();
10706 assert_eq!(code.len(), 4);
10707 }
10708
10709 #[test]
10710 fn test_encode_f32_ceil_arm32() {
10711 let encoder = ArmEncoder::new_arm32();
10712 let op = ArmOp::F32Ceil {
10713 sd: VfpReg::S0,
10714 sm: VfpReg::S2,
10715 };
10716 let code = encoder.encode(&op).unwrap();
10717 // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10718 assert_eq!(code.len(), 36);
10719 }
10720
10721 #[test]
10722 fn test_encode_f32_floor_thumb2() {
10723 let encoder = ArmEncoder::new_thumb2();
10724 let op = ArmOp::F32Floor {
10725 sd: VfpReg::S0,
10726 sm: VfpReg::S2,
10727 };
10728 let code = encoder.encode(&op).unwrap();
10729 // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10730 assert_eq!(code.len(), 36);
10731 }
10732
10733 #[test]
10734 fn test_encode_f32_min_arm32() {
10735 let encoder = ArmEncoder::new_arm32();
10736 let op = ArmOp::F32Min {
10737 sd: VfpReg::S0,
10738 sn: VfpReg::S2,
10739 sm: VfpReg::S4,
10740 };
10741 let code = encoder.encode(&op).unwrap();
10742 assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10743 }
10744
10745 #[test]
10746 fn test_encode_f32_max_thumb2() {
10747 let encoder = ArmEncoder::new_thumb2();
10748 let op = ArmOp::F32Max {
10749 sd: VfpReg::S0,
10750 sn: VfpReg::S2,
10751 sm: VfpReg::S4,
10752 };
10753 let code = encoder.encode(&op).unwrap();
10754 // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10755 assert_eq!(code.len(), 18);
10756 }
10757
10758 #[test]
10759 fn test_encode_f32_copysign_arm32() {
10760 let encoder = ArmEncoder::new_arm32();
10761 let op = ArmOp::F32Copysign {
10762 sd: VfpReg::S0,
10763 sn: VfpReg::S2,
10764 sm: VfpReg::S4,
10765 };
10766 let code = encoder.encode(&op).unwrap();
10767 // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10768 assert_eq!(code.len(), 24);
10769 }
10770
10771 // ========================================================================
10772 // f64 encoding tests
10773 // ========================================================================
10774
10775 #[test]
10776 fn test_encode_f64_add_arm32() {
10777 let encoder = ArmEncoder::new_arm32();
10778 let op = ArmOp::F64Add {
10779 dd: VfpReg::D0,
10780 dn: VfpReg::D1,
10781 dm: VfpReg::D2,
10782 };
10783 let code = encoder.encode(&op).unwrap();
10784 assert_eq!(code.len(), 4);
10785 // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10786 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10787 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10788 }
10789
10790 #[test]
10791 fn test_encode_f64_sub_thumb2() {
10792 let encoder = ArmEncoder::new_thumb2();
10793 let op = ArmOp::F64Sub {
10794 dd: VfpReg::D0,
10795 dn: VfpReg::D1,
10796 dm: VfpReg::D2,
10797 };
10798 let code = encoder.encode(&op).unwrap();
10799 assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10800 }
10801
10802 #[test]
10803 fn test_encode_f64_mul_arm32() {
10804 let encoder = ArmEncoder::new_arm32();
10805 let op = ArmOp::F64Mul {
10806 dd: VfpReg::D0,
10807 dn: VfpReg::D1,
10808 dm: VfpReg::D2,
10809 };
10810 let code = encoder.encode(&op).unwrap();
10811 assert_eq!(code.len(), 4);
10812 }
10813
10814 #[test]
10815 fn test_encode_f64_div_arm32() {
10816 let encoder = ArmEncoder::new_arm32();
10817 let op = ArmOp::F64Div {
10818 dd: VfpReg::D0,
10819 dn: VfpReg::D1,
10820 dm: VfpReg::D2,
10821 };
10822 let code = encoder.encode(&op).unwrap();
10823 assert_eq!(code.len(), 4);
10824 }
10825
10826 #[test]
10827 fn test_encode_f64_abs_arm32() {
10828 let encoder = ArmEncoder::new_arm32();
10829 let op = ArmOp::F64Abs {
10830 dd: VfpReg::D0,
10831 dm: VfpReg::D2,
10832 };
10833 let code = encoder.encode(&op).unwrap();
10834 assert_eq!(code.len(), 4);
10835 }
10836
10837 #[test]
10838 fn test_encode_f64_neg_arm32() {
10839 let encoder = ArmEncoder::new_arm32();
10840 let op = ArmOp::F64Neg {
10841 dd: VfpReg::D0,
10842 dm: VfpReg::D2,
10843 };
10844 let code = encoder.encode(&op).unwrap();
10845 assert_eq!(code.len(), 4);
10846 }
10847
10848 #[test]
10849 fn test_encode_f64_sqrt_arm32() {
10850 let encoder = ArmEncoder::new_arm32();
10851 let op = ArmOp::F64Sqrt {
10852 dd: VfpReg::D0,
10853 dm: VfpReg::D2,
10854 };
10855 let code = encoder.encode(&op).unwrap();
10856 assert_eq!(code.len(), 4);
10857 }
10858
10859 #[test]
10860 fn test_encode_f64_load_arm32() {
10861 let encoder = ArmEncoder::new_arm32();
10862 let op = ArmOp::F64Load {
10863 dd: VfpReg::D0,
10864 addr: MemAddr::imm(Reg::R0, 8),
10865 };
10866 let code = encoder.encode(&op).unwrap();
10867 assert_eq!(code.len(), 4);
10868 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10869 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
10870 assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
10871 }
10872
10873 #[test]
10874 fn test_encode_f64_store_thumb2() {
10875 let encoder = ArmEncoder::new_thumb2();
10876 let op = ArmOp::F64Store {
10877 dd: VfpReg::D0,
10878 addr: MemAddr::imm(Reg::SP, 0),
10879 };
10880 let code = encoder.encode(&op).unwrap();
10881 assert_eq!(code.len(), 4);
10882 }
10883
10884 #[test]
10885 fn test_encode_f64_compare_arm32() {
10886 let encoder = ArmEncoder::new_arm32();
10887 let op = ArmOp::F64Eq {
10888 rd: Reg::R0,
10889 dn: VfpReg::D0,
10890 dm: VfpReg::D1,
10891 };
10892 let code = encoder.encode(&op).unwrap();
10893 assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
10894 }
10895
10896 #[test]
10897 fn test_encode_f64_compare_thumb2() {
10898 let encoder = ArmEncoder::new_thumb2();
10899 let op = ArmOp::F64Lt {
10900 rd: Reg::R0,
10901 dn: VfpReg::D0,
10902 dm: VfpReg::D1,
10903 };
10904 let code = encoder.encode(&op).unwrap();
10905 // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
10906 assert_eq!(code.len(), 14);
10907 }
10908
10909 #[test]
10910 fn test_encode_f64_const_arm32() {
10911 let encoder = ArmEncoder::new_arm32();
10912 let op = ArmOp::F64Const {
10913 dd: VfpReg::D0,
10914 value: 3.125,
10915 };
10916 let code = encoder.encode(&op).unwrap();
10917 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10918 assert_eq!(code.len(), 20);
10919 }
10920
10921 #[test]
10922 fn test_encode_f64_const_thumb2() {
10923 let encoder = ArmEncoder::new_thumb2();
10924 let op = ArmOp::F64Const {
10925 dd: VfpReg::D0,
10926 value: 2.5,
10927 };
10928 let code = encoder.encode(&op).unwrap();
10929 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10930 assert_eq!(code.len(), 20);
10931 }
10932
10933 #[test]
10934 fn test_encode_f64_convert_i32s_arm32() {
10935 let encoder = ArmEncoder::new_arm32();
10936 let op = ArmOp::F64ConvertI32S {
10937 dd: VfpReg::D0,
10938 rm: Reg::R0,
10939 };
10940 let code = encoder.encode(&op).unwrap();
10941 // VMOV(4) + VCVT(4) = 8
10942 assert_eq!(code.len(), 8);
10943 }
10944
10945 #[test]
10946 fn test_encode_f64_promote_f32_arm32() {
10947 let encoder = ArmEncoder::new_arm32();
10948 let op = ArmOp::F64PromoteF32 {
10949 dd: VfpReg::D0,
10950 sm: VfpReg::S0,
10951 };
10952 let code = encoder.encode(&op).unwrap();
10953 assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
10954 }
10955
10956 #[test]
10957 fn test_encode_f64_promote_f32_thumb2() {
10958 let encoder = ArmEncoder::new_thumb2();
10959 let op = ArmOp::F64PromoteF32 {
10960 dd: VfpReg::D0,
10961 sm: VfpReg::S0,
10962 };
10963 let code = encoder.encode(&op).unwrap();
10964 assert_eq!(code.len(), 4);
10965 }
10966
10967 #[test]
10968 fn test_encode_i32_trunc_f64s_arm32() {
10969 let encoder = ArmEncoder::new_arm32();
10970 let op = ArmOp::I32TruncF64S {
10971 rd: Reg::R0,
10972 dm: VfpReg::D0,
10973 };
10974 let code = encoder.encode(&op).unwrap();
10975 // VCVT(4) + VMOV(4) = 8
10976 assert_eq!(code.len(), 8);
10977 }
10978
10979 #[test]
10980 fn test_encode_f64_reinterpret_i64_arm32() {
10981 let encoder = ArmEncoder::new_arm32();
10982 let op = ArmOp::F64ReinterpretI64 {
10983 dd: VfpReg::D0,
10984 rmlo: Reg::R0,
10985 rmhi: Reg::R1,
10986 };
10987 let code = encoder.encode(&op).unwrap();
10988 assert_eq!(code.len(), 4); // Single VMOV instruction
10989 }
10990
10991 #[test]
10992 fn test_encode_i64_reinterpret_f64_thumb2() {
10993 let encoder = ArmEncoder::new_thumb2();
10994 let op = ArmOp::I64ReinterpretF64 {
10995 rdlo: Reg::R0,
10996 rdhi: Reg::R1,
10997 dm: VfpReg::D0,
10998 };
10999 let code = encoder.encode(&op).unwrap();
11000 assert_eq!(code.len(), 4);
11001 }
11002
11003 #[test]
11004 fn test_encode_f64_trunc_thumb2() {
11005 let encoder = ArmEncoder::new_thumb2();
11006 let op = ArmOp::F64Trunc {
11007 dd: VfpReg::D0,
11008 dm: VfpReg::D1,
11009 };
11010 let code = encoder.encode(&op).unwrap();
11011 // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
11012 // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
11013 assert_eq!(code.len(), 4);
11014 assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
11015 }
11016
11017 /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
11018 /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
11019 /// clobbers ONLY its destination (+R12/flags where noted) — the previous
11020 /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
11021 /// min/max/rounding semantics were wrong (ordered IT select returned the
11022 /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
11023 #[test]
11024 fn test_369_f64_tail_thumb2_encodings_match_clang() {
11025 let enc = ArmEncoder::new_thumb2();
11026 // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
11027 for (op, want) in [
11028 (
11029 ArmOp::F64Nearest {
11030 dd: VfpReg::D1,
11031 dm: VfpReg::D2,
11032 },
11033 vec![0xb9, 0xfe, 0x42, 0x1b],
11034 ),
11035 (
11036 ArmOp::F64Ceil {
11037 dd: VfpReg::D1,
11038 dm: VfpReg::D2,
11039 },
11040 vec![0xba, 0xfe, 0x42, 0x1b],
11041 ),
11042 (
11043 ArmOp::F64Floor {
11044 dd: VfpReg::D1,
11045 dm: VfpReg::D2,
11046 },
11047 vec![0xbb, 0xfe, 0x42, 0x1b],
11048 ),
11049 ] {
11050 assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
11051 }
11052 // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
11053 let min = enc
11054 .encode(&ArmOp::F64Min {
11055 dd: VfpReg::D0,
11056 dn: VfpReg::D1,
11057 dm: VfpReg::D2,
11058 })
11059 .unwrap();
11060 assert_eq!(
11061 min,
11062 vec![
11063 0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
11064 0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
11065 0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
11066 0x68, 0xbf, // it vs
11067 0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11068 ]
11069 );
11070 // vmaxnm variant flips only bit6 of the VMINNM word.
11071 let max = enc
11072 .encode(&ArmOp::F64Max {
11073 dd: VfpReg::D0,
11074 dn: VfpReg::D1,
11075 dm: VfpReg::D2,
11076 })
11077 .unwrap();
11078 assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11079 // Destination aliasing a source must ERR (the NaN fix-up would read
11080 // a clobbered operand), never encode.
11081 assert!(
11082 enc.encode(&ArmOp::F64Min {
11083 dd: VfpReg::D1,
11084 dn: VfpReg::D1,
11085 dm: VfpReg::D2,
11086 })
11087 .is_err()
11088 );
11089 // copysign d0,(mag)d1,(sign)d2:
11090 // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11091 let cs = enc
11092 .encode(&ArmOp::F64Copysign {
11093 dd: VfpReg::D0,
11094 dn: VfpReg::D1,
11095 dm: VfpReg::D2,
11096 })
11097 .unwrap();
11098 assert_eq!(
11099 cs,
11100 vec![
11101 0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11102 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11103 0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11104 0x48, 0xbf, // it mi
11105 0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11106 ]
11107 );
11108 // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11109 // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11110 let cs32 = enc
11111 .encode(&ArmOp::F32Copysign {
11112 sd: VfpReg::S0,
11113 sn: VfpReg::S1,
11114 sm: VfpReg::S2,
11115 })
11116 .unwrap();
11117 assert_eq!(
11118 cs32,
11119 vec![
11120 0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11121 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11122 0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11123 0x48, 0xbf, // it mi
11124 0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11125 ]
11126 );
11127 // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11128 // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11129 // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11130 let conv_s = enc
11131 .encode(&ArmOp::F64ConvertI32S {
11132 dd: VfpReg::D0,
11133 rm: Reg::R3,
11134 })
11135 .unwrap();
11136 assert_eq!(
11137 conv_s,
11138 vec![
11139 0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11140 0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11141 ]
11142 );
11143 let conv_u = enc
11144 .encode(&ArmOp::F64ConvertI32U {
11145 dd: VfpReg::D0,
11146 rm: Reg::R3,
11147 })
11148 .unwrap();
11149 assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11150 // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11151 // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11152 let trunc_s = enc
11153 .encode(&ArmOp::I32TruncF64S {
11154 rd: Reg::R3,
11155 dm: VfpReg::D1,
11156 })
11157 .unwrap();
11158 assert_eq!(
11159 trunc_s,
11160 vec![
11161 0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11162 0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11163 ]
11164 );
11165 let trunc_u = enc
11166 .encode(&ArmOp::I32TruncF64U {
11167 rd: Reg::R3,
11168 dm: VfpReg::D1,
11169 })
11170 .unwrap();
11171 assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11172 // f32.demote_f64: vcvt.f32.f64 s1, d2
11173 let demote = enc
11174 .encode(&ArmOp::F32DemoteF64 {
11175 sd: VfpReg::S1,
11176 dm: VfpReg::D2,
11177 })
11178 .unwrap();
11179 assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11180 }
11181
11182 #[test]
11183 fn test_encode_f64_min_arm32() {
11184 let encoder = ArmEncoder::new_arm32();
11185 let op = ArmOp::F64Min {
11186 dd: VfpReg::D0,
11187 dn: VfpReg::D1,
11188 dm: VfpReg::D2,
11189 };
11190 let code = encoder.encode(&op).unwrap();
11191 // VMOV + VCMP + VMRS + conditional VMOV = 16
11192 assert_eq!(code.len(), 16);
11193 }
11194
11195 #[test]
11196 fn test_f64_cp11_encoding() {
11197 // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11198 let encoder = ArmEncoder::new_arm32();
11199
11200 // F64Add
11201 let code = encoder
11202 .encode(&ArmOp::F64Add {
11203 dd: VfpReg::D0,
11204 dn: VfpReg::D0,
11205 dm: VfpReg::D0,
11206 })
11207 .unwrap();
11208 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11209 assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11210
11211 // F32Add for comparison
11212 let code = encoder
11213 .encode(&ArmOp::F32Add {
11214 sd: VfpReg::S0,
11215 sn: VfpReg::S0,
11216 sm: VfpReg::S0,
11217 })
11218 .unwrap();
11219 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11220 assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11221 }
11222
11223 #[test]
11224 fn test_dreg_encoding_higher_registers() {
11225 let encoder = ArmEncoder::new_arm32();
11226
11227 // Test with D15 (highest register)
11228 let op = ArmOp::F64Add {
11229 dd: VfpReg::D15,
11230 dn: VfpReg::D14,
11231 dm: VfpReg::D13,
11232 };
11233 let code = encoder.encode(&op).unwrap();
11234 assert_eq!(code.len(), 4);
11235
11236 // Verify the register encoding worked (instruction is valid)
11237 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11238 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11239 }
11240
11241 // ========================================================================
11242 // Control flow encoding tests
11243 // ========================================================================
11244
11245 #[test]
11246 fn test_encode_label_emits_no_bytes() {
11247 let encoder = ArmEncoder::new_thumb2();
11248 let op = ArmOp::Label {
11249 name: ".Lblock_end_0".to_string(),
11250 };
11251 let code = encoder.encode(&op).unwrap();
11252 assert!(code.is_empty(), "Label should emit zero bytes");
11253
11254 let encoder32 = ArmEncoder::new_arm32();
11255 let code32 = encoder32.encode(&op).unwrap();
11256 assert!(
11257 code32.is_empty(),
11258 "Label should emit zero bytes in ARM32 too"
11259 );
11260 }
11261
11262 #[test]
11263 fn test_encode_bcc_eq_thumb2() {
11264 use synth_synthesis::Condition;
11265 let encoder = ArmEncoder::new_thumb2();
11266 let op = ArmOp::Bcc {
11267 cond: Condition::EQ,
11268 label: "target".to_string(),
11269 };
11270 let code = encoder.encode(&op).unwrap();
11271 assert_eq!(code.len(), 2); // 16-bit conditional branch
11272
11273 // BEQ with offset 0: 0xD000 in little-endian
11274 assert_eq!(code, vec![0x00, 0xD0]);
11275 }
11276
11277 #[test]
11278 fn test_encode_bcc_ne_thumb2() {
11279 use synth_synthesis::Condition;
11280 let encoder = ArmEncoder::new_thumb2();
11281 let op = ArmOp::Bcc {
11282 cond: Condition::NE,
11283 label: "target".to_string(),
11284 };
11285 let code = encoder.encode(&op).unwrap();
11286 assert_eq!(code.len(), 2);
11287
11288 // BNE with offset 0: 0xD100 in little-endian
11289 assert_eq!(code, vec![0x00, 0xD1]);
11290 }
11291
11292 #[test]
11293 fn test_encode_bcc_arm32() {
11294 use synth_synthesis::Condition;
11295 let encoder = ArmEncoder::new_arm32();
11296 let op = ArmOp::Bcc {
11297 cond: Condition::EQ,
11298 label: "target".to_string(),
11299 };
11300 let code = encoder.encode(&op).unwrap();
11301 assert_eq!(code.len(), 4); // 32-bit ARM instruction
11302
11303 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11304 // BEQ: cond=0x0, opcode=0xA, offset=0
11305 assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11306 assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11307 }
11308
11309 #[test]
11310 fn test_encode_udf_thumb2() {
11311 let encoder = ArmEncoder::new_thumb2();
11312 let op = ArmOp::Udf { imm: 0 };
11313 let code = encoder.encode(&op).unwrap();
11314 assert_eq!(code.len(), 2); // 16-bit
11315
11316 // UDF #0: 0xDE00 in little-endian
11317 assert_eq!(code, vec![0x00, 0xDE]);
11318 }
11319
11320 /// #610: the i64 rot/div/rem expansions must land the result in the
11321 /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11322 /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11323 /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11324 /// the div/rem expansions ignored their register fields outright.
11325 #[test]
11326 fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11327 let encoder = ArmEncoder::new_thumb2();
11328 for op in [
11329 ArmOp::I64Rotl {
11330 rdlo: Reg::R4,
11331 rdhi: Reg::R5,
11332 rnlo: Reg::R0,
11333 rnhi: Reg::R1,
11334 shift: Reg::R2,
11335 },
11336 ArmOp::I64Rotr {
11337 rdlo: Reg::R4,
11338 rdhi: Reg::R5,
11339 rnlo: Reg::R0,
11340 rnhi: Reg::R1,
11341 shift: Reg::R2,
11342 },
11343 ] {
11344 let code = encoder.encode(&op).unwrap();
11345 assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11346 // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11347 // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11348 let tail: Vec<u16> = code[code.len() - 12..]
11349 .chunks(2)
11350 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11351 .collect();
11352 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11353 }
11354 }
11355
11356 /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11357 /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11358 #[test]
11359 fn test_610_i64_div_rem_expansion_guard_and_rd() {
11360 let encoder = ArmEncoder::new_thumb2();
11361 let mk = |which: u8| {
11362 let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11363 (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11364 match which {
11365 0 => ArmOp::I64DivU {
11366 rdlo,
11367 rdhi,
11368 rnlo,
11369 rnhi,
11370 rmlo,
11371 rmhi,
11372 elide_zero_guard: false,
11373 },
11374 1 => ArmOp::I64RemU {
11375 rdlo,
11376 rdhi,
11377 rnlo,
11378 rnhi,
11379 rmlo,
11380 rmhi,
11381 elide_zero_guard: false,
11382 },
11383 2 => ArmOp::I64DivS {
11384 rdlo,
11385 rdhi,
11386 rnlo,
11387 rnhi,
11388 rmlo,
11389 rmhi,
11390 elide_zero_guard: false,
11391 elide_overflow_guard: false,
11392 },
11393 _ => ArmOp::I64RemS {
11394 rdlo,
11395 rdhi,
11396 rnlo,
11397 rnhi,
11398 rmlo,
11399 rmhi,
11400 elide_zero_guard: false,
11401 },
11402 }
11403 };
11404 for which in 0..4u8 {
11405 let code = encoder.encode(&mk(which)).unwrap();
11406 // Zero-divisor trap guard right after the 26-byte marshal prologue.
11407 let guard: Vec<u16> = code[26..34]
11408 .chunks(2)
11409 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11410 .collect();
11411 assert_eq!(
11412 guard,
11413 vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11414 "ORRS R12,R2,R3; BNE +0; UDF #0"
11415 );
11416 // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11417 let tail: Vec<u16> = code[code.len() - 12..]
11418 .chunks(2)
11419 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11420 .collect();
11421 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11422 }
11423 }
11424
11425 /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11426 /// registers (drop the saved caller word) instead of popping over them.
11427 #[test]
11428 fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11429 let encoder = ArmEncoder::new_thumb2();
11430 let code = encoder
11431 .encode(&ArmOp::I64DivU {
11432 rdlo: Reg::R0,
11433 rdhi: Reg::R1,
11434 rnlo: Reg::R0,
11435 rnhi: Reg::R1,
11436 rmlo: Reg::R2,
11437 rmhi: Reg::R3,
11438 elide_zero_guard: false,
11439 })
11440 .unwrap();
11441 let tail: Vec<u16> = code[code.len() - 12..]
11442 .chunks(2)
11443 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11444 .collect();
11445 // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11446 // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11447 assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11448 }
11449
11450 /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11451 /// materialized by two MOVs in either order — must be a loud Err, never
11452 /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11453 #[test]
11454 fn test_610_i64_swapped_rd_pair_rejected() {
11455 let encoder = ArmEncoder::new_thumb2();
11456 let result = encoder.encode(&ArmOp::I64RemU {
11457 rdlo: Reg::R1,
11458 rdhi: Reg::R0,
11459 rnlo: Reg::R2,
11460 rnhi: Reg::R3,
11461 rmlo: Reg::R4,
11462 rmhi: Reg::R5,
11463 elide_zero_guard: false,
11464 });
11465 assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11466 }
11467
11468 /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11469 /// must not clobber the result. Pre-fix the total was materialized with
11470 /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11471 /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11472 /// carried across the restore in R12 (never allocatable, never restored)
11473 /// and moved into rd only after the pop — structurally rd-independent.
11474 #[test]
11475 fn test_632_i64_popcnt_result_survives_scratch_restore() {
11476 let encoder = ArmEncoder::new_thumb2();
11477 // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11478 for rd in [
11479 Reg::R0,
11480 Reg::R2,
11481 Reg::R3,
11482 Reg::R4,
11483 Reg::R5,
11484 Reg::R6,
11485 Reg::R8,
11486 ] {
11487 let code = encoder
11488 .encode(&ArmOp::I64Popcnt {
11489 rd,
11490 rnlo: Reg::R6,
11491 rnhi: Reg::R7,
11492 })
11493 .unwrap();
11494 assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11495 let hw: Vec<u16> = code
11496 .chunks(2)
11497 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11498 .collect();
11499 let pop = hw
11500 .iter()
11501 .position(|&h| h == 0xBC38)
11502 .expect("POP {R3,R4,R5} present");
11503 // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11504 // in R12, which the POP cannot touch).
11505 assert_eq!(
11506 &hw[pop - 2..pop],
11507 &[0xEB04, 0x0C05],
11508 "total must be carried in R12 across the restore"
11509 );
11510 // Immediately after the POP: MOV rd, R12.
11511 let rd_bits = match rd {
11512 Reg::R8 => 8u16,
11513 Reg::R6 => 6,
11514 Reg::R5 => 5,
11515 Reg::R4 => 4,
11516 Reg::R3 => 3,
11517 Reg::R2 => 2,
11518 _ => 0,
11519 };
11520 let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11521 assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11522 // No write into rd between the PUSH and the POP (the old
11523 // pre-restore ADDS is gone).
11524 assert!(
11525 !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11526 "no ADDS rd, R4, R5 before the restore pop"
11527 );
11528 }
11529 }
11530
11531 /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11532 /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11533 /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11534 #[test]
11535 fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11536 let encoder = ArmEncoder::new_thumb2();
11537 let code = encoder
11538 .encode(&ArmOp::I64Popcnt {
11539 rd: Reg::R0,
11540 rnlo: Reg::R3,
11541 rnhi: Reg::R4,
11542 })
11543 .unwrap();
11544 let hw: Vec<u16> = code
11545 .chunks(2)
11546 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11547 .collect();
11548 // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11549 // write to R4); MOV R4, R12.
11550 assert_eq!(hw[0], 0xB438);
11551 assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11552 assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11553 assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11554 }
11555
11556 /// #632: A32 twin — same structural fix on the ARM-mode path
11557 /// (`--target cortex-r5`): total carried in R12 across the restore.
11558 #[test]
11559 fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11560 let encoder = ArmEncoder::new_arm32();
11561 for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11562 let code = encoder
11563 .encode(&ArmOp::I64Popcnt {
11564 rd,
11565 rnlo: Reg::R6,
11566 rnhi: Reg::R7,
11567 })
11568 .unwrap();
11569 let words: Vec<u32> = code
11570 .chunks(4)
11571 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11572 .collect();
11573 let pop = words
11574 .iter()
11575 .position(|&w| w == 0xE8BD_0038)
11576 .expect("POP {R3,R4,R5} present");
11577 assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11578 let rd_bits = match rd {
11579 Reg::R8 => 8u32,
11580 Reg::R5 => 5,
11581 Reg::R4 => 4,
11582 Reg::R3 => 3,
11583 _ => 0,
11584 };
11585 assert_eq!(
11586 words[pop + 1],
11587 0xE1A0_0000 | (rd_bits << 12) | 12,
11588 "MOV rd, R12 after the restore"
11589 );
11590 }
11591 }
11592
11593 /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11594 /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11595 /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11596 #[test]
11597 fn test_633_i64_divs_overflow_guard_emitted() {
11598 let encoder = ArmEncoder::new_thumb2();
11599 let code = encoder
11600 .encode(&ArmOp::I64DivS {
11601 rdlo: Reg::R4,
11602 rdhi: Reg::R5,
11603 rnlo: Reg::R0,
11604 rnhi: Reg::R1,
11605 rmlo: Reg::R2,
11606 rmhi: Reg::R3,
11607 elide_zero_guard: false,
11608 elide_overflow_guard: false,
11609 })
11610 .unwrap();
11611 // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11612 let guard: Vec<u16> = code[34..56]
11613 .chunks(2)
11614 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11615 .collect();
11616 assert_eq!(
11617 guard,
11618 vec![
11619 0xEA02, 0x0C03, // AND.W R12, R2, R3
11620 0xF11C, 0x0F01, // CMN.W R12, #1
11621 0xD105, // BNE .no_trap
11622 0x2800, // CMP R0, #0
11623 0xD103, // BNE .no_trap
11624 0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11625 0xD100, // BNE .no_trap
11626 0xDE00, // UDF #0 — signed-division overflow
11627 ],
11628 "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11629 );
11630 }
11631
11632 /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11633 /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11634 /// UDF (the zero-divisor trap) in the whole expansion.
11635 #[test]
11636 fn test_633_i64_rems_has_no_overflow_guard() {
11637 let encoder = ArmEncoder::new_thumb2();
11638 for (is_rem_s, op) in [
11639 (
11640 true,
11641 ArmOp::I64RemS {
11642 rdlo: Reg::R4,
11643 rdhi: Reg::R5,
11644 rnlo: Reg::R0,
11645 rnhi: Reg::R1,
11646 rmlo: Reg::R2,
11647 rmhi: Reg::R3,
11648 elide_zero_guard: false,
11649 },
11650 ),
11651 (
11652 false,
11653 ArmOp::I64DivS {
11654 rdlo: Reg::R4,
11655 rdhi: Reg::R5,
11656 rnlo: Reg::R0,
11657 rnhi: Reg::R1,
11658 rmlo: Reg::R2,
11659 rmhi: Reg::R3,
11660 elide_zero_guard: false,
11661 elide_overflow_guard: false,
11662 },
11663 ),
11664 ] {
11665 let code = encoder.encode(&op).unwrap();
11666 let udfs = code
11667 .chunks(2)
11668 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11669 .count();
11670 let want = if is_rem_s { 1 } else { 2 };
11671 assert_eq!(
11672 udfs, want,
11673 "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11674 );
11675 }
11676 }
11677
11678 /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11679 /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11680 /// rest of the expansion is byte-identical (splice check).
11681 #[test]
11682 fn test_494_i64_zero_guard_elision_is_exact_splice() {
11683 let encoder = ArmEncoder::new_thumb2();
11684 let mk = |elide_zero_guard: bool| {
11685 encoder
11686 .encode(&ArmOp::I64DivU {
11687 rdlo: Reg::R4,
11688 rdhi: Reg::R5,
11689 rnlo: Reg::R0,
11690 rnhi: Reg::R1,
11691 rmlo: Reg::R2,
11692 rmhi: Reg::R3,
11693 elide_zero_guard,
11694 })
11695 .unwrap()
11696 };
11697 let full = mk(false);
11698 let elided = mk(true);
11699 assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11700 // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11701 assert_eq!(&full[..26], &elided[..26]);
11702 assert_eq!(
11703 &full[26..34],
11704 &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11705 "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11706 );
11707 assert_eq!(&full[34..], &elided[26..]);
11708 }
11709
11710 /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11711 /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11712 /// OVERFLOW guard is a separate obligation and must survive
11713 /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11714 #[test]
11715 fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11716 let encoder = ArmEncoder::new_thumb2();
11717 let mk = |zero: bool, ovf: bool| {
11718 encoder
11719 .encode(&ArmOp::I64DivS {
11720 rdlo: Reg::R4,
11721 rdhi: Reg::R5,
11722 rnlo: Reg::R0,
11723 rnhi: Reg::R1,
11724 rmlo: Reg::R2,
11725 rmhi: Reg::R3,
11726 elide_zero_guard: zero,
11727 elide_overflow_guard: ovf,
11728 })
11729 .unwrap()
11730 };
11731 let udf_count = |code: &[u8]| {
11732 code.chunks(2)
11733 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11734 .count()
11735 };
11736 let full = mk(false, false);
11737 let zero_only = mk(true, false);
11738 let both = mk(true, true);
11739 assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11740 assert_eq!(
11741 udf_count(&zero_only),
11742 1,
11743 "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11744 guard must be retained"
11745 );
11746 // The retained guard is the 22-byte overflow sequence, now right
11747 // after the 26-byte marshal prologue.
11748 let guard: Vec<u16> = zero_only[26..48]
11749 .chunks(2)
11750 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11751 .collect();
11752 assert_eq!(
11753 guard,
11754 vec![
11755 0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11756 0xDE00,
11757 ],
11758 "the surviving guard is the INT64_MIN/-1 overflow trap"
11759 );
11760 assert_eq!(full.len(), zero_only.len() + 8);
11761 assert_eq!(zero_only.len(), both.len() + 22);
11762 assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11763 }
11764
11765 /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11766 /// and the A32 overflow guard survives a zero-only elision.
11767 #[test]
11768 fn test_494_a32_i64_guard_elision() {
11769 let encoder = ArmEncoder::new_arm32();
11770 let mk = |zero: bool, ovf: bool| {
11771 encoder
11772 .encode(&ArmOp::I64DivS {
11773 rdlo: Reg::R4,
11774 rdhi: Reg::R5,
11775 rnlo: Reg::R0,
11776 rnhi: Reg::R1,
11777 rmlo: Reg::R2,
11778 rmhi: Reg::R3,
11779 elide_zero_guard: zero,
11780 elide_overflow_guard: ovf,
11781 })
11782 .unwrap()
11783 };
11784 let full = mk(false, false);
11785 let zero_only = mk(true, false);
11786 let both = mk(true, true);
11787 // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11788 assert_eq!(full.len(), zero_only.len() + 12);
11789 assert_eq!(zero_only.len(), both.len() + 24);
11790 let udf_count = |code: &[u8]| {
11791 code.chunks(4)
11792 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11793 .count()
11794 };
11795 assert_eq!(udf_count(&full), 2);
11796 assert_eq!(
11797 udf_count(&zero_only),
11798 1,
11799 "A32: overflow guard retained under zero-only elision"
11800 );
11801 assert_eq!(udf_count(&both), 0);
11802 }
11803
11804 /// #633: A32 twin — the conditional-execution overflow guard on the
11805 /// ARM-mode I64DivS, and its absence from I64RemS.
11806 #[test]
11807 fn test_633_a32_i64_divs_overflow_guard() {
11808 let encoder = ArmEncoder::new_arm32();
11809 let mk_divs = ArmOp::I64DivS {
11810 rdlo: Reg::R4,
11811 rdhi: Reg::R5,
11812 rnlo: Reg::R0,
11813 rnhi: Reg::R1,
11814 rmlo: Reg::R2,
11815 rmhi: Reg::R3,
11816 elide_zero_guard: false,
11817 elide_overflow_guard: false,
11818 };
11819 let code = encoder.encode(&mk_divs).unwrap();
11820 let words: Vec<u32> = code
11821 .chunks(4)
11822 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11823 .collect();
11824 let guard = [
11825 0xE002_C003u32, // AND R12, R2, R3
11826 0xE37C_0001, // CMN R12, #1
11827 0x0350_0000, // CMPEQ R0, #0
11828 0x0351_0102, // CMPEQ R1, #0x80000000
11829 0x1A00_0000, // BNE +1 insn
11830 0xE7F0_00F0, // UDF #0
11831 ];
11832 assert!(
11833 words.windows(6).any(|w| w == guard),
11834 "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
11835 );
11836 let rems = encoder
11837 .encode(&ArmOp::I64RemS {
11838 rdlo: Reg::R4,
11839 rdhi: Reg::R5,
11840 rnlo: Reg::R0,
11841 rnhi: Reg::R1,
11842 rmlo: Reg::R2,
11843 rmhi: Reg::R3,
11844 elide_zero_guard: false,
11845 })
11846 .unwrap();
11847 let rems_udfs = rems
11848 .chunks(4)
11849 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11850 .count();
11851 assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
11852 }
11853
11854 #[test]
11855 fn test_encode_nop_thumb2() {
11856 let encoder = ArmEncoder::new_thumb2();
11857 let op = ArmOp::Nop;
11858 let code = encoder.encode(&op).unwrap();
11859 assert_eq!(code.len(), 2); // 16-bit
11860
11861 // NOP: 0xBF00 in little-endian
11862 assert_eq!(code, vec![0x00, 0xBF]);
11863 }
11864
11865 // =========================================================================
11866 // i64 Thumb-2 encoding tests
11867 // =========================================================================
11868
11869 #[test]
11870 fn test_encode_i64_add_thumb2() {
11871 let encoder = ArmEncoder::new_thumb2();
11872 let op = ArmOp::I64Add {
11873 rdlo: Reg::R0,
11874 rdhi: Reg::R1,
11875 rnlo: Reg::R0,
11876 rnhi: Reg::R1,
11877 rmlo: Reg::R2,
11878 rmhi: Reg::R3,
11879 };
11880 let code = encoder.encode(&op).unwrap();
11881 // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
11882 assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
11883 }
11884
11885 #[test]
11886 fn test_encode_i64_sub_thumb2() {
11887 let encoder = ArmEncoder::new_thumb2();
11888 let op = ArmOp::I64Sub {
11889 rdlo: Reg::R0,
11890 rdhi: Reg::R1,
11891 rnlo: Reg::R0,
11892 rnhi: Reg::R1,
11893 rmlo: Reg::R2,
11894 rmhi: Reg::R3,
11895 };
11896 let code = encoder.encode(&op).unwrap();
11897 // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
11898 assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
11899 }
11900
11901 #[test]
11902 fn test_encode_i64_and_thumb2() {
11903 let encoder = ArmEncoder::new_thumb2();
11904 let op = ArmOp::I64And {
11905 rdlo: Reg::R0,
11906 rdhi: Reg::R1,
11907 rnlo: Reg::R0,
11908 rnhi: Reg::R1,
11909 rmlo: Reg::R2,
11910 rmhi: Reg::R3,
11911 };
11912 let code = encoder.encode(&op).unwrap();
11913 // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
11914 assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
11915 }
11916
11917 #[test]
11918 fn test_encode_i64_or_thumb2() {
11919 let encoder = ArmEncoder::new_thumb2();
11920 let op = ArmOp::I64Or {
11921 rdlo: Reg::R0,
11922 rdhi: Reg::R1,
11923 rnlo: Reg::R0,
11924 rnhi: Reg::R1,
11925 rmlo: Reg::R2,
11926 rmhi: Reg::R3,
11927 };
11928 let code = encoder.encode(&op).unwrap();
11929 assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
11930 }
11931
11932 #[test]
11933 fn test_encode_i64_xor_thumb2() {
11934 let encoder = ArmEncoder::new_thumb2();
11935 let op = ArmOp::I64Xor {
11936 rdlo: Reg::R0,
11937 rdhi: Reg::R1,
11938 rnlo: Reg::R0,
11939 rnhi: Reg::R1,
11940 rmlo: Reg::R2,
11941 rmhi: Reg::R3,
11942 };
11943 let code = encoder.encode(&op).unwrap();
11944 assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
11945 }
11946
11947 #[test]
11948 fn test_encode_i64_const_small_thumb2() {
11949 let encoder = ArmEncoder::new_thumb2();
11950 // Small constant: only needs MOVW for each half
11951 let op = ArmOp::I64Const {
11952 rdlo: Reg::R0,
11953 rdhi: Reg::R1,
11954 value: 42,
11955 };
11956 let code = encoder.encode(&op).unwrap();
11957 // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
11958 assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
11959 }
11960
11961 #[test]
11962 fn test_encode_i64_const_large_thumb2() {
11963 let encoder = ArmEncoder::new_thumb2();
11964 // Large constant: needs MOVW+MOVT for each half
11965 let op = ArmOp::I64Const {
11966 rdlo: Reg::R0,
11967 rdhi: Reg::R1,
11968 value: 0x1234_5678_9ABC_DEF0_u64 as i64,
11969 };
11970 let code = encoder.encode(&op).unwrap();
11971 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11972 assert_eq!(
11973 code.len(),
11974 16,
11975 "I64Const with large value should be 16 bytes"
11976 );
11977 }
11978
11979 #[test]
11980 fn test_encode_i64_extend_i32_s_thumb2() {
11981 let encoder = ArmEncoder::new_thumb2();
11982 let op = ArmOp::I64ExtendI32S {
11983 rdlo: Reg::R0,
11984 rdhi: Reg::R1,
11985 rn: Reg::R0,
11986 };
11987 let code = encoder.encode(&op).unwrap();
11988 // When rdlo == rn, only ASR (4 bytes) is emitted
11989 assert_eq!(
11990 code.len(),
11991 4,
11992 "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
11993 );
11994 }
11995
11996 #[test]
11997 fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
11998 let encoder = ArmEncoder::new_thumb2();
11999 let op = ArmOp::I64ExtendI32S {
12000 rdlo: Reg::R0,
12001 rdhi: Reg::R1,
12002 rn: Reg::R2,
12003 };
12004 let code = encoder.encode(&op).unwrap();
12005 // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
12006 assert!(
12007 code.len() >= 6,
12008 "I64ExtendI32S (diff reg) should be at least 6 bytes"
12009 );
12010 }
12011
12012 #[test]
12013 fn test_encode_i64_extend_i32_u_thumb2() {
12014 let encoder = ArmEncoder::new_thumb2();
12015 let op = ArmOp::I64ExtendI32U {
12016 rdlo: Reg::R0,
12017 rdhi: Reg::R1,
12018 rn: Reg::R0,
12019 };
12020 let code = encoder.encode(&op).unwrap();
12021 // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
12022 assert_eq!(
12023 code.len(),
12024 2,
12025 "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
12026 );
12027 }
12028
12029 #[test]
12030 fn test_encode_i32_wrap_i64_nop_thumb2() {
12031 let encoder = ArmEncoder::new_thumb2();
12032 // When rd == rnlo, should be a NOP
12033 let op = ArmOp::I32WrapI64 {
12034 rd: Reg::R0,
12035 rnlo: Reg::R0,
12036 };
12037 let code = encoder.encode(&op).unwrap();
12038 assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
12039 assert_eq!(code, vec![0x00, 0xBF]); // NOP
12040 }
12041
12042 #[test]
12043 fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
12044 let encoder = ArmEncoder::new_thumb2();
12045 let op = ArmOp::I32WrapI64 {
12046 rd: Reg::R2,
12047 rnlo: Reg::R0,
12048 };
12049 let code = encoder.encode(&op).unwrap();
12050 // MOV R2, R0 (2 or 4 bytes)
12051 assert!(
12052 code.len() >= 2,
12053 "I32WrapI64 diff reg should emit at least 2 bytes"
12054 );
12055 }
12056
12057 #[test]
12058 fn test_encode_i64_eqz_thumb2() {
12059 let encoder = ArmEncoder::new_thumb2();
12060 let op = ArmOp::I64Eqz {
12061 rd: Reg::R0,
12062 rnlo: Reg::R0,
12063 rnhi: Reg::R1,
12064 };
12065 let code = encoder.encode(&op).unwrap();
12066 // Delegates to I64SetCondZ which is already encoded
12067 assert!(
12068 code.len() >= 6,
12069 "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12070 );
12071 }
12072
12073 #[test]
12074 fn test_encode_i64_eq_thumb2() {
12075 let encoder = ArmEncoder::new_thumb2();
12076 let op = ArmOp::I64Eq {
12077 rd: Reg::R0,
12078 rnlo: Reg::R0,
12079 rnhi: Reg::R1,
12080 rmlo: Reg::R2,
12081 rmhi: Reg::R3,
12082 };
12083 let code = encoder.encode(&op).unwrap();
12084 // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12085 assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12086 }
12087
12088 #[test]
12089 fn test_encode_i64_ldr_thumb2() {
12090 let encoder = ArmEncoder::new_thumb2();
12091 let op = ArmOp::I64Ldr {
12092 rdlo: Reg::R0,
12093 rdhi: Reg::R1,
12094 addr: MemAddr::imm(Reg::SP, 0),
12095 };
12096 let code = encoder.encode(&op).unwrap();
12097 // Two LDR instructions (lo at offset, hi at offset+4)
12098 assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12099 }
12100
12101 #[test]
12102 fn test_372_i64_ldr_indexed_materializes_address() {
12103 // #372: a memory i64.load carries an index register (R11 + addr + off).
12104 // The encoder must materialize `ip = base + index` (ADD.W) and load via
12105 // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12106 // stay byte-identical (plain `[base,#off]`, no ADD).
12107 let encoder = ArmEncoder::new_thumb2();
12108 let indexed = encoder
12109 .encode(&ArmOp::I64Ldr {
12110 rdlo: Reg::R0,
12111 rdhi: Reg::R1,
12112 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12113 })
12114 .unwrap();
12115 // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12116 assert_eq!(
12117 &indexed[0..4],
12118 &[0x0b, 0xeb, 0x00, 0x0c],
12119 "indexed I64Ldr must start with ADD.W ip, base, index"
12120 );
12121 let frame = encoder
12122 .encode(&ArmOp::I64Ldr {
12123 rdlo: Reg::R0,
12124 rdhi: Reg::R1,
12125 addr: MemAddr::imm(Reg::SP, 8),
12126 })
12127 .unwrap();
12128 // No index -> no ADD.W prefix (byte-identical frame access).
12129 assert_ne!(
12130 &frame[0..2],
12131 &[0x0b, 0xeb],
12132 "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12133 );
12134 }
12135
12136 #[test]
12137 fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12138 // #382: an indexed i64.load/store whose static offset > 0xFFF must
12139 // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12140 // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12141 // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12142 // vs arm-none-eabi-as.
12143 let encoder = ArmEncoder::new_thumb2();
12144 // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12145
12146 let ld = encoder
12147 .encode(&ArmOp::I64Ldr {
12148 rdlo: Reg::R0,
12149 rdhi: Reg::R1,
12150 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12151 })
12152 .expect("large-offset i64.load must lower, not skip");
12153 // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12154 assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12155 // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12156 // that path can only reach imm12 offsets.
12157 assert_ne!(
12158 &ld[0..2],
12159 &[0x0b, 0xeb],
12160 "must materialize the large offset"
12161 );
12162 // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12163 assert_eq!(
12164 &ld[4..20],
12165 &[
12166 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12167 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12168 0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12169 0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12170 ],
12171 "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12172 );
12173
12174 // Store: same base materialization, STR halves.
12175 let st = encoder
12176 .encode(&ArmOp::I64Str {
12177 rdlo: Reg::R2,
12178 rdhi: Reg::R3,
12179 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12180 })
12181 .expect("large-offset i64.store must lower, not skip");
12182 assert_eq!(st.len(), 20);
12183 assert_eq!(
12184 &st[4..20],
12185 &[
12186 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12187 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12188 0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12189 0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12190 ],
12191 "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12192 );
12193
12194 // Small-offset (imm12) indexed access stays byte-identical (#372): the
12195 // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12196 // folded immediates — NO extra MOVW/ADD.
12197 let small = encoder
12198 .encode(&ArmOp::I64Ldr {
12199 rdlo: Reg::R0,
12200 rdhi: Reg::R1,
12201 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12202 })
12203 .unwrap();
12204 assert_eq!(
12205 &small[0..4],
12206 &[0x0b, 0xeb, 0x00, 0x0c],
12207 "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12208 );
12209 assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12210 }
12211
12212 #[test]
12213 fn test_encode_i64_str_thumb2() {
12214 let encoder = ArmEncoder::new_thumb2();
12215 let op = ArmOp::I64Str {
12216 rdlo: Reg::R0,
12217 rdhi: Reg::R1,
12218 addr: MemAddr::imm(Reg::SP, 0),
12219 };
12220 let code = encoder.encode(&op).unwrap();
12221 // Two STR instructions (lo at offset, hi at offset+4)
12222 assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12223 }
12224
12225 #[test]
12226 fn test_encode_i64_all_comparisons_thumb2() {
12227 let encoder = ArmEncoder::new_thumb2();
12228
12229 let ops = vec![
12230 ArmOp::I64Ne {
12231 rd: Reg::R0,
12232 rnlo: Reg::R0,
12233 rnhi: Reg::R1,
12234 rmlo: Reg::R2,
12235 rmhi: Reg::R3,
12236 },
12237 ArmOp::I64LtS {
12238 rd: Reg::R0,
12239 rnlo: Reg::R0,
12240 rnhi: Reg::R1,
12241 rmlo: Reg::R2,
12242 rmhi: Reg::R3,
12243 },
12244 ArmOp::I64LtU {
12245 rd: Reg::R0,
12246 rnlo: Reg::R0,
12247 rnhi: Reg::R1,
12248 rmlo: Reg::R2,
12249 rmhi: Reg::R3,
12250 },
12251 ArmOp::I64LeS {
12252 rd: Reg::R0,
12253 rnlo: Reg::R0,
12254 rnhi: Reg::R1,
12255 rmlo: Reg::R2,
12256 rmhi: Reg::R3,
12257 },
12258 ArmOp::I64LeU {
12259 rd: Reg::R0,
12260 rnlo: Reg::R0,
12261 rnhi: Reg::R1,
12262 rmlo: Reg::R2,
12263 rmhi: Reg::R3,
12264 },
12265 ArmOp::I64GtS {
12266 rd: Reg::R0,
12267 rnlo: Reg::R0,
12268 rnhi: Reg::R1,
12269 rmlo: Reg::R2,
12270 rmhi: Reg::R3,
12271 },
12272 ArmOp::I64GtU {
12273 rd: Reg::R0,
12274 rnlo: Reg::R0,
12275 rnhi: Reg::R1,
12276 rmlo: Reg::R2,
12277 rmhi: Reg::R3,
12278 },
12279 ArmOp::I64GeS {
12280 rd: Reg::R0,
12281 rnlo: Reg::R0,
12282 rnhi: Reg::R1,
12283 rmlo: Reg::R2,
12284 rmhi: Reg::R3,
12285 },
12286 ArmOp::I64GeU {
12287 rd: Reg::R0,
12288 rnlo: Reg::R0,
12289 rnhi: Reg::R1,
12290 rmlo: Reg::R2,
12291 rmhi: Reg::R3,
12292 },
12293 ];
12294
12295 for op in &ops {
12296 let code = encoder.encode(op).unwrap();
12297 assert!(
12298 code.len() >= 8,
12299 "i64 comparison {:?} should emit at least 8 bytes, got {}",
12300 op,
12301 code.len()
12302 );
12303 }
12304 }
12305
12306 #[test]
12307 fn test_encode_i64_const_zero_thumb2() {
12308 let encoder = ArmEncoder::new_thumb2();
12309 let op = ArmOp::I64Const {
12310 rdlo: Reg::R0,
12311 rdhi: Reg::R1,
12312 value: 0,
12313 };
12314 let code = encoder.encode(&op).unwrap();
12315 // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12316 assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12317 }
12318
12319 #[test]
12320 fn test_encode_i64_const_negative_one_thumb2() {
12321 let encoder = ArmEncoder::new_thumb2();
12322 let op = ArmOp::I64Const {
12323 rdlo: Reg::R0,
12324 rdhi: Reg::R1,
12325 value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12326 };
12327 let code = encoder.encode(&op).unwrap();
12328 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12329 assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12330 }
12331
12332 // =========================================================================
12333 // Sub-word load/store encoding tests
12334 // =========================================================================
12335
12336 #[test]
12337 fn test_encode_ldrb_arm32() {
12338 let encoder = ArmEncoder::new_arm32();
12339 let op = ArmOp::Ldrb {
12340 rd: Reg::R0,
12341 addr: MemAddr::imm(Reg::R1, 4),
12342 };
12343 let code = encoder.encode(&op).unwrap();
12344 assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12345 // LDRB R0, [R1, #4] = 0xE5D10004
12346 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12347 assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12348 }
12349
12350 #[test]
12351 fn test_encode_strb_arm32() {
12352 let encoder = ArmEncoder::new_arm32();
12353 let op = ArmOp::Strb {
12354 rd: Reg::R0,
12355 addr: MemAddr::imm(Reg::R1, 0),
12356 };
12357 let code = encoder.encode(&op).unwrap();
12358 assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12359 // STRB R0, [R1, #0] = 0xE5C10000
12360 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12361 assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12362 }
12363
12364 #[test]
12365 fn test_encode_ldrh_arm32() {
12366 let encoder = ArmEncoder::new_arm32();
12367 let op = ArmOp::Ldrh {
12368 rd: Reg::R0,
12369 addr: MemAddr::imm(Reg::R1, 2),
12370 };
12371 let code = encoder.encode(&op).unwrap();
12372 assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12373 }
12374
12375 #[test]
12376 fn test_encode_strh_arm32() {
12377 let encoder = ArmEncoder::new_arm32();
12378 let op = ArmOp::Strh {
12379 rd: Reg::R0,
12380 addr: MemAddr::imm(Reg::R1, 0),
12381 };
12382 let code = encoder.encode(&op).unwrap();
12383 assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12384 }
12385
12386 #[test]
12387 fn test_encode_ldrsb_arm32() {
12388 let encoder = ArmEncoder::new_arm32();
12389 let op = ArmOp::Ldrsb {
12390 rd: Reg::R0,
12391 addr: MemAddr::imm(Reg::R1, 0),
12392 };
12393 let code = encoder.encode(&op).unwrap();
12394 assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12395 }
12396
12397 #[test]
12398 fn test_encode_ldrsh_arm32() {
12399 let encoder = ArmEncoder::new_arm32();
12400 let op = ArmOp::Ldrsh {
12401 rd: Reg::R0,
12402 addr: MemAddr::imm(Reg::R1, 0),
12403 };
12404 let code = encoder.encode(&op).unwrap();
12405 assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12406 }
12407
12408 #[test]
12409 fn test_encode_ldrb_thumb2_16bit() {
12410 let encoder = ArmEncoder::new_thumb2();
12411 let op = ArmOp::Ldrb {
12412 rd: Reg::R0,
12413 addr: MemAddr::imm(Reg::R1, 4),
12414 };
12415 let code = encoder.encode(&op).unwrap();
12416 // Low registers + small offset -> 16-bit encoding
12417 assert_eq!(
12418 code.len(),
12419 2,
12420 "Thumb-2 LDRB with small offset should be 16-bit"
12421 );
12422 }
12423
12424 #[test]
12425 fn test_encode_ldrb_thumb2_32bit() {
12426 let encoder = ArmEncoder::new_thumb2();
12427 let op = ArmOp::Ldrb {
12428 rd: Reg::R0,
12429 addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12430 };
12431 let code = encoder.encode(&op).unwrap();
12432 assert_eq!(
12433 code.len(),
12434 4,
12435 "Thumb-2 LDRB with large offset should be 32-bit"
12436 );
12437 }
12438
12439 #[test]
12440 fn test_encode_strb_thumb2_16bit() {
12441 let encoder = ArmEncoder::new_thumb2();
12442 let op = ArmOp::Strb {
12443 rd: Reg::R0,
12444 addr: MemAddr::imm(Reg::R1, 10),
12445 };
12446 let code = encoder.encode(&op).unwrap();
12447 assert_eq!(
12448 code.len(),
12449 2,
12450 "Thumb-2 STRB with small offset should be 16-bit"
12451 );
12452 }
12453
12454 #[test]
12455 fn test_encode_ldrh_thumb2_16bit() {
12456 let encoder = ArmEncoder::new_thumb2();
12457 let op = ArmOp::Ldrh {
12458 rd: Reg::R0,
12459 addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12460 };
12461 let code = encoder.encode(&op).unwrap();
12462 assert_eq!(
12463 code.len(),
12464 2,
12465 "Thumb-2 LDRH with small aligned offset should be 16-bit"
12466 );
12467 }
12468
12469 #[test]
12470 fn test_encode_strh_thumb2_16bit() {
12471 let encoder = ArmEncoder::new_thumb2();
12472 let op = ArmOp::Strh {
12473 rd: Reg::R0,
12474 addr: MemAddr::imm(Reg::R1, 4),
12475 };
12476 let code = encoder.encode(&op).unwrap();
12477 assert_eq!(
12478 code.len(),
12479 2,
12480 "Thumb-2 STRH with small aligned offset should be 16-bit"
12481 );
12482 }
12483
12484 #[test]
12485 fn test_encode_ldrsb_thumb2() {
12486 let encoder = ArmEncoder::new_thumb2();
12487 let op = ArmOp::Ldrsb {
12488 rd: Reg::R0,
12489 addr: MemAddr::imm(Reg::R1, 0),
12490 };
12491 let code = encoder.encode(&op).unwrap();
12492 // LDRSB has no 16-bit immediate form, always 32-bit
12493 assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12494 }
12495
12496 #[test]
12497 fn test_encode_ldrsh_thumb2() {
12498 let encoder = ArmEncoder::new_thumb2();
12499 let op = ArmOp::Ldrsh {
12500 rd: Reg::R0,
12501 addr: MemAddr::imm(Reg::R1, 0),
12502 };
12503 let code = encoder.encode(&op).unwrap();
12504 assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12505 }
12506
12507 #[test]
12508 fn test_encode_memory_size_thumb2() {
12509 let encoder = ArmEncoder::new_thumb2();
12510 let op = ArmOp::MemorySize { rd: Reg::R0 };
12511 let code = encoder.encode(&op).unwrap();
12512 // R0 and R10 are not both low registers, so this needs careful handling
12513 assert!(!code.is_empty(), "MemorySize should produce code");
12514 }
12515
12516 #[test]
12517 fn test_encode_memory_grow_thumb2() {
12518 let encoder = ArmEncoder::new_thumb2();
12519 let op = ArmOp::MemoryGrow {
12520 rd: Reg::R0,
12521 rn: Reg::R0,
12522 };
12523 let code = encoder.encode(&op).unwrap();
12524 assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12525 }
12526
12527 #[test]
12528 fn test_encode_subword_reg_offset_thumb2() {
12529 let encoder = ArmEncoder::new_thumb2();
12530
12531 // LDRB with register offset
12532 let op = ArmOp::Ldrb {
12533 rd: Reg::R0,
12534 addr: MemAddr::reg(Reg::R1, Reg::R2),
12535 };
12536 let code = encoder.encode(&op).unwrap();
12537 assert_eq!(
12538 code.len(),
12539 4,
12540 "Thumb-2 LDRB with reg offset should be 32-bit"
12541 );
12542
12543 // STRB with register offset
12544 let op = ArmOp::Strb {
12545 rd: Reg::R0,
12546 addr: MemAddr::reg(Reg::R1, Reg::R2),
12547 };
12548 let code = encoder.encode(&op).unwrap();
12549 assert_eq!(
12550 code.len(),
12551 4,
12552 "Thumb-2 STRB with reg offset should be 32-bit"
12553 );
12554
12555 // LDRH with register offset
12556 let op = ArmOp::Ldrh {
12557 rd: Reg::R0,
12558 addr: MemAddr::reg(Reg::R1, Reg::R2),
12559 };
12560 let code = encoder.encode(&op).unwrap();
12561 assert_eq!(
12562 code.len(),
12563 4,
12564 "Thumb-2 LDRH with reg offset should be 32-bit"
12565 );
12566
12567 // STRH with register offset
12568 let op = ArmOp::Strh {
12569 rd: Reg::R0,
12570 addr: MemAddr::reg(Reg::R1, Reg::R2),
12571 };
12572 let code = encoder.encode(&op).unwrap();
12573 assert_eq!(
12574 code.len(),
12575 4,
12576 "Thumb-2 STRH with reg offset should be 32-bit"
12577 );
12578 }
12579
12580 #[test]
12581 fn test_encode_subword_reg_imm_offset_thumb2() {
12582 let encoder = ArmEncoder::new_thumb2();
12583
12584 // LDRB with both register and immediate offset
12585 let op = ArmOp::Ldrb {
12586 rd: Reg::R0,
12587 addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12588 };
12589 let code = encoder.encode(&op).unwrap();
12590 // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12591 assert_eq!(
12592 code.len(),
12593 8,
12594 "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12595 );
12596 }
12597
12598 // ========================================================================
12599 // Helium MVE encoding tests
12600 // ========================================================================
12601
12602 #[test]
12603 fn test_encode_mve_addi32_thumb2() {
12604 let encoder = ArmEncoder::new_thumb2();
12605 let op = ArmOp::MveAddI {
12606 qd: QReg::Q0,
12607 qn: QReg::Q1,
12608 qm: QReg::Q2,
12609 size: MveSize::S32,
12610 };
12611 let code = encoder.encode(&op).unwrap();
12612 assert_eq!(
12613 code.len(),
12614 4,
12615 "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
12616 );
12617 }
12618
12619 #[test]
12620 fn test_encode_mve_subi16_thumb2() {
12621 let encoder = ArmEncoder::new_thumb2();
12622 let op = ArmOp::MveSubI {
12623 qd: QReg::Q0,
12624 qn: QReg::Q1,
12625 qm: QReg::Q2,
12626 size: MveSize::S16,
12627 };
12628 let code = encoder.encode(&op).unwrap();
12629 assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
12630 }
12631
12632 #[test]
12633 fn test_encode_mve_muli8_thumb2() {
12634 let encoder = ArmEncoder::new_thumb2();
12635 let op = ArmOp::MveMulI {
12636 qd: QReg::Q0,
12637 qn: QReg::Q1,
12638 qm: QReg::Q2,
12639 size: MveSize::S8,
12640 };
12641 let code = encoder.encode(&op).unwrap();
12642 assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
12643 }
12644
12645 #[test]
12646 fn test_encode_mve_bitwise_thumb2() {
12647 let encoder = ArmEncoder::new_thumb2();
12648
12649 let ops = vec![
12650 ArmOp::MveAnd {
12651 qd: QReg::Q0,
12652 qn: QReg::Q1,
12653 qm: QReg::Q2,
12654 },
12655 ArmOp::MveOrr {
12656 qd: QReg::Q0,
12657 qn: QReg::Q1,
12658 qm: QReg::Q2,
12659 },
12660 ArmOp::MveEor {
12661 qd: QReg::Q0,
12662 qn: QReg::Q1,
12663 qm: QReg::Q2,
12664 },
12665 ArmOp::MveBic {
12666 qd: QReg::Q0,
12667 qn: QReg::Q1,
12668 qm: QReg::Q2,
12669 },
12670 ];
12671 for op in ops {
12672 let code = encoder.encode(&op).unwrap();
12673 assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
12674 }
12675 }
12676
12677 #[test]
12678 fn test_encode_mve_mvn_thumb2() {
12679 let encoder = ArmEncoder::new_thumb2();
12680 let op = ArmOp::MveMvn {
12681 qd: QReg::Q0,
12682 qm: QReg::Q1,
12683 };
12684 let code = encoder.encode(&op).unwrap();
12685 assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
12686 }
12687
12688 #[test]
12689 fn test_encode_mve_load_store_thumb2() {
12690 let encoder = ArmEncoder::new_thumb2();
12691
12692 let load = ArmOp::MveLoad {
12693 qd: QReg::Q0,
12694 addr: MemAddr::imm(Reg::R0, 16),
12695 };
12696 let code = encoder.encode(&load).unwrap();
12697 assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
12698
12699 let store = ArmOp::MveStore {
12700 qd: QReg::Q1,
12701 addr: MemAddr::imm(Reg::R1, 0),
12702 };
12703 let code = encoder.encode(&store).unwrap();
12704 assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
12705 }
12706
12707 #[test]
12708 fn test_encode_mve_const_thumb2() {
12709 let encoder = ArmEncoder::new_thumb2();
12710 let op = ArmOp::MveConst {
12711 qd: QReg::Q0,
12712 bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
12713 };
12714 let code = encoder.encode(&op).unwrap();
12715 // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
12716 // Some words with hi16=0 skip MOVT, so length varies
12717 assert!(
12718 code.len() >= 24,
12719 "MVE const should produce multiple instructions"
12720 );
12721 }
12722
12723 #[test]
12724 fn test_encode_mve_dup_thumb2() {
12725 let encoder = ArmEncoder::new_thumb2();
12726 let op = ArmOp::MveDup {
12727 qd: QReg::Q0,
12728 rn: Reg::R0,
12729 size: MveSize::S32,
12730 };
12731 let code = encoder.encode(&op).unwrap();
12732 assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
12733 }
12734
12735 #[test]
12736 fn test_encode_mve_extract_lane_thumb2() {
12737 let encoder = ArmEncoder::new_thumb2();
12738 let op = ArmOp::MveExtractLane {
12739 rd: Reg::R0,
12740 qn: QReg::Q1,
12741 lane: 2,
12742 size: MveSize::S32,
12743 };
12744 let code = encoder.encode(&op).unwrap();
12745 assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
12746 }
12747
12748 #[test]
12749 fn test_encode_mve_insert_lane_thumb2() {
12750 let encoder = ArmEncoder::new_thumb2();
12751 let op = ArmOp::MveInsertLane {
12752 qd: QReg::Q0,
12753 rn: Reg::R1,
12754 lane: 3,
12755 size: MveSize::S32,
12756 };
12757 let code = encoder.encode(&op).unwrap();
12758 assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
12759 }
12760
12761 #[test]
12762 fn test_encode_mve_addf32_thumb2() {
12763 let encoder = ArmEncoder::new_thumb2();
12764 let op = ArmOp::MveAddF32 {
12765 qd: QReg::Q0,
12766 qn: QReg::Q1,
12767 qm: QReg::Q2,
12768 };
12769 let code = encoder.encode(&op).unwrap();
12770 assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
12771 }
12772
12773 #[test]
12774 fn test_encode_mve_divf32_thumb2() {
12775 let encoder = ArmEncoder::new_thumb2();
12776 let op = ArmOp::MveDivF32 {
12777 qd: QReg::Q0,
12778 qn: QReg::Q1,
12779 qm: QReg::Q2,
12780 };
12781 let code = encoder.encode(&op).unwrap();
12782 // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
12783 assert_eq!(
12784 code.len(),
12785 16,
12786 "MVE VDIV.F32 (lane-wise) should be 16 bytes"
12787 );
12788 }
12789
12790 #[test]
12791 fn test_encode_mve_sqrtf32_thumb2() {
12792 let encoder = ArmEncoder::new_thumb2();
12793 let op = ArmOp::MveSqrtF32 {
12794 qd: QReg::Q0,
12795 qm: QReg::Q1,
12796 };
12797 let code = encoder.encode(&op).unwrap();
12798 // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
12799 assert_eq!(
12800 code.len(),
12801 16,
12802 "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
12803 );
12804 }
12805
12806 #[test]
12807 fn test_encode_mve_negf32_thumb2() {
12808 let encoder = ArmEncoder::new_thumb2();
12809 let op = ArmOp::MveNegF32 {
12810 qd: QReg::Q0,
12811 qm: QReg::Q1,
12812 };
12813 let code = encoder.encode(&op).unwrap();
12814 assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
12815 }
12816
12817 #[test]
12818 fn test_encode_mve_absf32_thumb2() {
12819 let encoder = ArmEncoder::new_thumb2();
12820 let op = ArmOp::MveAbsF32 {
12821 qd: QReg::Q0,
12822 qm: QReg::Q1,
12823 };
12824 let code = encoder.encode(&op).unwrap();
12825 assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
12826 }
12827
12828 /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
12829 /// immediate encoding for the byte range and documents its bound.
12830 ///
12831 /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
12832 /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
12833 /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
12834 /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
12835 /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
12836 /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
12837 /// ThumbExpandImm pattern (rotation / replication), which is NOT
12838 /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
12839 /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
12840 /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
12841 /// This bound covers the measured `flat_flight` waste (#209).
12842 #[test]
12843 fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
12844 let encoder = ArmEncoder::new_thumb2();
12845 let op = ArmOp::And {
12846 rd: Reg::R2,
12847 rn: Reg::R0,
12848 op2: Operand2::Imm(0x7e),
12849 };
12850 let code = encoder.encode(&op).unwrap();
12851 assert_eq!(
12852 code,
12853 vec![0x00, 0xf0, 0x7e, 0x02],
12854 "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
12855 );
12856 }
12857
12858 /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
12859 /// data-processing immediate fix. Encodable modified immediates round-trip to
12860 /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
12861 /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
12862 /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
12863 /// this encodes it correctly.
12864 #[test]
12865 fn try_thumb_expand_imm_encodes_modified_immediates() {
12866 assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
12867 assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
12868 assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
12869 assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
12870 assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
12871 assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
12872 assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
12873 assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
12874 // Genuinely unrepresentable (bits too far apart for an 8-bit window).
12875 assert_eq!(try_thumb_expand_imm(0x101), None);
12876 assert_eq!(try_thumb_expand_imm(0x12345), None);
12877 }
12878
12879 /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
12880 /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
12881 /// forcing the selector to materialize into a register — closing the
12882 /// silent-miscompile class of #251/#253.
12883 #[test]
12884 fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
12885 let encoder = ArmEncoder::new_thumb2();
12886 // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
12887 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
12888 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
12889 // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
12890 assert!(
12891 encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
12892 "cmp #0x101 must error, not compare the wrong constant"
12893 );
12894 assert!(
12895 encoder
12896 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
12897 .is_err()
12898 );
12899 assert!(
12900 encoder
12901 .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
12902 .is_err()
12903 );
12904 // ...but a valid modified immediate still encodes.
12905 assert!(
12906 encoder
12907 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
12908 .is_ok()
12909 );
12910 }
12911
12912 /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
12913 /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
12914 #[test]
12915 fn mla_thumb2_encodes_correctly() {
12916 let encoder = ArmEncoder::new_thumb2();
12917 let code = encoder
12918 .encode(&ArmOp::Mla {
12919 rd: Reg::R2,
12920 rn: Reg::R3,
12921 rm: Reg::R4,
12922 ra: Reg::R8,
12923 })
12924 .unwrap();
12925 // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
12926 assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
12927 }
12928
12929 /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
12930 /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
12931 /// They now error (the selector must use register-offset addressing) — the
12932 /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
12933 #[test]
12934 fn ldst_imm12_offset_errors_when_out_of_range() {
12935 let encoder = ArmEncoder::new_thumb2();
12936 // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
12937 assert!(
12938 encoder
12939 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
12940 .is_ok()
12941 );
12942 // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
12943 assert!(
12944 encoder
12945 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
12946 .is_err(),
12947 "ldr offset 4096 must error, not wrap to 0"
12948 );
12949 assert!(
12950 encoder
12951 .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
12952 .is_err()
12953 );
12954 assert!(
12955 encoder
12956 .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
12957 .is_err()
12958 );
12959 assert!(
12960 encoder
12961 .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
12962 .is_err()
12963 );
12964 }
12965
12966 /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
12967 /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
12968 /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
12969 /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
12970 /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
12971 /// beyond 4095.
12972 #[test]
12973 fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
12974 let encoder = ArmEncoder::new_thumb2();
12975 // add sp, sp, #256 → ADDW (T4) SP, SP, #256 = 0d f2 00 1d
12976 assert_eq!(
12977 encoder
12978 .encode(&ArmOp::Add {
12979 rd: Reg::SP,
12980 rn: Reg::SP,
12981 op2: Operand2::Imm(256),
12982 })
12983 .unwrap(),
12984 vec![0x0d, 0xf2, 0x00, 0x1d],
12985 "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
12986 );
12987 // sub sp, sp, #256 → SUBW (T4) SP, SP, #256 = ad f2 00 1d
12988 assert_eq!(
12989 encoder
12990 .encode(&ArmOp::Sub {
12991 rd: Reg::SP,
12992 rn: Reg::SP,
12993 op2: Operand2::Imm(256),
12994 })
12995 .unwrap(),
12996 vec![0xad, 0xf2, 0x00, 0x1d],
12997 );
12998 // > 4095 has no single-instruction encoding → error, not silent wrong.
12999 assert!(
13000 encoder
13001 .encode(&ArmOp::Add {
13002 rd: Reg::SP,
13003 rn: Reg::SP,
13004 op2: Operand2::Imm(5000),
13005 })
13006 .is_err(),
13007 "add #5000 must error (no single ADDW), not mis-encode"
13008 );
13009 }
13010
13011 /// Closes the data-proc immediate class: AND and CMN now go through
13012 /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
13013 /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
13014 /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
13015 #[test]
13016 fn and_cmn_immediate_thumb_expand_else_error() {
13017 let encoder = ArmEncoder::new_thumb2();
13018 // byte range unchanged (bit-identical with the pre-retrofit encoding)
13019 assert_eq!(
13020 encoder
13021 .encode(&ArmOp::And {
13022 rd: Reg::R2,
13023 rn: Reg::R0,
13024 op2: Operand2::Imm(0x7e),
13025 })
13026 .unwrap(),
13027 vec![0x00, 0xf0, 0x7e, 0x02],
13028 );
13029 // a valid replicated modified immediate now encodes (was silently wrong)
13030 assert!(
13031 encoder
13032 .encode(&ArmOp::And {
13033 rd: Reg::R2,
13034 rn: Reg::R0,
13035 op2: Operand2::Imm(0xff00ff00u32 as i32),
13036 })
13037 .is_ok()
13038 );
13039 // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
13040 assert!(
13041 encoder
13042 .encode(&ArmOp::And {
13043 rd: Reg::R2,
13044 rn: Reg::R0,
13045 op2: Operand2::Imm(0x101),
13046 })
13047 .is_err()
13048 );
13049 assert!(
13050 encoder
13051 .encode(&ArmOp::Cmn {
13052 rn: Reg::R0,
13053 op2: Operand2::Imm(0x101),
13054 })
13055 .is_err(),
13056 "CMN #0x101 must error, not emit a NOP"
13057 );
13058 }
13059
13060 /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
13061 /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
13062 /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
13063 #[test]
13064 fn orr_eor_immediate_encode_in_byte_range_else_error() {
13065 let encoder = ArmEncoder::new_thumb2();
13066 // orr r2, r0, #0x7e → ORR.W T1, imm8=0x7e
13067 assert_eq!(
13068 encoder
13069 .encode(&ArmOp::Orr {
13070 rd: Reg::R2,
13071 rn: Reg::R0,
13072 op2: Operand2::Imm(0x7e),
13073 })
13074 .unwrap(),
13075 vec![0x40, 0xf0, 0x7e, 0x02],
13076 );
13077 // eor r2, r0, #0x7e → EOR.W T1, imm8=0x7e
13078 assert_eq!(
13079 encoder
13080 .encode(&ArmOp::Eor {
13081 rd: Reg::R2,
13082 rn: Reg::R0,
13083 op2: Operand2::Imm(0x7e),
13084 })
13085 .unwrap(),
13086 vec![0x80, 0xf0, 0x7e, 0x02],
13087 );
13088 // Out-of-range immediates error rather than silently mis-encode / NOP.
13089 assert!(
13090 encoder
13091 .encode(&ArmOp::Orr {
13092 rd: Reg::R2,
13093 rn: Reg::R0,
13094 op2: Operand2::Imm(0x140),
13095 })
13096 .is_err(),
13097 "ORR #0x140 must error, not emit a NOP"
13098 );
13099 }
13100
13101 #[test]
13102 fn test_encode_mve_different_qregs() {
13103 let encoder = ArmEncoder::new_thumb2();
13104
13105 // Test that different Q-register numbers produce different encodings
13106 let op1 = ArmOp::MveAddI {
13107 qd: QReg::Q0,
13108 qn: QReg::Q0,
13109 qm: QReg::Q0,
13110 size: MveSize::S32,
13111 };
13112 let op2 = ArmOp::MveAddI {
13113 qd: QReg::Q3,
13114 qn: QReg::Q5,
13115 qm: QReg::Q7,
13116 size: MveSize::S32,
13117 };
13118 let code1 = encoder.encode(&op1).unwrap();
13119 let code2 = encoder.encode(&op2).unwrap();
13120 assert_ne!(
13121 code1, code2,
13122 "Different Q-registers should produce different encodings"
13123 );
13124 }
13125
13126 #[test]
13127 fn test_encode_mve_arm32_loud_err() {
13128 // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13129 // a silent NOP here (dropping the vector op); it must now be a typed
13130 // Err so a broken "MVE implies Thumb" invariant fails loudly.
13131 let encoder = ArmEncoder::new_arm32();
13132 let op = ArmOp::MveAddI {
13133 qd: QReg::Q0,
13134 qn: QReg::Q1,
13135 qm: QReg::Q2,
13136 size: MveSize::S32,
13137 };
13138 let err = encoder
13139 .encode(&op)
13140 .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13141 assert!(
13142 err.to_string().contains("Thumb-2 only"),
13143 "unexpected error message: {err}"
13144 );
13145 }
13146}