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 debug_assert!(expected_id <= 255, "selector enforces the CMP imm8 range");
194 debug_assert!(type_off <= 4095, "selector enforces the LDR imm12 range");
195 // MOV r12, idx, LSL #2 (same as the dispatch tail's scale).
196 bytes.extend_from_slice(&(0xE1A0C000u32 | (2 << 7) | idx).to_le_bytes());
197 // ADD r12, r11, r12 — data-processing ADD (register).
198 bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
199 // LDR r12, [r12, #type_off] — immediate offset, P=1 U=1 L=1.
200 bytes.extend_from_slice(&(0xE59CC000u32 | (type_off & 0xFFF)).to_le_bytes());
201 // CMP r12, #expected_id — data-processing CMP (immediate).
202 bytes.extend_from_slice(&(0xE35C_0000u32 | (expected_id & 0xFF)).to_le_bytes());
203 // BEQ +1 insn (skip the UDF when the class id matches) —
204 // cond=EQ(0000), imm24=0: target = branch + 8.
205 bytes.extend_from_slice(&0x0A00_0000u32.to_le_bytes());
206 // UDF — the §4.4.8 type-mismatch trap.
207 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
208 }
209 // MOV r12, idx, LSL #2 — data-processing MOV, register op2 with
210 // imm5=2/LSL: cond=E, opcode=1101, S=0, Rd=r12.
211 let mov: u32 = 0xE1A0C000 | (2 << 7) | idx;
212 bytes.extend_from_slice(&mov.to_le_bytes());
213 if table_byte_offset == 0 {
214 // Table 0 (base = R11 itself): the pre-#650 single-load form.
215 // LDR r12, [r11, r12] — register offset, P=1 U=1 B=0 W=0 L=1.
216 let ldr: u32 = 0xE79BC00C;
217 bytes.extend_from_slice(&ldr.to_le_bytes());
218 } else {
219 // #650: fold the table's compile-time base offset into the
220 // pointer load via the LDR imm12 form.
221 assert!(
222 table_byte_offset <= 4095,
223 "call_indirect table base offset {table_byte_offset} exceeds \
224 LDR imm12 — the selector must have declined this (#650)"
225 );
226 // ADD r12, r11, r12 — data-processing ADD (register).
227 bytes.extend_from_slice(&0xE08BC00Cu32.to_le_bytes());
228 // LDR r12, [r12, #offset] — immediate offset, P=1 U=1 L=1.
229 let ldr: u32 = 0xE59CC000 | (table_byte_offset & 0xFFF);
230 bytes.extend_from_slice(&ldr.to_le_bytes());
231 }
232 // #664: null-slot trap — only when the table image has null slots
233 // (zero-linked words). A fully-initialized table keeps the pre-#664
234 // bytes identical by construction.
235 if null_check {
236 // CMP r12, #0 — data-processing CMP (immediate), Rn=r12.
237 bytes.extend_from_slice(&0xE35C_0000u32.to_le_bytes());
238 // BNE +1 insn (skip the UDF when the pointer is non-null) —
239 // cond=NE(0001), imm24=0: target = branch + 8.
240 bytes.extend_from_slice(&0x1A00_0000u32.to_le_bytes());
241 // UDF — the §4.4.8 uninitialized-element trap (same idiom as
242 // the bounds guard).
243 bytes.extend_from_slice(&0xE7F0_00F0u32.to_le_bytes());
244 }
245 // BLX r12 — cond=E, 0001 0010 1111 1111 1111 0011, Rm=r12.
246 let blx: u32 = 0xE12FFF3C;
247 bytes.extend_from_slice(&blx.to_le_bytes());
248 bytes
249 }
250
251 /// #615: A32 (ARM-mode) expansions for the multi-instruction ops that the
252 /// Thumb-2 encoder expands but the A32 arm previously encoded as a single
253 /// literal NOP (`0xE1A00000`) — i64 mul / shifts / rotates / comparisons /
254 /// eqz, plus i64 const/load/store/extend/wrap and the i32 SetCond /
255 /// SelectMove pseudo-ops. Each expansion mirrors its Thumb-2 twin's
256 /// register contract and semantics exactly (A32 conditional execution
257 /// replaces the IT blocks). Returns `Ok(None)` for ops this helper does
258 /// not handle; the caller's match encodes or loudly rejects those.
259 fn encode_arm_expanded(&self, op: &ArmOp) -> Result<Option<Vec<u8>>> {
260 use synth_synthesis::Condition;
261
262 /// A32 condition-field bits (instruction bits [31:28]).
263 fn cond_bits(cond: &Condition) -> u32 {
264 match cond {
265 Condition::EQ => 0x0,
266 Condition::NE => 0x1,
267 Condition::HS => 0x2, // CS: unsigned >=
268 Condition::LO => 0x3, // CC: unsigned <
269 Condition::HI => 0x8, // unsigned >
270 Condition::LS => 0x9, // unsigned <=
271 Condition::GE => 0xA,
272 Condition::LT => 0xB,
273 Condition::GT => 0xC,
274 Condition::LE => 0xD,
275 }
276 }
277 fn w(b: &mut Vec<u8>, word: u32) {
278 b.extend_from_slice(&word.to_le_bytes());
279 }
280 /// MOV<cond> rd, #imm (rotated-immediate form; only 0/1 used here).
281 fn mov_cond_imm(b: &mut Vec<u8>, cond: u32, rd: u32, imm: u32) {
282 w(b, (cond << 28) | 0x03A0_0000 | (rd << 12) | imm);
283 }
284 /// After a flag-setting pair: MOV<cond> rd,#1 ; MOV<!cond> rd,#0.
285 fn set_cond(b: &mut Vec<u8>, cond: &Condition, rd: u32) {
286 mov_cond_imm(b, cond_bits(cond), rd, 1);
287 mov_cond_imm(b, cond_bits(&cond.invert()), rd, 0);
288 }
289 /// CMP rn, rm (register form).
290 fn cmp_reg(b: &mut Vec<u8>, rn: u32, rm: u32) {
291 w(b, 0xE150_0000 | (rn << 16) | rm);
292 }
293 /// SBCS rd, rn, rm — the 64-bit compare idiom's high-word subtract.
294 fn sbcs(b: &mut Vec<u8>, rd: u32, rn: u32, rm: u32) {
295 w(b, 0xE0D0_0000 | (rn << 16) | (rd << 12) | rm);
296 }
297 /// MOVW rd, #imm16.
298 fn movw(b: &mut Vec<u8>, rd: u32, v: u32) {
299 w(
300 b,
301 0xE300_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
302 );
303 }
304 /// MOVT rd, #imm16.
305 fn movt(b: &mut Vec<u8>, rd: u32, v: u32) {
306 w(
307 b,
308 0xE340_0000 | (((v >> 12) & 0xF) << 16) | (rd << 12) | (v & 0xFFF),
309 );
310 }
311 /// Register-controlled shift: MOV rd, rn, <LSL|LSR|ASR> rs.
312 /// `ty`: 0=LSL, 1=LSR, 2=ASR. A32 uses the bottom byte of rs;
313 /// amounts of 32 or more yield 0 (LSL/LSR) or all-sign (ASR) — same
314 /// semantics the Thumb-2 expansions rely on.
315 fn shift_reg(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, rs: u32) {
316 w(b, 0xE1A0_0010 | (rd << 12) | (rs << 8) | (ty << 5) | rn);
317 }
318 const LSL: u32 = 0;
319 const LSR: u32 = 1;
320 const ASR: u32 = 2;
321 /// Immediate-shift move: MOV rd, rn, <LSL|LSR|ASR> #imm.
322 fn shift_imm(b: &mut Vec<u8>, ty: u32, rd: u32, rn: u32, imm: u32) {
323 w(
324 b,
325 0xE1A0_0000 | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rn,
326 );
327 }
328 /// Data-processing register form: `base | rn<<16 | rd<<12 | rm`.
329 /// `base` carries cond/opcode/S (e.g. 0xE090_0000 = ADDS).
330 fn dp_reg(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32) {
331 w(b, base | (rn << 16) | (rd << 12) | rm);
332 }
333 /// Data-processing with an immediate-shifted register operand:
334 /// `<op> rd, rn, rm, <LSL|LSR|ASR> #imm` — the A32 barrel shifter
335 /// folds a shift into the second operand for free. #1021 uses this to
336 /// run the popcnt SWAR fold on R12 alone (no second scratch, so R11 —
337 /// the linear-memory base — is never touched).
338 fn dp_reg_shift(b: &mut Vec<u8>, base: u32, rd: u32, rn: u32, rm: u32, ty: u32, imm: u32) {
339 w(
340 b,
341 base | (rn << 16) | (rd << 12) | ((imm & 0x1F) << 7) | (ty << 5) | rm,
342 );
343 }
344 /// ORR rd, rd, rm, LSR #31 — the carry-propagation idiom of the
345 /// shift-subtract division loop (bring rm's MSB into rd's bit 0).
346 fn orr_lsr31(b: &mut Vec<u8>, rd: u32, rm: u32) {
347 w(
348 b,
349 0xE180_0000 | (rd << 16) | (rd << 12) | (31 << 7) | (1 << 5) | rm,
350 );
351 }
352 /// 64-bit two's-complement negate of the lo:hi pair (MVN/MVN/ADDS/ADC).
353 fn negate64(b: &mut Vec<u8>, lo: u32, hi: u32) {
354 w(b, 0xE1E0_0000 | (lo << 12) | lo); // MVN lo, lo
355 w(b, 0xE1E0_0000 | (hi << 12) | hi); // MVN hi, hi
356 w(b, 0xE290_0001 | (lo << 16) | (lo << 12)); // ADDS lo, lo, #1
357 w(b, 0xE2A0_0000 | (hi << 16) | (hi << 12)); // ADC hi, hi, #0
358 }
359 /// TST x, x ; BPL +4-instructions — the "skip the negate64 when the
360 /// sign bit is clear" guard of the signed div/rem arms.
361 fn skip_negate_if_positive(b: &mut Vec<u8>, x: u32) {
362 w(b, 0xE110_0000 | (x << 16) | x); // TST x, x
363 w(b, 0x5A00_0003); // BPL +4 insns (past negate64)
364 }
365 /// The 64-iteration shift-subtract division loop — A32 transcription
366 /// of the Thumb-2 #610 core: dividend R0:R1, divisor R2:R3, quotient
367 /// R4:R5, remainder R6:R7, loop counter in `counter` (R12 or R8).
368 fn div_loop(b: &mut Vec<u8>, counter: u32) {
369 w(b, 0xE3A0_0040 | (counter << 12)); // MOV counter, #64
370 let loop_start = b.len();
371 // quotient <<= 1
372 shift_imm(b, LSL, 5, 5, 1);
373 orr_lsr31(b, 5, 4);
374 shift_imm(b, LSL, 4, 4, 1);
375 // remainder <<= 1, OR in dividend MSB
376 shift_imm(b, LSL, 7, 7, 1);
377 orr_lsr31(b, 7, 6);
378 shift_imm(b, LSL, 6, 6, 1);
379 orr_lsr31(b, 6, 1);
380 // dividend <<= 1
381 shift_imm(b, LSL, 1, 1, 1);
382 orr_lsr31(b, 1, 0);
383 shift_imm(b, LSL, 0, 0, 1);
384 // if remainder >= divisor (64-bit unsigned): subtract, set q bit
385 w(b, 0xE157_0003); // CMP R7, R3 (high words)
386 w(b, 0x8A00_0002); // BHI .subtract (+2 insns)
387 w(b, 0x3A00_0004); // BLO .next (+4 insns)
388 w(b, 0xE156_0002); // CMP R6, R2 (low words, highs equal)
389 w(b, 0x3A00_0002); // BLO .next (+2 insns)
390 w(b, 0xE056_6002); // .subtract: SUBS R6, R6, R2
391 w(b, 0xE0C7_7003); // SBC R7, R7, R3
392 w(b, 0xE384_4001); // ORR R4, R4, #1
393 // .next: decrement and loop
394 w(b, 0xE250_0001 | (counter << 16) | (counter << 12)); // SUBS counter, #1
395 let diff = (loop_start as i64) - (b.len() as i64 + 8);
396 w(b, 0x1A00_0000 | (((diff / 4) as u32) & 0x00FF_FFFF)); // BNE loop
397 }
398 /// 32-bit population count on working register `x` — A32 transcription
399 /// of the Thumb-2 I64Popcnt per-word core (mul-based fold): `c` is the
400 /// constant register, R12 the shifted temp. Both are clobbered.
401 fn popcnt_word(b: &mut Vec<u8>, x: u32, c: u32) {
402 // x = x - ((x >> 1) & 0x55555555)
403 shift_imm(b, LSR, 12, x, 1);
404 movw(b, c, 0x5555);
405 movt(b, c, 0x5555);
406 dp_reg(b, 0xE000_0000, 12, 12, c); // AND R12, R12, c
407 dp_reg(b, 0xE040_0000, x, x, 12); // SUB x, x, R12
408 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
409 movw(b, c, 0x3333);
410 movt(b, c, 0x3333);
411 dp_reg(b, 0xE000_0000, 12, x, c); // AND R12, x, c
412 shift_imm(b, LSR, x, x, 2);
413 dp_reg(b, 0xE000_0000, x, x, c); // AND x, x, c
414 dp_reg(b, 0xE080_0000, x, x, 12); // ADD x, x, R12
415 // x = (x + (x >> 4)) & 0x0F0F0F0F
416 shift_imm(b, LSR, 12, x, 4);
417 dp_reg(b, 0xE080_0000, x, x, 12); // ADD x, x, R12
418 movw(b, c, 0x0F0F);
419 movt(b, c, 0x0F0F);
420 dp_reg(b, 0xE000_0000, x, x, c); // AND x, x, c
421 // x = (x * 0x01010101) >> 24
422 movw(b, c, 0x0101);
423 movt(b, c, 0x0101);
424 w(b, 0xE000_0090 | (x << 16) | (c << 8) | x); // MUL x, x, c
425 shift_imm(b, LSR, x, x, 24);
426 }
427
428 let mut b: Vec<u8> = Vec::new();
429 match op {
430 // SetCond: materialize a flags-predicate as 0/1 — the A32 twin of
431 // the Thumb `ITE cond; MOV rd,#1; MOV rd,#0`.
432 ArmOp::SetCond { rd, cond } => {
433 set_cond(&mut b, cond, reg_to_bits(rd));
434 }
435
436 // SelectMove: conditional register move (Thumb: IT cond; MOV).
437 ArmOp::SelectMove { rd, rm, cond } => {
438 w(
439 &mut b,
440 (cond_bits(cond) << 28)
441 | 0x01A0_0000
442 | (reg_to_bits(rd) << 12)
443 | reg_to_bits(rm),
444 );
445 }
446
447 // I64SetCond: compare two i64 register pairs, 0/1 into rd.
448 // EQ/NE: CMP lo,lo; CMPEQ hi,hi (only if lows equal); set.
449 // Ordered: CMP lo,lo; SBCS rd,hi,hi; set — with the same
450 // operand-swap + condition mapping as the Thumb-2 arm.
451 ArmOp::I64SetCond {
452 rd,
453 rn_lo,
454 rn_hi,
455 rm_lo,
456 rm_hi,
457 cond,
458 } => {
459 let rd_b = reg_to_bits(rd);
460 let (n_lo, n_hi, m_lo, m_hi) = (
461 reg_to_bits(rn_lo),
462 reg_to_bits(rn_hi),
463 reg_to_bits(rm_lo),
464 reg_to_bits(rm_hi),
465 );
466 match cond {
467 Condition::EQ | Condition::NE => {
468 cmp_reg(&mut b, n_lo, m_lo);
469 // CMP<EQ> rn_hi, rm_hi — compare highs only if lows equal.
470 w(&mut b, 0x0150_0000 | (n_hi << 16) | m_hi);
471 set_cond(&mut b, cond, rd_b);
472 }
473 // (swap operands?, condition after SBCS) per the Thumb arm:
474 // LT/GE/LO/HS compare (rn, rm); GT/LE/HI/LS swap to (rm, rn).
475 Condition::LT => {
476 cmp_reg(&mut b, n_lo, m_lo);
477 sbcs(&mut b, rd_b, n_hi, m_hi);
478 set_cond(&mut b, &Condition::LT, rd_b);
479 }
480 Condition::GE => {
481 cmp_reg(&mut b, n_lo, m_lo);
482 sbcs(&mut b, rd_b, n_hi, m_hi);
483 set_cond(&mut b, &Condition::GE, rd_b);
484 }
485 Condition::GT => {
486 cmp_reg(&mut b, m_lo, n_lo);
487 sbcs(&mut b, rd_b, m_hi, n_hi);
488 set_cond(&mut b, &Condition::LT, rd_b);
489 }
490 Condition::LE => {
491 cmp_reg(&mut b, m_lo, n_lo);
492 sbcs(&mut b, rd_b, m_hi, n_hi);
493 set_cond(&mut b, &Condition::GE, rd_b);
494 }
495 Condition::LO => {
496 cmp_reg(&mut b, n_lo, m_lo);
497 sbcs(&mut b, rd_b, n_hi, m_hi);
498 set_cond(&mut b, &Condition::LO, rd_b);
499 }
500 Condition::HS => {
501 cmp_reg(&mut b, n_lo, m_lo);
502 sbcs(&mut b, rd_b, n_hi, m_hi);
503 set_cond(&mut b, &Condition::HS, rd_b);
504 }
505 Condition::HI => {
506 cmp_reg(&mut b, m_lo, n_lo);
507 sbcs(&mut b, rd_b, m_hi, n_hi);
508 set_cond(&mut b, &Condition::LO, rd_b);
509 }
510 Condition::LS => {
511 cmp_reg(&mut b, m_lo, n_lo);
512 sbcs(&mut b, rd_b, m_hi, n_hi);
513 set_cond(&mut b, &Condition::HS, rd_b);
514 }
515 }
516 }
517
518 // I64SetCondZ: ORRS rd, lo, hi sets Z iff the pair is zero.
519 ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
520 let rd_b = reg_to_bits(rd);
521 w(
522 &mut b,
523 0xE190_0000 | (reg_to_bits(rn_lo) << 16) | (rd_b << 12) | reg_to_bits(rn_hi),
524 );
525 set_cond(&mut b, &Condition::EQ, rd_b);
526 }
527
528 // i64 comparison wrappers: delegate to I64SetCond/Z, mirroring the
529 // Thumb-2 delegation arms.
530 ArmOp::I64Eqz { rd, rnlo, rnhi } => {
531 return self
532 .encode_arm(&ArmOp::I64SetCondZ {
533 rd: *rd,
534 rn_lo: *rnlo,
535 rn_hi: *rnhi,
536 })
537 .map(Some);
538 }
539 ArmOp::I64Eq {
540 rd,
541 rnlo,
542 rnhi,
543 rmlo,
544 rmhi,
545 }
546 | ArmOp::I64Ne {
547 rd,
548 rnlo,
549 rnhi,
550 rmlo,
551 rmhi,
552 }
553 | ArmOp::I64LtS {
554 rd,
555 rnlo,
556 rnhi,
557 rmlo,
558 rmhi,
559 }
560 | ArmOp::I64LtU {
561 rd,
562 rnlo,
563 rnhi,
564 rmlo,
565 rmhi,
566 }
567 | ArmOp::I64LeS {
568 rd,
569 rnlo,
570 rnhi,
571 rmlo,
572 rmhi,
573 }
574 | ArmOp::I64LeU {
575 rd,
576 rnlo,
577 rnhi,
578 rmlo,
579 rmhi,
580 }
581 | ArmOp::I64GtS {
582 rd,
583 rnlo,
584 rnhi,
585 rmlo,
586 rmhi,
587 }
588 | ArmOp::I64GtU {
589 rd,
590 rnlo,
591 rnhi,
592 rmlo,
593 rmhi,
594 }
595 | ArmOp::I64GeS {
596 rd,
597 rnlo,
598 rnhi,
599 rmlo,
600 rmhi,
601 }
602 | ArmOp::I64GeU {
603 rd,
604 rnlo,
605 rnhi,
606 rmlo,
607 rmhi,
608 } => {
609 let cond = match op {
610 ArmOp::I64Eq { .. } => Condition::EQ,
611 ArmOp::I64Ne { .. } => Condition::NE,
612 ArmOp::I64LtS { .. } => Condition::LT,
613 ArmOp::I64LtU { .. } => Condition::LO,
614 ArmOp::I64LeS { .. } => Condition::LE,
615 ArmOp::I64LeU { .. } => Condition::LS,
616 ArmOp::I64GtS { .. } => Condition::GT,
617 ArmOp::I64GtU { .. } => Condition::HI,
618 ArmOp::I64GeS { .. } => Condition::GE,
619 _ => Condition::HS,
620 };
621 return self
622 .encode_arm(&ArmOp::I64SetCond {
623 rd: *rd,
624 rn_lo: *rnlo,
625 rn_hi: *rnhi,
626 rm_lo: *rmlo,
627 rm_hi: *rmhi,
628 cond,
629 })
630 .map(Some);
631 }
632
633 // I64Mul: cross products into R12, then UMULL — same sequence and
634 // ordering as the Thumb-2 arm (R12 is encoder scratch, #212).
635 ArmOp::I64Mul {
636 rd_lo,
637 rd_hi,
638 rn_lo,
639 rn_hi,
640 rm_lo,
641 rm_hi,
642 } => {
643 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
644 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
645 let (ml, mh) = (reg_to_bits(rm_lo), reg_to_bits(rm_hi));
646 // MUL R12, rn_lo, rm_hi (R12 = a_lo * b_hi)
647 w(&mut b, 0xE000_0090 | (12 << 16) | (mh << 8) | nl);
648 // MLA R12, rn_hi, rm_lo, R12 (R12 += a_hi * b_lo)
649 w(
650 &mut b,
651 0xE020_0090 | (12 << 16) | (12 << 12) | (ml << 8) | nh,
652 );
653 // UMULL rd_lo, rd_hi, rn_lo, rm_lo
654 w(
655 &mut b,
656 0xE080_0090 | (dh << 16) | (dl << 12) | (ml << 8) | nl,
657 );
658 // ADD rd_hi, rd_hi, R12
659 w(&mut b, 0xE080_0000 | (dh << 16) | (dh << 12) | 12);
660 }
661
662 // I64Shl / I64ShrU / I64ShrS: same small/large-shift structure as
663 // the Thumb-2 arms. #1048: the expansion must never write its own
664 // input operands — the pre-#1048 A32 arms masked the amount in
665 // place (`AND ml, ml, #63`) and used the amount's home high
666 // register as scratch, identically to the Thumb-2 defect. R12
667 // (encoder scratch, never allocatable, #212) is the ONLY
668 // temporary; the masked amount is re-derived from the untouched
669 // rm_lo where a second live temp would otherwise be needed.
670 // Register-controlled shifts >= 32 yield 0, which the small path
671 // relies on for n = 0. Same #1039-style loud alias guards as the
672 // Thumb-2 arms.
673 ArmOp::I64Shl {
674 rd_lo,
675 rd_hi,
676 rn_lo,
677 rn_hi,
678 rm_lo,
679 rm_hi: _,
680 } => {
681 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
682 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
683 let ml = reg_to_bits(rm_lo);
684 if dh == nl || dh == ml {
685 return Err(synth_core::Error::synthesis(format!(
686 "I64Shl (A32): rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
687 )));
688 }
689 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
690 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
691 w(&mut b, 0x5A00_0007); // BPL .large
692 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
693 shift_reg(&mut b, LSL, dh, nh, 12); // dh = hi << n
694 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
695 shift_reg(&mut b, LSR, 12, nl, 12); // r12 = lo >> (32-n)
696 w(&mut b, 0xE180_0000 | (dh << 16) | (dh << 12) | 12); // ORR dh, dh, r12
697 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
698 shift_reg(&mut b, LSL, dl, nl, 12); // dl = lo << n
699 w(&mut b, 0xEA00_0001); // B .done
700 shift_reg(&mut b, LSL, dh, nl, 12); // .large: dh = lo << (n-32)
701 w(&mut b, 0xE3A0_0000 | (dl << 12)); // MOV dl, #0
702 }
703 ArmOp::I64ShrU {
704 rd_lo,
705 rd_hi,
706 rn_lo,
707 rn_hi,
708 rm_lo,
709 rm_hi: _,
710 } => {
711 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
712 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
713 let ml = reg_to_bits(rm_lo);
714 if dl == nh || dl == ml {
715 return Err(synth_core::Error::synthesis(format!(
716 "I64ShrU (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
717 )));
718 }
719 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
720 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
721 w(&mut b, 0x5A00_0007); // BPL .large
722 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
723 shift_reg(&mut b, LSR, dl, nl, 12); // dl = lo >> n
724 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
725 shift_reg(&mut b, LSL, 12, nh, 12); // r12 = hi << (32-n)
726 w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
727 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
728 shift_reg(&mut b, LSR, dh, nh, 12); // dh = hi >> n
729 w(&mut b, 0xEA00_0001); // B .done
730 shift_reg(&mut b, LSR, dl, nh, 12); // .large: dl = hi >> (n-32)
731 w(&mut b, 0xE3A0_0000 | (dh << 12)); // MOV dh, #0
732 }
733 ArmOp::I64ShrS {
734 rd_lo,
735 rd_hi,
736 rn_lo,
737 rn_hi,
738 rm_lo,
739 rm_hi: _,
740 } => {
741 let (dl, dh) = (reg_to_bits(rd_lo), reg_to_bits(rd_hi));
742 let (nl, nh) = (reg_to_bits(rn_lo), reg_to_bits(rn_hi));
743 let ml = reg_to_bits(rm_lo);
744 if dl == nh || dl == ml {
745 return Err(synth_core::Error::synthesis(format!(
746 "I64ShrS (A32): rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
747 )));
748 }
749 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
750 w(&mut b, 0xE250_0020 | (12 << 16) | (12 << 12)); // SUBS r12, r12, #32
751 w(&mut b, 0x5A00_0007); // BPL .large
752 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
753 shift_reg(&mut b, LSR, dl, nl, 12); // dl = lo >> n
754 w(&mut b, 0xE260_0020 | (12 << 16) | (12 << 12)); // RSB r12, r12, #32
755 shift_reg(&mut b, LSL, 12, nh, 12); // r12 = hi << (32-n)
756 w(&mut b, 0xE180_0000 | (dl << 16) | (dl << 12) | 12); // ORR dl, dl, r12
757 w(&mut b, 0xE200_003F | (ml << 16) | (12 << 12)); // AND r12, ml, #63
758 shift_reg(&mut b, ASR, dh, nh, 12); // dh = hi >> n (arith)
759 w(&mut b, 0xEA00_0001); // B .done
760 shift_reg(&mut b, ASR, dl, nh, 12); // .large: dl = hi >> (n-32)
761 w(&mut b, 0xE1A0_0040 | (dh << 12) | (31 << 7) | nh); // ASR dh, nh, #31
762 }
763
764 // I64Rotl / I64Rotr: the #610 fixed-ABI wrapper (A32 form) around
765 // the same fixed-register core as the Thumb-2 arms — value in
766 // R0:R1, amount in R2, scratch R3 + R12.
767 ArmOp::I64Rotl {
768 rdlo,
769 rdhi,
770 rnlo,
771 rnhi,
772 shift,
773 } => {
774 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
775 for word in [
776 0xE202_203Fu32, // AND R2, R2, #63 (mask amount mod 64)
777 0xE252_3020, // SUBS R3, R2, #32 (R3 = n-32, sets N)
778 0x5A00_0007, // BPL .large (n >= 32)
779 // --- small rotation (n < 32) ---
780 0xE262_3020, // RSB R3, R2, #32 (R3 = 32-n)
781 0xE1A0_C330, // LSR R12, R0, R3 (lo >> (32-n))
782 0xE1A0_3331, // LSR R3, R1, R3 (hi >> (32-n))
783 0xE1A0_1211, // LSL R1, R1, R2 (hi << n)
784 0xE181_100C, // ORR R1, R1, R12 (new_hi)
785 0xE1A0_0210, // LSL R0, R0, R2 (lo << n)
786 0xE180_0003, // ORR R0, R0, R3 (new_lo)
787 0xEA00_0007, // B .done
788 // --- large rotation (n >= 32), R3 = m = n-32 ---
789 0xE263_2020, // RSB R2, R3, #32 (R2 = 32-m = 64-n)
790 0xE1A0_C231, // LSR R12, R1, R2 (hi >> (64-n))
791 0xE1A0_2230, // LSR R2, R0, R2 (lo >> (64-n))
792 0xE1A0_0310, // LSL R0, R0, R3 (lo << m)
793 0xE1A0_1311, // LSL R1, R1, R3 (hi << m)
794 0xE180_C00C, // ORR R12, R0, R12 (new_hi = (lo<<m)|(hi>>(64-n)))
795 0xE181_0002, // ORR R0, R1, R2 (new_lo = (hi<<m)|(lo>>(64-n)))
796 0xE1A0_100C, // MOV R1, R12 (new_hi into place)
797 ] {
798 w(&mut b, word);
799 }
800 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
801 }
802 ArmOp::I64Rotr {
803 rdlo,
804 rdhi,
805 rnlo,
806 rnhi,
807 shift,
808 } => {
809 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, shift]);
810 for word in [
811 0xE202_203Fu32, // AND R2, R2, #63 (mask amount mod 64)
812 0xE252_3020, // SUBS R3, R2, #32 (R3 = n-32, sets N)
813 0x5A00_0007, // BPL .large (n >= 32)
814 // --- small rotation (n < 32) ---
815 0xE262_3020, // RSB R3, R2, #32 (R3 = 32-n)
816 0xE1A0_C311, // LSL R12, R1, R3 (hi << (32-n))
817 0xE1A0_3310, // LSL R3, R0, R3 (lo << (32-n))
818 0xE1A0_0230, // LSR R0, R0, R2 (lo >> n)
819 0xE180_000C, // ORR R0, R0, R12 (new_lo)
820 0xE1A0_1231, // LSR R1, R1, R2 (hi >> n)
821 0xE181_1003, // ORR R1, R1, R3 (new_hi)
822 0xEA00_0007, // B .done
823 // --- large rotation (n >= 32), R3 = m = n-32 ---
824 0xE263_2020, // RSB R2, R3, #32 (R2 = 32-m = 64-n)
825 0xE1A0_C210, // LSL R12, R0, R2 (lo << (64-n))
826 0xE1A0_2211, // LSL R2, R1, R2 (hi << (64-n))
827 0xE1A0_1331, // LSR R1, R1, R3 (hi >> m)
828 0xE181_C00C, // ORR R12, R1, R12 (new_lo = (hi>>m)|(lo<<(64-n)))
829 0xE1A0_1330, // LSR R1, R0, R3 (lo >> m)
830 0xE181_1002, // ORR R1, R1, R2 (new_hi = (lo>>m)|(hi<<(64-n)))
831 0xE1A0_000C, // MOV R0, R12 (new_lo into place)
832 ] {
833 w(&mut b, word);
834 }
835 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
836 }
837
838 // I64Clz: CLZ(hi), or 32 + CLZ(lo) when hi == 0. Conditional
839 // execution replaces the Thumb branches; like the Thumb arm, the
840 // high word of the result pair (rnhi) is cleared last.
841 ArmOp::I64Clz { rd, rnlo, rnhi } => {
842 let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
843 w(&mut b, 0xE350_0000 | (hi << 16)); // CMP rnhi, #0
844 w(&mut b, 0x116F_0F10 | (rd_b << 12) | hi); // CLZNE rd, rnhi
845 w(&mut b, 0x016F_0F10 | (rd_b << 12) | lo); // CLZEQ rd, rnlo
846 w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
847 // #1048: the former trailing `MOV rnhi, #0` is GONE — it
848 // wrote the OPERAND's home high register (see the Thumb-2
849 // I64Clz comment). Callers that relied on the implicit clear
850 // emit their own explicit hi-zero op.
851 }
852
853 // I64Ctz: CLZ(RBIT(lo)), or 32 + CLZ(RBIT(hi)) when lo == 0.
854 // RBIT/CLZ leave the flags intact, so the CMP's Z survives to the
855 // conditional ADD.
856 ArmOp::I64Ctz { rd, rnlo, rnhi } => {
857 let (rd_b, lo, hi) = (reg_to_bits(rd), reg_to_bits(rnlo), reg_to_bits(rnhi));
858 w(&mut b, 0xE350_0000 | (lo << 16)); // CMP rnlo, #0
859 w(&mut b, 0x16FF_0F30 | (rd_b << 12) | lo); // RBITNE rd, rnlo
860 w(&mut b, 0x06FF_0F30 | (rd_b << 12) | hi); // RBITEQ rd, rnhi
861 w(&mut b, 0xE16F_0F10 | (rd_b << 12) | rd_b); // CLZ rd, rd
862 w(&mut b, 0x0280_0020 | (rd_b << 16) | (rd_b << 12)); // ADDEQ rd, rd, #32
863 // #1048: no trailing `MOV rnhi, #0` — see I64Clz above.
864 }
865
866 // I64Const: MOVW/MOVT per half (MOVT elided when the half fits in
867 // 16 bits, mirroring the Thumb-2 arm).
868 ArmOp::I64Const { rdlo, rdhi, value } => {
869 let lo32 = *value as u32;
870 let hi32 = (*value >> 32) as u32;
871 movw(&mut b, reg_to_bits(rdlo), lo32 & 0xFFFF);
872 if lo32 > 0xFFFF {
873 movt(&mut b, reg_to_bits(rdlo), lo32 >> 16);
874 }
875 movw(&mut b, reg_to_bits(rdhi), hi32 & 0xFFFF);
876 if hi32 > 0xFFFF {
877 movt(&mut b, reg_to_bits(rdhi), hi32 >> 16);
878 }
879 }
880
881 // I64Ldr / I64Str: two word accesses at [base, #off] / #off+4.
882 // A register offset is materialized into IP once (the #206/#372
883 // hazard: dropping it would read the wrong address).
884 ArmOp::I64Ldr { rdlo, rdhi, addr } | ArmOp::I64Str { rdlo, rdhi, addr } => {
885 let base = if let Some(rm) = addr.offset_reg {
886 // ADD ip, base, rm
887 w(
888 &mut b,
889 0xE080_0000
890 | (reg_to_bits(&addr.base) << 16)
891 | (12 << 12)
892 | reg_to_bits(&rm),
893 );
894 12
895 } else {
896 reg_to_bits(&addr.base)
897 };
898 if addr.offset < 0 || addr.offset > 0xFFB {
899 return Err(synth_core::Error::synthesis(format!(
900 "i64 load/store offset {} out of the A32 imm12 range (0..=4091) — materialize the offset into a register",
901 addr.offset
902 )));
903 }
904 let off = addr.offset as u32;
905 let opc: u32 = if matches!(op, ArmOp::I64Ldr { .. }) {
906 0xE590_0000 // LDR
907 } else {
908 0xE580_0000 // STR
909 };
910 w(&mut b, opc | (base << 16) | (reg_to_bits(rdlo) << 12) | off);
911 w(
912 &mut b,
913 opc | (base << 16) | (reg_to_bits(rdhi) << 12) | (off + 4),
914 );
915 }
916
917 // I64ExtendI32S: rdlo = rn; rdhi = rdlo >> 31 (arithmetic).
918 ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
919 if rdlo != rn {
920 w(
921 &mut b,
922 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
923 );
924 }
925 w(
926 &mut b,
927 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
928 );
929 }
930
931 // I64ExtendI32U: rdlo = rn; rdhi = 0.
932 ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
933 if rdlo != rn {
934 w(
935 &mut b,
936 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rn),
937 );
938 }
939 w(&mut b, 0xE3A0_0000 | (reg_to_bits(rdhi) << 12));
940 }
941
942 // I64Extend8S / I64Extend16S: SXTB/SXTH then sign-fill the high word.
943 ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
944 w(
945 &mut b,
946 0xE6AF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
947 );
948 w(
949 &mut b,
950 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
951 );
952 }
953 ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
954 w(
955 &mut b,
956 0xE6BF_0070 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
957 );
958 w(
959 &mut b,
960 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rdlo),
961 );
962 }
963 ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
964 if rdlo != rnlo {
965 w(
966 &mut b,
967 0xE1A0_0000 | (reg_to_bits(rdlo) << 12) | reg_to_bits(rnlo),
968 );
969 }
970 w(
971 &mut b,
972 0xE1A0_0040 | (reg_to_bits(rdhi) << 12) | (31 << 7) | reg_to_bits(rnlo),
973 );
974 }
975
976 // I32WrapI64: take the low word. When rd == rnlo this is a genuine
977 // no-op (the one case where a NOP word is the correct encoding).
978 ArmOp::I32WrapI64 { rd, rnlo } => {
979 w(
980 &mut b,
981 0xE1A0_0000 | (reg_to_bits(rd) << 12) | reg_to_bits(rnlo),
982 );
983 }
984
985 // I64Add / I64Sub: the classic pair — ADDS lo + ADC hi (SUBS/SBC).
986 // The selector emits these as separate Adds/Adc ops; the fused
987 // variants are verification-constructed, but they encode for real.
988 ArmOp::I64Add {
989 rdlo,
990 rdhi,
991 rnlo,
992 rnhi,
993 rmlo,
994 rmhi,
995 } => {
996 dp_reg(
997 &mut b,
998 0xE090_0000, // ADDS
999 reg_to_bits(rdlo),
1000 reg_to_bits(rnlo),
1001 reg_to_bits(rmlo),
1002 );
1003 dp_reg(
1004 &mut b,
1005 0xE0A0_0000, // ADC
1006 reg_to_bits(rdhi),
1007 reg_to_bits(rnhi),
1008 reg_to_bits(rmhi),
1009 );
1010 }
1011 ArmOp::I64Sub {
1012 rdlo,
1013 rdhi,
1014 rnlo,
1015 rnhi,
1016 rmlo,
1017 rmhi,
1018 } => {
1019 dp_reg(
1020 &mut b,
1021 0xE050_0000, // SUBS
1022 reg_to_bits(rdlo),
1023 reg_to_bits(rnlo),
1024 reg_to_bits(rmlo),
1025 );
1026 dp_reg(
1027 &mut b,
1028 0xE0C0_0000, // SBC
1029 reg_to_bits(rdhi),
1030 reg_to_bits(rnhi),
1031 reg_to_bits(rmhi),
1032 );
1033 }
1034
1035 // I64And / I64Or / I64Xor: two independent word ops.
1036 ArmOp::I64And {
1037 rdlo,
1038 rdhi,
1039 rnlo,
1040 rnhi,
1041 rmlo,
1042 rmhi,
1043 }
1044 | ArmOp::I64Or {
1045 rdlo,
1046 rdhi,
1047 rnlo,
1048 rnhi,
1049 rmlo,
1050 rmhi,
1051 }
1052 | ArmOp::I64Xor {
1053 rdlo,
1054 rdhi,
1055 rnlo,
1056 rnhi,
1057 rmlo,
1058 rmhi,
1059 } => {
1060 let base = match op {
1061 ArmOp::I64And { .. } => 0xE000_0000, // AND
1062 ArmOp::I64Or { .. } => 0xE180_0000, // ORR
1063 _ => 0xE020_0000, // EOR
1064 };
1065 dp_reg(
1066 &mut b,
1067 base,
1068 reg_to_bits(rdlo),
1069 reg_to_bits(rnlo),
1070 reg_to_bits(rmlo),
1071 );
1072 dp_reg(
1073 &mut b,
1074 base,
1075 reg_to_bits(rdhi),
1076 reg_to_bits(rnhi),
1077 reg_to_bits(rmhi),
1078 );
1079 }
1080
1081 // I64DivU: binary long division — A32 transcription of the Thumb-2
1082 // #610/#613 arm (fixed-ABI marshal, zero-divisor trap, 64-round
1083 // shift-subtract core, quotient to R0:R1, result to rd pair).
1084 ArmOp::I64DivU {
1085 rdlo,
1086 rdhi,
1087 rnlo,
1088 rnhi,
1089 rmlo,
1090 rmhi,
1091 elide_zero_guard,
1092 } => {
1093 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1094 // #494 phase 2b: elided only under a certificate-discharged
1095 // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
1096 if !elide_zero_guard {
1097 emit_a32_i64_divisor_zero_trap(&mut b);
1098 }
1099 w(&mut b, 0xE92D_00F0); // PUSH {R4-R7}
1100 for r in 4..8u32 {
1101 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1102 }
1103 div_loop(&mut b, 12); // counter in R12 (encoder scratch)
1104 w(&mut b, 0xE1A0_0004); // MOV R0, R4 (quotient lo)
1105 w(&mut b, 0xE1A0_1005); // MOV R1, R5 (quotient hi)
1106 w(&mut b, 0xE8BD_00F0); // POP {R4-R7}
1107 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1108 }
1109
1110 // I64DivS: sign-extract, unsigned core, conditional negate —
1111 // A32 transcription of the Thumb-2 arm.
1112 ArmOp::I64DivS {
1113 rdlo,
1114 rdhi,
1115 rnlo,
1116 rnhi,
1117 rmlo,
1118 rmhi,
1119 elide_zero_guard,
1120 elide_overflow_guard,
1121 } => {
1122 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1123 // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
1124 // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
1125 // the #633 overflow guard falls ONLY to
1126 // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
1127 // divisor-nonzero fact alone must keep it.
1128 if !elide_zero_guard {
1129 emit_a32_i64_divisor_zero_trap(&mut b);
1130 }
1131 if !elide_overflow_guard {
1132 // #633: INT64_MIN / -1 overflows — trap like the i32 path
1133 // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
1134 emit_a32_i64_divs_overflow_trap(&mut b);
1135 }
1136 w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1137 w(&mut b, 0xE021_9003); // EOR R9, R1, R3 (result sign in MSB)
1138 skip_negate_if_positive(&mut b, 1);
1139 negate64(&mut b, 0, 1);
1140 skip_negate_if_positive(&mut b, 3);
1141 negate64(&mut b, 2, 3);
1142 for r in 4..8u32 {
1143 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1144 }
1145 div_loop(&mut b, 8); // counter in R8 (saved above)
1146 w(&mut b, 0xE1A0_0004); // MOV R0, R4
1147 w(&mut b, 0xE1A0_1005); // MOV R1, R5
1148 skip_negate_if_positive(&mut b, 9);
1149 negate64(&mut b, 0, 1);
1150 w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1151 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1152 }
1153
1154 // I64RemU: same core as I64DivU, returns the remainder (R6:R7).
1155 ArmOp::I64RemU {
1156 rdlo,
1157 rdhi,
1158 rnlo,
1159 rnhi,
1160 rmlo,
1161 rmhi,
1162 elide_zero_guard,
1163 } => {
1164 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1165 if !elide_zero_guard {
1166 emit_a32_i64_divisor_zero_trap(&mut b);
1167 }
1168 w(&mut b, 0xE92D_01F0); // PUSH {R4-R8}
1169 for r in 4..8u32 {
1170 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1171 }
1172 div_loop(&mut b, 8);
1173 w(&mut b, 0xE1A0_0006); // MOV R0, R6 (remainder lo)
1174 w(&mut b, 0xE1A0_1007); // MOV R1, R7 (remainder hi)
1175 w(&mut b, 0xE8BD_01F0); // POP {R4-R8}
1176 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1177 }
1178
1179 // I64RemS: remainder takes the DIVIDEND's sign (WASM semantics).
1180 ArmOp::I64RemS {
1181 rdlo,
1182 rdhi,
1183 rnlo,
1184 rnhi,
1185 rmlo,
1186 rmhi,
1187 elide_zero_guard,
1188 } => {
1189 emit_a32_i64_fixed_abi_entry(&mut b, &[rnlo, rnhi, rmlo, rmhi]);
1190 if !elide_zero_guard {
1191 emit_a32_i64_divisor_zero_trap(&mut b);
1192 }
1193 w(&mut b, 0xE92D_0FF0); // PUSH {R4-R11}
1194 w(&mut b, 0xE1A0_9001); // MOV R9, R1 (dividend sign)
1195 skip_negate_if_positive(&mut b, 1);
1196 negate64(&mut b, 0, 1);
1197 skip_negate_if_positive(&mut b, 3);
1198 negate64(&mut b, 2, 3);
1199 for r in 4..8u32 {
1200 w(&mut b, 0xE3A0_0000 | (r << 12)); // MOV Rr, #0
1201 }
1202 div_loop(&mut b, 8);
1203 w(&mut b, 0xE1A0_0006); // MOV R0, R6
1204 w(&mut b, 0xE1A0_1007); // MOV R1, R7
1205 skip_negate_if_positive(&mut b, 9);
1206 negate64(&mut b, 0, 1);
1207 w(&mut b, 0xE8BD_0FF0); // POP {R4-R11}
1208 emit_a32_i64_fixed_abi_exit(&mut b, rdlo, rdhi)?;
1209 }
1210
1211 // Popcnt (i32): bit-twiddle expansion (no native A32 popcount),
1212 // mirroring the Thumb-2 arm's #1021 register contract: R12 is the
1213 // ONLY scratch. The previous transcription copied the old Thumb
1214 // contract's R11 borrow — but R11 is the linear-memory base on
1215 // this path too, so it inherited the same live miscompile. A32
1216 // has no ThumbExpandImm for 0xXYXYXYXY masks, so instead the
1217 // barrel shifter folds each shift into the mask AND itself
1218 // (`AND R12, R12, rd, LSR #n`), and step 2 recovers `x & C` from
1219 // one term via `x - (((x >> 2) & C) << 2) = x & C` — the second
1220 // temp disappears algebraically. Straight-line, no PUSH/POP,
1221 // nothing to skip on a trap edge.
1222 ArmOp::Popcnt { rd, rm } => {
1223 let rd_b = reg_to_bits(rd);
1224 // Defensive (#1021), same contract as the Thumb-2 arm.
1225 if rd_b >= 11 {
1226 return Err(synth_core::Error::synthesis(
1227 "Popcnt destination must be R0-R10: R11 is the linear-memory \
1228 base and R12 is the expansion's scratch (#1021)",
1229 ));
1230 }
1231 if rd != rm {
1232 w(&mut b, 0xE1A0_0000 | (rd_b << 12) | reg_to_bits(rm)); // MOV rd, rm
1233 }
1234 // x = x - ((x >> 1) & 0x55555555)
1235 movw(&mut b, 12, 0x5555);
1236 movt(&mut b, 12, 0x5555);
1237 dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 1); // AND R12, R12, rd, LSR #1
1238 dp_reg(&mut b, 0xE040_0000, rd_b, rd_b, 12); // SUB rd, rd, R12
1239 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333), one temp:
1240 // R12 = (x >> 2) & C; x - (R12 << 2) = x & C; then + R12.
1241 movw(&mut b, 12, 0x3333);
1242 movt(&mut b, 12, 0x3333);
1243 dp_reg_shift(&mut b, 0xE000_0000, 12, 12, rd_b, LSR, 2); // AND R12, R12, rd, LSR #2
1244 dp_reg_shift(&mut b, 0xE040_0000, rd_b, rd_b, 12, LSL, 2); // SUB rd, rd, R12, LSL #2
1245 dp_reg(&mut b, 0xE080_0000, rd_b, rd_b, 12); // ADD rd, rd, R12
1246 // x = (x + (x >> 4)) & 0x0F0F0F0F
1247 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 4); // ADD rd, rd, rd, LSR #4
1248 movw(&mut b, 12, 0x0F0F);
1249 movt(&mut b, 12, 0x0F0F);
1250 dp_reg(&mut b, 0xE000_0000, rd_b, rd_b, 12); // AND rd, rd, R12
1251 // x += x >> 8; x += x >> 16; x &= 0x3F
1252 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 8);
1253 dp_reg_shift(&mut b, 0xE080_0000, rd_b, rd_b, rd_b, LSR, 16);
1254 w(&mut b, 0xE200_003F | (rd_b << 16) | (rd_b << 12)); // AND rd, rd, #63
1255 }
1256
1257 // I64Popcnt: POPCNT(lo) + POPCNT(hi) — A32 transcription of the
1258 // Thumb-2 arm (R3/R4/R5 saved, mul-based per-word fold, high
1259 // result word rnhi cleared last, mirroring the Thumb contract).
1260 ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
1261 let hi = reg_to_bits(rnhi);
1262 w(&mut b, 0xE92D_0038); // PUSH {R3, R4, R5}
1263 // #632 audit: route rnlo through R12 so a pair living at
1264 // (R3,R4) cannot read a clobbered R4 (sources read before any
1265 // scratch register they could occupy is written).
1266 w(&mut b, 0xE1A0_C000 | reg_to_bits(rnlo)); // MOV R12, rnlo
1267 w(&mut b, 0xE1A0_5000 | hi); // MOV R5, rnhi
1268 w(&mut b, 0xE1A0_400C); // MOV R4, R12
1269 popcnt_word(&mut b, 4, 3);
1270 popcnt_word(&mut b, 5, 3);
1271 // #632: carry the count across the scratch restore in R12 —
1272 // rd is allocator-assigned and can land inside {R3,R4,R5};
1273 // the old `ADD rd, R4, R5` before the POP was destroyed by
1274 // the restore. R12 is never allocatable and never restored.
1275 dp_reg(&mut b, 0xE080_0000, 12, 4, 5); // ADD R12, R4, R5
1276 w(&mut b, 0xE8BD_0038); // POP {R3, R4, R5}
1277 w(&mut b, 0xE1A0_0000 | (reg_to_bits(rd) << 12) | 12); // MOV rd, R12
1278 // #1048: no trailing `MOV rnhi, #0` — the hi-word clear wrote
1279 // the OPERAND's home high register; callers emit it explicitly.
1280 }
1281
1282 _ => return Ok(None),
1283 }
1284 Ok(Some(b))
1285 }
1286
1287 fn encode_arm(&self, op: &ArmOp) -> Result<Vec<u8>> {
1288 // #615: A32 multi-instruction expansions (i64 arithmetic/shift/rotate/
1289 // compare, SetCond/SelectMove, popcnt, ...). These ops were literal
1290 // NOPs on the A32 path — user-reachable via `--target cortex-r5` —
1291 // so the value silently vanished. Mirror of the #594 CallIndirect
1292 // early-return: if the expansion helper covers the op, its bytes are
1293 // the encoding.
1294 if let Some(bytes) = self.encode_arm_expanded(op)? {
1295 return Ok(bytes);
1296 }
1297 // #206: ARM32 register-offset loads/stores. `encode_mem_addr` only
1298 // returns the 12-bit immediate, so the immediate-form arms below
1299 // silently DROP `addr.offset_reg` — a runtime address index vanished,
1300 // turning `ldr rd,[rn,rm,#off]` into `ldr rd,[rn,#off]` (the access went
1301 // to the wrong address). Compute the effective base into IP and re-encode
1302 // against `[ip, #off]`, which is uniform for word/byte/halfword/signed.
1303 if let Some(bytes) = self.encode_arm_reg_offset_mem(op)? {
1304 return Ok(bytes);
1305 }
1306 // #594: call_indirect was encoded as a literal NOP on the A32 path
1307 // (`--target cortex-r5`) — the call never happened and the function
1308 // silently returned garbage. Emit the same three-instruction expansion
1309 // as the Thumb-2 path (R11 = function-pointer table base, R12 scratch):
1310 // MOV r12, idx, LSL #2 ; LDR r12, [r11, r12] ; BLX r12
1311 if let ArmOp::CallIndirect {
1312 table_index_reg,
1313 table_size,
1314 table_byte_offset,
1315 null_check,
1316 type_check,
1317 ..
1318 } = op
1319 {
1320 return Ok(Self::encode_arm_call_indirect(
1321 table_index_reg,
1322 *table_size,
1323 *table_byte_offset,
1324 *null_check,
1325 *type_check,
1326 ));
1327 }
1328 let instr: u32 = match op {
1329 // Data processing instructions
1330 ArmOp::Add { rd, rn, op2 } => {
1331 let rd_bits = reg_to_bits(rd);
1332 let rn_bits = reg_to_bits(rn);
1333 let (op2_bits, i_flag) = encode_operand2(op2)?;
1334
1335 // ADD encoding: cond(4) | 00 | I(1) | 0100 | S(1) | Rn(4) | Rd(4) | operand2(12)
1336 0xE0800000 // condition=always(E), opcode=ADD(0100), S=0
1337 | (i_flag << 25)
1338 | (rn_bits << 16)
1339 | (rd_bits << 12)
1340 | op2_bits
1341 }
1342
1343 ArmOp::Sub { rd, rn, op2 } => {
1344 let rd_bits = reg_to_bits(rd);
1345 let rn_bits = reg_to_bits(rn);
1346 let (op2_bits, i_flag) = encode_operand2(op2)?;
1347
1348 // SUB encoding: opcode=0010
1349 0xE0400000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1350 }
1351
1352 // i64 support: ADDS, ADC, SUBS, SBC for ARM32
1353 ArmOp::Adds { rd, rn, op2 } => {
1354 let rd_bits = reg_to_bits(rd);
1355 let rn_bits = reg_to_bits(rn);
1356 let (op2_bits, i_flag) = encode_operand2(op2)?;
1357
1358 // ADDS encoding: opcode=0100, S=1
1359 0xE0900000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1360 }
1361
1362 ArmOp::Adc { 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 // ADC encoding: opcode=0101
1368 0xE0A00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1369 }
1370
1371 ArmOp::Subs { 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 // SUBS encoding: opcode=0010, S=1
1377 0xE0500000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1378 }
1379
1380 ArmOp::Sbc { 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 // SBC encoding: opcode=0110
1386 0xE0C00000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1387 }
1388
1389 ArmOp::Mul { rd, rn, rm } => {
1390 let rd_bits = reg_to_bits(rd);
1391 let rn_bits = reg_to_bits(rn);
1392 let rm_bits = reg_to_bits(rm);
1393
1394 // MUL encoding: cond(4) | 000000 | A(1) | S(1) | Rd(4) | Rn(4) | Rs(4) | 1001 | Rm(4)
1395 0xE0000090 | (rd_bits << 16) | (rn_bits << 8) | rm_bits
1396 }
1397
1398 ArmOp::Umull { rdlo, rdhi, rn, rm } => {
1399 let rdlo_bits = reg_to_bits(rdlo);
1400 let rdhi_bits = reg_to_bits(rdhi);
1401 let rn_bits = reg_to_bits(rn);
1402 let rm_bits = reg_to_bits(rm);
1403
1404 // UMULL encoding: cond(4) | 0000 1000 | RdHi(4) | RdLo(4) | Rm(4) | 1001 | Rn(4)
1405 0xE0800090 | (rdhi_bits << 16) | (rdlo_bits << 12) | (rm_bits << 8) | rn_bits
1406 }
1407
1408 ArmOp::Sdiv { rd, rn, rm } => {
1409 let rd_bits = reg_to_bits(rd);
1410 let rn_bits = reg_to_bits(rn);
1411 let rm_bits = reg_to_bits(rm);
1412
1413 // SDIV encoding: cond(4) | 01110001 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1414 // ARMv7-M and above
1415 0xE710F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1416 }
1417
1418 ArmOp::Udiv { rd, rn, rm } => {
1419 let rd_bits = reg_to_bits(rd);
1420 let rn_bits = reg_to_bits(rn);
1421 let rm_bits = reg_to_bits(rm);
1422
1423 // UDIV encoding: cond(4) | 01110011 | Rd(4) | 1111 | Rm(4) | 0001 | Rn(4)
1424 // ARMv7-M and above
1425 0xE730F010 | (rd_bits << 16) | (rm_bits << 8) | rn_bits
1426 }
1427
1428 ArmOp::Mls { rd, rn, rm, ra } => {
1429 let rd_bits = reg_to_bits(rd);
1430 let rn_bits = reg_to_bits(rn);
1431 let rm_bits = reg_to_bits(rm);
1432 let ra_bits = reg_to_bits(ra);
1433
1434 // MLS encoding: cond(4) | 00000110 | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1435 // Rd = Ra - (Rn * Rm)
1436 0xE0600090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1437 }
1438
1439 ArmOp::Mla { rd, rn, rm, ra } => {
1440 let rd_bits = reg_to_bits(rd);
1441 let rn_bits = reg_to_bits(rn);
1442 let rm_bits = reg_to_bits(rm);
1443 let ra_bits = reg_to_bits(ra);
1444
1445 // MLA encoding: cond(4) | 0000001 S | Rd(4) | Ra(4) | Rm(4) | 1001 | Rn(4)
1446 // Rd = Ra + (Rn * Rm). Base 0xE0200090 (S=0).
1447 0xE0200090 | (rd_bits << 16) | (ra_bits << 12) | (rm_bits << 8) | rn_bits
1448 }
1449
1450 ArmOp::And { rd, rn, op2 } => {
1451 let rd_bits = reg_to_bits(rd);
1452 let rn_bits = reg_to_bits(rn);
1453 let (op2_bits, i_flag) = encode_operand2(op2)?;
1454
1455 // AND encoding: opcode=0000
1456 0xE0000000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1457 }
1458
1459 ArmOp::Orr { 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 // ORR encoding: opcode=1100
1465 0xE1800000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1466 }
1467
1468 ArmOp::Eor { 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 // EOR encoding: opcode=0001
1474 0xE0200000 | (i_flag << 25) | (rn_bits << 16) | (rd_bits << 12) | op2_bits
1475 }
1476
1477 // Shift instructions
1478 ArmOp::Lsl { rd, rn, shift } => {
1479 let rd_bits = reg_to_bits(rd);
1480 let rn_bits = reg_to_bits(rn);
1481 let shift_bits = *shift & 0x1F;
1482
1483 // LSL encoding: MOV with shift
1484 0xE1A00000 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1485 }
1486
1487 ArmOp::Lsr { 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 // LSR encoding
1493 0xE1A00020 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1494 }
1495
1496 ArmOp::Asr { 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 // ASR encoding
1502 0xE1A00040 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1503 }
1504
1505 ArmOp::Ror { 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 // ROR encoding: MOV with ROR shift
1511 0xE1A00060 | (rd_bits << 12) | (shift_bits << 7) | rn_bits
1512 }
1513
1514 // Register-based shifts (ARM32)
1515 // LSL Rd, Rn, Rm: cond 0001101S 0000 Rd Rs 0001 Rn
1516 ArmOp::LslReg { rd, rn, rm } => {
1517 let rd_bits = reg_to_bits(rd);
1518 let rn_bits = reg_to_bits(rn);
1519 let rm_bits = reg_to_bits(rm);
1520 0xE1A00010 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1521 }
1522 ArmOp::LsrReg { rd, rn, rm } => {
1523 let rd_bits = reg_to_bits(rd);
1524 let rn_bits = reg_to_bits(rn);
1525 let rm_bits = reg_to_bits(rm);
1526 0xE1A00030 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1527 }
1528 ArmOp::AsrReg { rd, rn, rm } => {
1529 let rd_bits = reg_to_bits(rd);
1530 let rn_bits = reg_to_bits(rn);
1531 let rm_bits = reg_to_bits(rm);
1532 0xE1A00050 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1533 }
1534 ArmOp::RorReg { rd, rn, rm } => {
1535 let rd_bits = reg_to_bits(rd);
1536 let rn_bits = reg_to_bits(rn);
1537 let rm_bits = reg_to_bits(rm);
1538 0xE1A00070 | (rd_bits << 12) | (rm_bits << 8) | rn_bits
1539 }
1540
1541 // RSB (Reverse Subtract): Rd = imm - Rn
1542 ArmOp::Rsb { rd, rn, imm } => {
1543 let rd_bits = reg_to_bits(rd);
1544 let rn_bits = reg_to_bits(rn);
1545 // RSB encoding: cond(4) | 00 1 0011 S | Rn(4) | Rd(4) | imm12
1546 // Opcode for RSB = 0011, I=1 (immediate), S=0
1547 //
1548 // #681 class audit: the A32 imm12 is a rotate(4):imm8 modified
1549 // immediate; `*imm & 0xFF` silently encoded a WRONG constant
1550 // for imm > 0xFF (#378 masking class). All current emitters use
1551 // imm 32, so erroring here is byte-identical for real codegen.
1552 if *imm > 0xFF {
1553 return Err(synth_core::Error::synthesis(
1554 "A32 RSB immediate > 0xFF requires a rotated-immediate encoding \
1555 (not supported) — materialize into a register",
1556 ));
1557 }
1558 0xE2600000 | (rn_bits << 16) | (rd_bits << 12) | (*imm & 0xFF)
1559 }
1560
1561 // Bit manipulation instructions
1562 ArmOp::Clz { rd, rm } => {
1563 let rd_bits = reg_to_bits(rd);
1564 let rm_bits = reg_to_bits(rm);
1565
1566 // CLZ encoding: cond(4) | 00010110 | 1111 | Rd(4) | 1111 | 0001 | Rm(4)
1567 // ARMv5T and above
1568 0xE16F0F10 | (rd_bits << 12) | rm_bits
1569 }
1570
1571 ArmOp::Rbit { rd, rm } => {
1572 let rd_bits = reg_to_bits(rd);
1573 let rm_bits = reg_to_bits(rm);
1574
1575 // RBIT encoding: cond(4) | 01101111 | 1111 | Rd(4) | 1111 | 0011 | Rm(4)
1576 // ARMv6T2 and above
1577 0xE6FF0F30 | (rd_bits << 12) | rm_bits
1578 }
1579
1580 ArmOp::Sxtb { rd, rm } => {
1581 let rd_bits = reg_to_bits(rd);
1582 let rm_bits = reg_to_bits(rm);
1583
1584 // SXTB encoding: cond(4) | 01101010 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1585 // ARMv6 and above. rotate=00 for no rotation
1586 0xE6AF0070 | (rd_bits << 12) | rm_bits
1587 }
1588
1589 ArmOp::Sxth { rd, rm } => {
1590 let rd_bits = reg_to_bits(rd);
1591 let rm_bits = reg_to_bits(rm);
1592
1593 // SXTH encoding: cond(4) | 01101011 | 1111 | Rd(4) | rotate(2) | 00 | 0111 | Rm(4)
1594 // ARMv6 and above. rotate=00 for no rotation
1595 0xE6BF0070 | (rd_bits << 12) | rm_bits
1596 }
1597
1598 ArmOp::Uxtb { rd, rm } => {
1599 let rd_bits = reg_to_bits(rd);
1600 let rm_bits = reg_to_bits(rm);
1601 // UXTB encoding: cond | 01101110 1111 Rd rotate 00 0111 Rm (rotate=00)
1602 0xE6EF0070 | (rd_bits << 12) | rm_bits
1603 }
1604
1605 ArmOp::Uxth { rd, rm } => {
1606 let rd_bits = reg_to_bits(rd);
1607 let rm_bits = reg_to_bits(rm);
1608 // UXTH encoding: cond | 01101111 1111 Rd rotate 00 0111 Rm (rotate=00)
1609 0xE6FF0070 | (rd_bits << 12) | rm_bits
1610 }
1611
1612 // Move instructions
1613 ArmOp::Mov { rd, op2 } => {
1614 let rd_bits = reg_to_bits(rd);
1615 let (op2_bits, i_flag) = encode_operand2(op2)?;
1616
1617 // MOV encoding: opcode=1101
1618 0xE1A00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1619 }
1620
1621 ArmOp::Mvn { rd, op2 } => {
1622 let rd_bits = reg_to_bits(rd);
1623 let (op2_bits, i_flag) = encode_operand2(op2)?;
1624
1625 // MVN encoding: opcode=1111
1626 0xE1E00000 | (i_flag << 25) | (rd_bits << 12) | op2_bits
1627 }
1628
1629 // MOVW - Move Wide (ARM32)
1630 // Encoding: cond(4) | 0011 0000 | imm4(4) | Rd(4) | imm12(12)
1631 ArmOp::Movw { rd, imm16 } => {
1632 let rd_bits = reg_to_bits(rd);
1633 let imm4 = ((*imm16 as u32) >> 12) & 0xF;
1634 let imm12 = (*imm16 as u32) & 0xFFF;
1635 0xE3000000 | (imm4 << 16) | (rd_bits << 12) | imm12
1636 }
1637
1638 // MOVT - Move Top (ARM32)
1639 // Encoding: cond(4) | 0011 0100 | imm4(4) | Rd(4) | imm12(12)
1640 ArmOp::Movt { 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 0xE3400000 | (imm4 << 16) | (rd_bits << 12) | imm12
1645 }
1646
1647 // #237: symbol-relative MOVW/MOVT (ARM mode) — addend in place, the
1648 // backend records the MOVW_ABS/MOVT_ABS relocation against `symbol`.
1649 ArmOp::MovwSym { rd, addend, .. } => {
1650 let rd_bits = reg_to_bits(rd);
1651 let v = (*addend as u32) & 0xffff;
1652 0xE3000000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1653 }
1654 ArmOp::MovtSym { rd, addend, .. } => {
1655 let rd_bits = reg_to_bits(rd);
1656 let v = ((*addend as u32) >> 16) & 0xffff;
1657 0xE3400000 | (((v >> 12) & 0xF) << 16) | (rd_bits << 12) | (v & 0xFFF)
1658 }
1659
1660 // #345: LdrSym is the Thumb-2 literal-pool address load. A32 mode is
1661 // not used for relocatable native-pointer objects; fail loudly rather
1662 // than miscompile if it is ever reached here.
1663 ArmOp::LdrSym { .. } => {
1664 return Err(synth_core::Error::synthesis(
1665 "LdrSym (literal-pool address load) is Thumb-2-only",
1666 ));
1667 }
1668
1669 // Compare
1670 ArmOp::Cmp { rn, op2 } => {
1671 let rn_bits = reg_to_bits(rn);
1672 let (op2_bits, i_flag) = encode_operand2(op2)?;
1673
1674 // CMP encoding: opcode=1010, S=1
1675 0xE1500000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1676 }
1677
1678 // Compare Negative (CMN) - computes Rn + op2 and sets flags
1679 ArmOp::Cmn { rn, op2 } => {
1680 let rn_bits = reg_to_bits(rn);
1681 let (op2_bits, i_flag) = encode_operand2(op2)?;
1682
1683 // CMN encoding: opcode=1011, S=1
1684 0xE1700000 | (i_flag << 25) | (rn_bits << 16) | op2_bits
1685 }
1686
1687 // Load/Store
1688 ArmOp::Ldr { rd, addr } => {
1689 let rd_bits = reg_to_bits(rd);
1690 let (base_bits, offset_bits) = encode_mem_addr(addr);
1691
1692 // LDR encoding: cond(4) | 01 | I(1) | P(1) | U(1) | B(1) | W(1) | L(1) | Rn(4) | Rd(4) | offset(12)
1693 // P=1 (pre-indexed), U=1 (add offset), L=1 (load)
1694 0xE5900000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1695 }
1696
1697 ArmOp::Str { rd, addr } => {
1698 let rd_bits = reg_to_bits(rd);
1699 let (base_bits, offset_bits) = encode_mem_addr(addr);
1700
1701 // STR encoding: L=0 (store)
1702 0xE5800000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1703 }
1704
1705 // Sub-word loads (ARM32 encoding)
1706 ArmOp::Ldrb { rd, addr } => {
1707 let rd_bits = reg_to_bits(rd);
1708 let (base_bits, offset_bits) = encode_mem_addr(addr);
1709 // LDRB: LDR with B=1 (byte): cond|01|I|P|U|1|W|L|Rn|Rd|offset
1710 0xE5D00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1711 }
1712
1713 ArmOp::Ldrsb { rd, addr } => {
1714 let rd_bits = reg_to_bits(rd);
1715 let (base_bits, offset_bits) = encode_mem_addr(addr);
1716 // LDRSB (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1101|imm4L
1717 // Simplified with immediate offset
1718 let offset_val = offset_bits & 0xFF;
1719 let imm4h = (offset_val >> 4) & 0xF;
1720 let imm4l = offset_val & 0xF;
1721 0xE1D000D0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1722 }
1723
1724 ArmOp::Ldrh { rd, addr } => {
1725 let rd_bits = reg_to_bits(rd);
1726 let (base_bits, offset_bits) = encode_mem_addr(addr);
1727 // LDRH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1011|imm4L
1728 let offset_val = offset_bits & 0xFF;
1729 let imm4h = (offset_val >> 4) & 0xF;
1730 let imm4l = offset_val & 0xF;
1731 0xE1D000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1732 }
1733
1734 ArmOp::Ldrsh { rd, addr } => {
1735 let rd_bits = reg_to_bits(rd);
1736 let (base_bits, offset_bits) = encode_mem_addr(addr);
1737 // LDRSH (misc load): cond|000|P|U|1|W|1|Rn|Rd|imm4H|1111|imm4L
1738 let offset_val = offset_bits & 0xFF;
1739 let imm4h = (offset_val >> 4) & 0xF;
1740 let imm4l = offset_val & 0xF;
1741 0xE1D000F0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1742 }
1743
1744 // Sub-word stores (ARM32 encoding)
1745 ArmOp::Strb { rd, addr } => {
1746 let rd_bits = reg_to_bits(rd);
1747 let (base_bits, offset_bits) = encode_mem_addr(addr);
1748 // STRB: STR with B=1 (byte): cond|01|I|P|U|1|W|0|Rn|Rd|offset
1749 0xE5C00000 | (base_bits << 16) | (rd_bits << 12) | offset_bits
1750 }
1751
1752 ArmOp::Strh { rd, addr } => {
1753 let rd_bits = reg_to_bits(rd);
1754 let (base_bits, offset_bits) = encode_mem_addr(addr);
1755 // STRH (misc store): cond|000|P|U|1|W|0|Rn|Rd|imm4H|1011|imm4L
1756 let offset_val = offset_bits & 0xFF;
1757 let imm4h = (offset_val >> 4) & 0xF;
1758 let imm4l = offset_val & 0xF;
1759 0xE1C000B0 | (base_bits << 16) | (rd_bits << 12) | (imm4h << 8) | imm4l
1760 }
1761
1762 // Memory management (ARM32 encoding)
1763 ArmOp::MemorySize { rd } => {
1764 let rd_bits = reg_to_bits(rd);
1765 // MOV rd, R10, LSR #16 (memory size in bytes / 65536 = pages)
1766 // cond|000|1101|S|0000|Rd|shift5|type|0|Rm
1767 // LSR #16: shift5=10000, type=01
1768 0xE1A00820 | (rd_bits << 12) | 0x0A // Rm=R10, shift=16, LSR
1769 }
1770
1771 ArmOp::MemoryGrow { rd, .. } => {
1772 let rd_bits = reg_to_bits(rd);
1773 // On embedded, always fail: MOV rd, #-1
1774 0xE3E00000 | (rd_bits << 12) // MVN rd, #0 = MOV rd, #-1
1775 }
1776
1777 // Label pseudo-instruction: emits no machine code
1778 ArmOp::Label { .. } => {
1779 return Ok(Vec::new());
1780 }
1781
1782 // Branch instructions
1783 ArmOp::B { label: _ } => {
1784 // B encoding: cond(4) | 1010 | offset(24)
1785 // Simplified: branch to offset 0 (will be patched by linker/resolver)
1786 0xEA000000
1787 }
1788
1789 // Conditional branch to label (generic)
1790 ArmOp::Bcc { cond, label: _ } => {
1791 use synth_synthesis::Condition;
1792 let cond_bits: u32 = match cond {
1793 Condition::EQ => 0x0,
1794 Condition::NE => 0x1,
1795 Condition::HS => 0x2,
1796 Condition::LO => 0x3,
1797 Condition::HI => 0x8,
1798 Condition::LS => 0x9,
1799 Condition::GE => 0xA,
1800 Condition::LT => 0xB,
1801 Condition::GT => 0xC,
1802 Condition::LE => 0xD,
1803 };
1804 // B<cond> with offset 0 (will be patched)
1805 (cond_bits << 28) | 0x0A000000
1806 }
1807
1808 // BHS (Branch if Higher or Same) - used for bounds checking
1809 ArmOp::Bhs { label: _ } => {
1810 // BHS encoding: cond(2=HS) | 1010 | offset(24)
1811 0x2A000000 // BHS with offset 0
1812 }
1813
1814 // BLO (Branch if Lower) - complementary to BHS
1815 ArmOp::Blo { label: _ } => {
1816 // BLO encoding: cond(3=LO) | 1010 | offset(24)
1817 0x3A000000 // BLO with offset 0
1818 }
1819
1820 // Branch with numeric offset (in instructions)
1821 // ARM32 B instruction: offset is in instructions, stored as words
1822 // The offset is relative to PC+8 (due to ARM pipeline)
1823 ArmOp::BOffset { offset } => {
1824 // B encoding: cond(4) | 1010 | offset(24)
1825 // Offset is signed, in words (4-byte units)
1826 // ARM adds PC+8 to the offset, so we need to adjust:
1827 // target = PC + 8 + (offset * 4)
1828 // For backward branch of N instructions: offset = -(N + 2)
1829 // wrapping_sub keeps the encoder total under fuzzing (#186): an
1830 // extreme i32::MIN offset would otherwise overflow-panic; for any
1831 // real branch offset this is identical to `- 2`.
1832 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1833 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1834 0xEA000000 | offset_bits
1835 }
1836
1837 // Conditional branch with numeric offset
1838 ArmOp::BCondOffset { cond, offset } => {
1839 use synth_synthesis::Condition;
1840 let cond_bits: u32 = match cond {
1841 Condition::EQ => 0x0,
1842 Condition::NE => 0x1,
1843 Condition::HS => 0x2,
1844 Condition::LO => 0x3,
1845 Condition::HI => 0x8,
1846 Condition::LS => 0x9,
1847 Condition::GE => 0xA,
1848 Condition::LT => 0xB,
1849 Condition::GT => 0xC,
1850 Condition::LE => 0xD,
1851 };
1852 // B<cond> encoding: cond(4) | 1010 | offset(24)
1853 // wrapping_sub: total under fuzzing (#186), identical for real offsets.
1854 let adjusted_offset = offset.wrapping_sub(2); // Account for PC+8
1855 let offset_bits = (adjusted_offset as u32) & 0x00FFFFFF;
1856 (cond_bits << 28) | 0x0A000000 | offset_bits
1857 }
1858
1859 ArmOp::Bl { label: _ } => {
1860 // BL encoding: cond(4) | 1011 | offset(24)
1861 0xEB000000
1862 }
1863
1864 ArmOp::Bx { rm } => {
1865 let rm_bits = reg_to_bits(rm);
1866
1867 // BX encoding: cond(4) | 000100101111111111110001 | Rm(4)
1868 0xE12FFF10 | rm_bits
1869 }
1870
1871 ArmOp::Blx { rm } => {
1872 let rm_bits = reg_to_bits(rm);
1873
1874 // BLX (register) encoding: cond(4) | 000100101111111111110011 | Rm(4)
1875 0xE12FFF30 | rm_bits
1876 }
1877
1878 ArmOp::Push { regs } => {
1879 // STMDB SP!, {regs} encoding: cond(4) | 100100 | 10 | 1101 | register_list(16)
1880 let mut reg_list: u32 = 0;
1881 for r in regs {
1882 reg_list |= 1 << reg_to_bits(r);
1883 }
1884 0xE92D0000 | reg_list
1885 }
1886
1887 ArmOp::Pop { regs } => {
1888 // LDMIA SP!, {regs} encoding: cond(4) | 100010 | 11 | 1101 | register_list(16)
1889 let mut reg_list: u32 = 0;
1890 for r in regs {
1891 reg_list |= 1 << reg_to_bits(r);
1892 }
1893 0xE8BD0000 | reg_list
1894 }
1895
1896 ArmOp::Nop => {
1897 // NOP encoding: MOV R0, R0
1898 0xE1A00000
1899 }
1900
1901 ArmOp::Udf { imm } => {
1902 // UDF (Undefined) encoding in ARM: 0xE7F000F0 | (imm12_hi << 8) | imm4_lo
1903 // We only use imm8, so split into imm4_hi and imm4_lo
1904 let imm8 = *imm as u32;
1905 0xE7F000F0 | ((imm8 & 0xF0) << 4) | (imm8 & 0x0F)
1906 }
1907
1908 // #615: handled by the `encode_arm_expanded` early return at the
1909 // top of this function — a real MOV{cond}/MOV pair now, never a
1910 // silent NOP again.
1911 ArmOp::Popcnt { .. } | ArmOp::SetCond { .. } | ArmOp::SelectMove { .. } => {
1912 unreachable!("handled by encode_arm_expanded (#615)")
1913 }
1914
1915 // Verification-only pseudo-ops: `synth-verify`'s ArmSemantics
1916 // models these, but NO codegen path constructs them (the selector
1917 // lowers select/locals/globals/br_table/call to real instruction
1918 // sequences before the encoder). Encoding one as a NOP silently
1919 // dropped the operation (#615 class); a typed Err keeps the
1920 // encoder total (Ok-or-Err, the `encoder_no_panic` contract)
1921 // while making any future reachability LOUD.
1922 ArmOp::Select { .. }
1923 | ArmOp::LocalGet { .. }
1924 | ArmOp::LocalSet { .. }
1925 | ArmOp::LocalTee { .. }
1926 | ArmOp::GlobalGet { .. }
1927 | ArmOp::GlobalSet { .. }
1928 | ArmOp::BrTable { .. }
1929 | ArmOp::Call { .. } => {
1930 return Err(synth_core::Error::synthesis(format!(
1931 "verification-only pseudo-op {op:?} reached the A32 encoder — \
1932 codegen lowers it before encoding; refusing to emit a silent NOP (#615)"
1933 )));
1934 }
1935
1936 // #594: CallIndirect is expanded to a real multi-instruction
1937 // sequence by the early return at the top of this function —
1938 // it must NEVER fall through to a silent NOP again.
1939 ArmOp::CallIndirect { .. } => {
1940 unreachable!("CallIndirect handled by encode_arm_call_indirect (#594)")
1941 }
1942
1943 // #615: every i64 op (and I32WrapI64) is expanded to a real A32
1944 // multi-instruction sequence by `encode_arm_expanded` — the
1945 // "encode as NOP for now" era ended with the value silently
1946 // vanishing on `--target cortex-r5`.
1947 ArmOp::I64Add { .. }
1948 | ArmOp::I64Sub { .. }
1949 | ArmOp::I64DivS { .. }
1950 | ArmOp::I64DivU { .. }
1951 | ArmOp::I64RemS { .. }
1952 | ArmOp::I64RemU { .. }
1953 | ArmOp::I64Clz { .. }
1954 | ArmOp::I64Ctz { .. }
1955 | ArmOp::I64Popcnt { .. }
1956 | ArmOp::I64And { .. }
1957 | ArmOp::I64Or { .. }
1958 | ArmOp::I64Xor { .. }
1959 | ArmOp::I64Eqz { .. }
1960 | ArmOp::I64Eq { .. }
1961 | ArmOp::I64Ne { .. }
1962 | ArmOp::I64LtS { .. }
1963 | ArmOp::I64LtU { .. }
1964 | ArmOp::I64LeS { .. }
1965 | ArmOp::I64LeU { .. }
1966 | ArmOp::I64GtS { .. }
1967 | ArmOp::I64GtU { .. }
1968 | ArmOp::I64GeS { .. }
1969 | ArmOp::I64GeU { .. }
1970 | ArmOp::I64Const { .. }
1971 | ArmOp::I64Ldr { .. }
1972 | ArmOp::I64Str { .. }
1973 | ArmOp::I64ExtendI32S { .. }
1974 | ArmOp::I64ExtendI32U { .. }
1975 | ArmOp::I64Extend8S { .. }
1976 | ArmOp::I64Extend16S { .. }
1977 | ArmOp::I64Extend32S { .. }
1978 | ArmOp::I32WrapI64 { .. } => {
1979 unreachable!("handled by encode_arm_expanded (#615)")
1980 }
1981
1982 // f32 VFP single-precision instructions
1983 ArmOp::F32Add { sd, sn, sm } => encode_vfp_3reg(0xEE300A00, sd, sn, sm)?,
1984 ArmOp::F32Sub { sd, sn, sm } => encode_vfp_3reg(0xEE300A40, sd, sn, sm)?,
1985 ArmOp::F32Mul { sd, sn, sm } => encode_vfp_3reg(0xEE200A00, sd, sn, sm)?,
1986 ArmOp::F32Div { sd, sn, sm } => encode_vfp_3reg(0xEE800A00, sd, sn, sm)?,
1987 ArmOp::F32Abs { sd, sm } => encode_vfp_2reg(0xEEB00AC0, sd, sm)?,
1988 ArmOp::F32Neg { sd, sm } => encode_vfp_2reg(0xEEB10A40, sd, sm)?,
1989 ArmOp::F32Sqrt { sd, sm } => encode_vfp_2reg(0xEEB10AC0, sd, sm)?,
1990
1991 // f32 pseudo-ops — multi-instruction sequences
1992 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
1993 ArmOp::F32Ceil { sd, sm } => {
1994 return self.encode_arm_f32_rounding(sd, sm, 0b01); // Round toward +Inf
1995 }
1996 ArmOp::F32Floor { sd, sm } => {
1997 return self.encode_arm_f32_rounding(sd, sm, 0b10); // Round toward -Inf
1998 }
1999 ArmOp::F32Trunc { sd, sm } => {
2000 return self.encode_arm_f32_rounding(sd, sm, 0b11); // VCVT toward zero
2001 }
2002 ArmOp::F32Nearest { sd, sm } => {
2003 return self.encode_arm_f32_rounding(sd, sm, 0b00); // VCVT to nearest
2004 }
2005 ArmOp::F32Min { sd, sn, sm } => {
2006 return self.encode_arm_f32_minmax(sd, sn, sm, true);
2007 }
2008 ArmOp::F32Max { sd, sn, sm } => {
2009 return self.encode_arm_f32_minmax(sd, sn, sm, false);
2010 }
2011 ArmOp::F32Copysign { sd, sn, sm } => {
2012 return self.encode_arm_f32_copysign(sd, sn, sm);
2013 }
2014
2015 // f32 comparisons — multi-instruction: VCMP + VMRS + conditional MOV
2016 ArmOp::F32Eq { rd, sn, sm } => {
2017 return self.encode_arm_f32_compare(rd, sn, sm, 0x0); // EQ
2018 }
2019 ArmOp::F32Ne { rd, sn, sm } => {
2020 return self.encode_arm_f32_compare(rd, sn, sm, 0x1); // NE
2021 }
2022 ArmOp::F32Lt { rd, sn, sm } => {
2023 return self.encode_arm_f32_compare(rd, sn, sm, 0x4); // MI (less than)
2024 }
2025 ArmOp::F32Le { rd, sn, sm } => {
2026 return self.encode_arm_f32_compare(rd, sn, sm, 0x9); // LS (less or same)
2027 }
2028 ArmOp::F32Gt { rd, sn, sm } => {
2029 return self.encode_arm_f32_compare(rd, sn, sm, 0xC); // GT
2030 }
2031 ArmOp::F32Ge { rd, sn, sm } => {
2032 return self.encode_arm_f32_compare(rd, sn, sm, 0xA); // GE
2033 }
2034
2035 // f32 const — multi-instruction: MOVW + MOVT + VMOV
2036 ArmOp::F32Const { sd, value } => {
2037 return self.encode_arm_f32_const(sd, *value);
2038 }
2039
2040 ArmOp::F32Load { sd, addr } => encode_vfp_ldst(0xED900A00, sd, addr)?,
2041 ArmOp::F32Store { sd, addr } => encode_vfp_ldst(0xED800A00, sd, addr)?,
2042
2043 // f32 conversions — multi-instruction sequences
2044 ArmOp::F32ConvertI32S { sd, rm } => {
2045 return self.encode_arm_f32_convert_i32(sd, rm, true);
2046 }
2047 ArmOp::F32ConvertI32U { sd, rm } => {
2048 return self.encode_arm_f32_convert_i32(sd, rm, false);
2049 }
2050 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
2051 return Err(synth_core::Error::synthesis(
2052 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2053 ));
2054 }
2055 ArmOp::F32ReinterpretI32 { sd, rm } => encode_vmov_core_sreg(true, sd, rm)?,
2056 ArmOp::I32ReinterpretF32 { rd, sm } => encode_vmov_core_sreg(false, sm, rd)?,
2057 ArmOp::I32TruncF32S { rd, sm } => {
2058 return self.encode_arm_i32_trunc_f32(rd, sm, true);
2059 }
2060 ArmOp::I32TruncF32U { rd, sm } => {
2061 return self.encode_arm_i32_trunc_f32(rd, sm, false);
2062 }
2063
2064 // f64 VFP double-precision instructions (ARM32)
2065 // F64 arithmetic: same as F32 but with sz=1 (bit 8 = 1, cp11 = 0xB)
2066 ArmOp::F64Add { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B00, dd, dn, dm)?,
2067 ArmOp::F64Sub { dd, dn, dm } => encode_vfp_3reg_f64(0xEE300B40, dd, dn, dm)?,
2068 ArmOp::F64Mul { dd, dn, dm } => encode_vfp_3reg_f64(0xEE200B00, dd, dn, dm)?,
2069 ArmOp::F64Div { dd, dn, dm } => encode_vfp_3reg_f64(0xEE800B00, dd, dn, dm)?,
2070 ArmOp::F64Abs { dd, dm } => encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?,
2071 ArmOp::F64Neg { dd, dm } => encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?,
2072 ArmOp::F64Sqrt { dd, dm } => encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?,
2073
2074 // f64 pseudo-ops
2075 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
2076 ArmOp::F64Ceil { dd, dm } => {
2077 return self.encode_arm_f64_rounding(dd, dm, 0b01);
2078 }
2079 ArmOp::F64Floor { dd, dm } => {
2080 return self.encode_arm_f64_rounding(dd, dm, 0b10);
2081 }
2082 ArmOp::F64Trunc { dd, dm } => {
2083 return self.encode_arm_f64_rounding(dd, dm, 0b11);
2084 }
2085 ArmOp::F64Nearest { dd, dm } => {
2086 return self.encode_arm_f64_rounding(dd, dm, 0b00);
2087 }
2088 ArmOp::F64Min { dd, dn, dm } => {
2089 return self.encode_arm_f64_minmax(dd, dn, dm, true);
2090 }
2091 ArmOp::F64Max { dd, dn, dm } => {
2092 return self.encode_arm_f64_minmax(dd, dn, dm, false);
2093 }
2094 ArmOp::F64Copysign { dd, dn, dm } => {
2095 return self.encode_arm_f64_copysign(dd, dn, dm);
2096 }
2097
2098 // f64 comparisons
2099 ArmOp::F64Eq { rd, dn, dm } => {
2100 return self.encode_arm_f64_compare(rd, dn, dm, 0x0);
2101 }
2102 ArmOp::F64Ne { rd, dn, dm } => {
2103 return self.encode_arm_f64_compare(rd, dn, dm, 0x1);
2104 }
2105 ArmOp::F64Lt { rd, dn, dm } => {
2106 return self.encode_arm_f64_compare(rd, dn, dm, 0x4);
2107 }
2108 ArmOp::F64Le { rd, dn, dm } => {
2109 return self.encode_arm_f64_compare(rd, dn, dm, 0x9);
2110 }
2111 ArmOp::F64Gt { rd, dn, dm } => {
2112 return self.encode_arm_f64_compare(rd, dn, dm, 0xC);
2113 }
2114 ArmOp::F64Ge { rd, dn, dm } => {
2115 return self.encode_arm_f64_compare(rd, dn, dm, 0xA);
2116 }
2117
2118 ArmOp::F64Const { dd, value } => {
2119 return self.encode_arm_f64_const(dd, *value);
2120 }
2121
2122 ArmOp::F64Load { dd, addr } => encode_vfp_ldst_f64(0xED900B00, dd, addr)?,
2123 ArmOp::F64Store { dd, addr } => encode_vfp_ldst_f64(0xED800B00, dd, addr)?,
2124
2125 ArmOp::F64ConvertI32S { dd, rm } => {
2126 return self.encode_arm_f64_convert_i32(dd, rm, true);
2127 }
2128 ArmOp::F64ConvertI32U { dd, rm } => {
2129 return self.encode_arm_f64_convert_i32(dd, rm, false);
2130 }
2131 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
2132 return Err(synth_core::Error::synthesis(
2133 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
2134 ));
2135 }
2136 ArmOp::F64PromoteF32 { dd, sm } => {
2137 return self.encode_arm_f64_promote_f32(dd, sm);
2138 }
2139 // GI-FPU-002 (#369): no synth A32 target carries an FPU (cortex-r5
2140 // has none — the selector declines every float op there), so the
2141 // A32 encoder refuses loudly instead of shipping an untested
2142 // encoding (#615: never a silent wrong byte).
2143 ArmOp::F32DemoteF64 { .. } => {
2144 return Err(synth_core::Error::synthesis(
2145 "F32DemoteF64 has no A32 encoding (no A32 target has an FPU)",
2146 ));
2147 }
2148 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => {
2149 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?
2150 }
2151 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => {
2152 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?
2153 }
2154 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
2155 return Err(synth_core::Error::synthesis(
2156 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
2157 ));
2158 }
2159 ArmOp::I32TruncF64S { rd, dm } => {
2160 return self.encode_arm_i32_trunc_f64(rd, dm, true);
2161 }
2162 ArmOp::I32TruncF64U { rd, dm } => {
2163 return self.encode_arm_i32_trunc_f64(rd, dm, false);
2164 }
2165 // #615: multi-instruction i64 sequences — expanded to real A32 by
2166 // `encode_arm_expanded`, no longer "Thumb-2 only" NOPs.
2167 ArmOp::I64SetCond { .. }
2168 | ArmOp::I64SetCondZ { .. }
2169 | ArmOp::I64Mul { .. }
2170 | ArmOp::I64Shl { .. }
2171 | ArmOp::I64ShrS { .. }
2172 | ArmOp::I64ShrU { .. }
2173 | ArmOp::I64Rotl { .. }
2174 | ArmOp::I64Rotr { .. } => {
2175 unreachable!("handled by encode_arm_expanded (#615)")
2176 }
2177
2178 // MVE instructions — Thumb-2 only (Cortex-M55 is always Thumb-2)
2179 ArmOp::MveLoad { .. }
2180 | ArmOp::MveStore { .. }
2181 | ArmOp::MveConst { .. }
2182 | ArmOp::MveAnd { .. }
2183 | ArmOp::MveOrr { .. }
2184 | ArmOp::MveEor { .. }
2185 | ArmOp::MveMvn { .. }
2186 | ArmOp::MveBic { .. }
2187 | ArmOp::MveAddI { .. }
2188 | ArmOp::MveSubI { .. }
2189 | ArmOp::MveMulI { .. }
2190 | ArmOp::MveNegI { .. }
2191 | ArmOp::MveCmpEqI { .. }
2192 | ArmOp::MveCmpNeI { .. }
2193 | ArmOp::MveCmpLtS { .. }
2194 | ArmOp::MveCmpLtU { .. }
2195 | ArmOp::MveCmpGtS { .. }
2196 | ArmOp::MveCmpGtU { .. }
2197 | ArmOp::MveCmpLeS { .. }
2198 | ArmOp::MveCmpLeU { .. }
2199 | ArmOp::MveCmpGeS { .. }
2200 | ArmOp::MveCmpGeU { .. }
2201 | ArmOp::MveDup { .. }
2202 | ArmOp::MveExtractLane { .. }
2203 | ArmOp::MveInsertLane { .. }
2204 | ArmOp::MveAddF32 { .. }
2205 | ArmOp::MveSubF32 { .. }
2206 | ArmOp::MveMulF32 { .. }
2207 | ArmOp::MveNegF32 { .. }
2208 | ArmOp::MveAbsF32 { .. }
2209 | ArmOp::MveCmpEqF32 { .. }
2210 | ArmOp::MveCmpNeF32 { .. }
2211 | ArmOp::MveCmpLtF32 { .. }
2212 | ArmOp::MveCmpLeF32 { .. }
2213 | ArmOp::MveCmpGtF32 { .. }
2214 | ArmOp::MveCmpGeF32 { .. }
2215 | ArmOp::MveDupF32 { .. }
2216 | ArmOp::MveExtractLaneF32 { .. }
2217 | ArmOp::MveReplaceLaneF32 { .. }
2218 | ArmOp::MveDivF32 { .. }
2219 | ArmOp::MveSqrtF32 { .. } => {
2220 // MVE (Helium) is a Thumb-2-only extension (Cortex-M55); there
2221 // is no A32 encoding. The selector only emits MVE ops for
2222 // Thumb targets — a NOP here silently dropped the vector op
2223 // if that invariant ever broke (#615 class). Err keeps the
2224 // encoder total and the failure loud.
2225 return Err(synth_core::Error::synthesis(format!(
2226 "MVE op {op:?} has no A32 (ARM-mode) encoding — MVE is Thumb-2 only (#615)"
2227 )));
2228 }
2229 };
2230
2231 // ARM32 instructions are little-endian
2232 Ok(instr.to_le_bytes().to_vec())
2233 }
2234
2235 // === ARM32 VFP multi-instruction helpers ===
2236
2237 /// Encode F32 comparison as ARM32: VCMP.F32 + VMRS + MOV rd,#0 + MOVcond rd,#1
2238 fn encode_arm_f32_compare(
2239 &self,
2240 rd: &Reg,
2241 sn: &VfpReg,
2242 sm: &VfpReg,
2243 cond_code: u32,
2244 ) -> Result<Vec<u8>> {
2245 let mut bytes = Vec::new();
2246
2247 // VCMP.F32 Sn, Sm: 0xEEB40A40 with Sn in Vd position, Sm in Vm position
2248 let sn_num = vfp_sreg_to_num(sn)?;
2249 let sm_num = vfp_sreg_to_num(sm)?;
2250 let (vd, d) = encode_sreg(sn_num);
2251 let (vm, m) = encode_sreg(sm_num);
2252 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2253 bytes.extend_from_slice(&vcmp.to_le_bytes());
2254
2255 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10
2256 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2257
2258 // MOV rd, #0: 0xE3A0_0000 | (rd << 12)
2259 let rd_bits = reg_to_bits(rd);
2260 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2261 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2262
2263 // MOVcond rd, #1: cond(4) | 0011 1010 0000 rd(4) 0000 0000 0001
2264 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2265 bytes.extend_from_slice(&mov_one.to_le_bytes());
2266
2267 Ok(bytes)
2268 }
2269
2270 /// Encode F32 constant load as ARM32: MOVW Rt,#lo16 + MOVT Rt,#hi16 + VMOV Sd,Rt
2271 fn encode_arm_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
2272 let mut bytes = Vec::new();
2273 let bits = value.to_bits();
2274
2275 // Use R12 as temp register for constant loading
2276 let rt: u32 = 12; // R12/IP
2277
2278 // MOVW R12, #lo16: 0xE300_C000 | (imm4 << 16) | imm12
2279 let lo16 = bits & 0xFFFF;
2280 let movw = 0xE3000000 | (rt << 12) | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2281 bytes.extend_from_slice(&movw.to_le_bytes());
2282
2283 // MOVT R12, #hi16: 0xE340_C000 | (imm4 << 16) | imm12
2284 let hi16 = (bits >> 16) & 0xFFFF;
2285 let movt = 0xE3400000 | (rt << 12) | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2286 bytes.extend_from_slice(&movt.to_le_bytes());
2287
2288 // VMOV Sd, R12
2289 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
2290 bytes.extend_from_slice(&vmov.to_le_bytes());
2291
2292 Ok(bytes)
2293 }
2294
2295 /// Encode VMOV + VCVT.F32.S32/U32 as ARM32
2296 fn encode_arm_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2297 let mut bytes = Vec::new();
2298
2299 // VMOV Sd, Rm — move integer to VFP register
2300 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
2301 bytes.extend_from_slice(&vmov.to_le_bytes());
2302
2303 // VCVT.F32.S32 Sd, Sd (signed) or VCVT.F32.U32 Sd, Sd (unsigned).
2304 // The "op" bit (bit 7) selects signedness: 1 = signed (S32), 0 =
2305 // unsigned (U32). So signed = 0xEEB80AC0, unsigned = 0xEEB80A40 —
2306 // objdump confirms 0xEEB80A40 decodes to `vcvt.f32.u32` (GI-FPU-002:
2307 // the two were previously swapped, silently making `convert_i32_s`
2308 // an unsigned conversion).
2309 let sd_num = vfp_sreg_to_num(sd)?;
2310 let (vd, d) = encode_sreg(sd_num);
2311 let (vm, m) = encode_sreg(sd_num); // same register as source
2312 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
2313 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2314 bytes.extend_from_slice(&vcvt.to_le_bytes());
2315
2316 Ok(bytes)
2317 }
2318
2319 /// Encode F32 rounding pseudo-op as ARM32 via VCVT to integer and back.
2320 /// mode: 0b00=nearest, 0b01=floor(-Inf), 0b10=ceil(+Inf), 0b11=trunc(zero)
2321 /// Strategy: VCVT.S32.F32 Sd, Sm (toward zero), then VCVT.F32.S32 Sd, Sd
2322 /// For ceil/floor/nearest, we use VCVTR (round toward mode) + convert back.
2323 /// Simplified: convert to int (toward zero for trunc) then back to float.
2324 /// Encode F32 rounding as ARM32.
2325 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2326 ///
2327 /// For trunc (mode=0b11): uses VCVTR.S32.F32 (always rounds toward zero).
2328 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant
2329 /// which honours FPSCR rmode), then restores FPSCR.
2330 fn encode_arm_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2331 let mut bytes = Vec::new();
2332 let sm_num = vfp_sreg_to_num(sm)?;
2333 let sd_num = vfp_sreg_to_num(sd)?;
2334 let (vd_s, d_s) = encode_sreg(sd_num);
2335 let (vm_s, m_s) = encode_sreg(sm_num);
2336
2337 if mode == 0b11 {
2338 // Trunc (toward zero): VCVTR.S32.F32 — the "R" variant always truncates.
2339 // 0xEEBD0AC0: bit[7]=1 => round toward zero regardless of FPSCR
2340 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2341 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2342 } else {
2343 // ceil/floor/nearest: manipulate FPSCR rounding mode
2344 let rt: u32 = 12; // R12/IP as temp
2345
2346 // VMRS R12, FPSCR
2347 let vmrs = 0xEEF10A10 | (rt << 12);
2348 bytes.extend_from_slice(&vmrs.to_le_bytes());
2349
2350 // BIC R12, R12, #(3 << 22) — clear RMode bits [23:22]
2351 // 3<<22 = 0x00C00000. ARM rotated imm: 0x03 ror 10 (rotation=5, imm8=0x03)
2352 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2353 bytes.extend_from_slice(&bic.to_le_bytes());
2354
2355 // ORR R12, R12, #(mode << 22) — set desired rounding mode
2356 if mode != 0 {
2357 // mode<<22: rotation=5, imm8=mode
2358 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2359 bytes.extend_from_slice(&orr.to_le_bytes());
2360 }
2361
2362 // VMSR FPSCR, R12
2363 let vmsr = 0xEEE10A10 | (rt << 12);
2364 bytes.extend_from_slice(&vmsr.to_le_bytes());
2365
2366 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rounding mode
2367 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
2368 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2369
2370 // Restore FPSCR: clear rmode bits back to nearest (default)
2371 bytes.extend_from_slice(&vmrs.to_le_bytes());
2372 bytes.extend_from_slice(&bic.to_le_bytes());
2373 bytes.extend_from_slice(&vmsr.to_le_bytes());
2374 }
2375
2376 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
2377 let (vd2, d2) = encode_sreg(sd_num);
2378 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
2379 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2380
2381 Ok(bytes)
2382 }
2383
2384 /// Encode F32 min/max as ARM32: VCMP + VMRS + conditional VMOV
2385 fn encode_arm_f32_minmax(
2386 &self,
2387 sd: &VfpReg,
2388 sn: &VfpReg,
2389 sm: &VfpReg,
2390 is_min: bool,
2391 ) -> Result<Vec<u8>> {
2392 let mut bytes = Vec::new();
2393 let sn_num = vfp_sreg_to_num(sn)?;
2394 let sm_num = vfp_sreg_to_num(sm)?;
2395 let sd_num = vfp_sreg_to_num(sd)?;
2396
2397 // VMOV Sd, Sn (start with first operand)
2398 let (vd, d) = encode_sreg(sd_num);
2399 let (vn, n) = encode_sreg(sn_num);
2400 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2401 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2402
2403 // VCMP.F32 Sn, Sm
2404 let (vm, m) = encode_sreg(sm_num);
2405 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2406 bytes.extend_from_slice(&vcmp.to_le_bytes());
2407
2408 // VMRS APSR_nzcv, FPSCR
2409 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2410
2411 // For min: if Sn > Sm (GT), use Sm. Condition = GT (0xC)
2412 // For max: if Sn < Sm (MI/LT), use Sm. Condition = MI (0x4)
2413 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2414
2415 // VMOV{cond} Sd, Sm — conditional VMOV
2416 let vmov_cond = (cond << 28) | 0x0EB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2417 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2418
2419 Ok(bytes)
2420 }
2421
2422 /// Encode F32 copysign as ARM32: extract sign from Sm, magnitude from Sn
2423 fn encode_arm_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2424 let mut bytes = Vec::new();
2425
2426 // VMOV R12, Sm (get sign source bits)
2427 let vmov_sm = encode_vmov_core_sreg(false, sm, &Reg::R12)?;
2428 bytes.extend_from_slice(&vmov_sm.to_le_bytes());
2429
2430 // VMOV R0, Sn (get magnitude source bits) — use R0 as temp
2431 let vmov_sn = encode_vmov_core_sreg(false, sn, &Reg::R0)?;
2432 bytes.extend_from_slice(&vmov_sn.to_le_bytes());
2433
2434 // AND R12, R12, #0x80000000 (keep only sign bit)
2435 // Thumb-2 constant 0x80000000 needs special encoding; in ARM32 use rotated imm
2436 // 0x80000000 = 0x02 rotated right by 2 (rotation=1, imm8=0x02)
2437 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2438 bytes.extend_from_slice(&and_sign.to_le_bytes());
2439
2440 // BIC R0, R0, #0x80000000 (clear sign bit from magnitude)
2441 // R0 = register 0, so Rn and Rd fields are 0
2442 let bic_sign = 0xE3C00000u32 | (1 << 8) | 0x02;
2443 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2444
2445 // ORR R0, R0, R12 (combine sign + magnitude)
2446 // R0 = register 0, so Rn and Rd fields are 0
2447 let orr = 0xE1800000u32 | 12;
2448 bytes.extend_from_slice(&orr.to_le_bytes());
2449
2450 // VMOV Sd, R0
2451 let vmov_result = encode_vmov_core_sreg(true, sd, &Reg::R0)?;
2452 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2453
2454 Ok(bytes)
2455 }
2456
2457 /// Encode F64 comparison as ARM32: VCMP.F64 + VMRS + MOV rd,#0 + MOVcond rd,#1
2458 fn encode_arm_f64_compare(
2459 &self,
2460 rd: &Reg,
2461 dn: &VfpReg,
2462 dm: &VfpReg,
2463 cond_code: u32,
2464 ) -> Result<Vec<u8>> {
2465 let mut bytes = Vec::new();
2466
2467 // VCMP.F64 Dn, Dm: 0xEEB40B40 with Dn in Vd position, Dm in Vm position
2468 let dn_num = vfp_dreg_to_num(dn)?;
2469 let dm_num = vfp_dreg_to_num(dm)?;
2470 let (vd, d) = encode_dreg(dn_num);
2471 let (vm, m) = encode_dreg(dm_num);
2472 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2473 bytes.extend_from_slice(&vcmp.to_le_bytes());
2474
2475 // VMRS APSR_nzcv, FPSCR
2476 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2477
2478 // MOV rd, #0
2479 let rd_bits = reg_to_bits(rd);
2480 let mov_zero = 0xE3A00000 | (rd_bits << 12);
2481 bytes.extend_from_slice(&mov_zero.to_le_bytes());
2482
2483 // MOVcond rd, #1
2484 let mov_one = (cond_code << 28) | 0x03A00001 | (rd_bits << 12);
2485 bytes.extend_from_slice(&mov_one.to_le_bytes());
2486
2487 Ok(bytes)
2488 }
2489
2490 /// Encode F64 constant load as ARM32: MOVW + MOVT + MOVW + MOVT + VMOV
2491 fn encode_arm_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
2492 let mut bytes = Vec::new();
2493 let bits = value.to_bits();
2494 let lo32 = bits as u32;
2495 let hi32 = (bits >> 32) as u32;
2496
2497 // Load low 32 bits into R0 (Rd field = 0 for R0)
2498 let lo16 = lo32 & 0xFFFF;
2499 let movw_r0 = 0xE3000000 | ((lo16 >> 12) << 16) | (lo16 & 0xFFF);
2500 bytes.extend_from_slice(&movw_r0.to_le_bytes());
2501 let hi16 = (lo32 >> 16) & 0xFFFF;
2502 let movt_r0 = 0xE3400000 | ((hi16 >> 12) << 16) | (hi16 & 0xFFF);
2503 bytes.extend_from_slice(&movt_r0.to_le_bytes());
2504
2505 // Load high 32 bits into R12
2506 let lo16 = hi32 & 0xFFFF;
2507 let movw_r12 = 0xE3000000 | ((lo16 >> 12) << 16) | (12 << 12) | (lo16 & 0xFFF);
2508 bytes.extend_from_slice(&movw_r12.to_le_bytes());
2509 let hi16 = (hi32 >> 16) & 0xFFFF;
2510 let movt_r12 = 0xE3400000 | ((hi16 >> 12) << 16) | (12 << 12) | (hi16 & 0xFFF);
2511 bytes.extend_from_slice(&movt_r12.to_le_bytes());
2512
2513 // VMOV Dd, R0, R12
2514 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
2515 bytes.extend_from_slice(&vmov.to_le_bytes());
2516
2517 Ok(bytes)
2518 }
2519
2520 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as ARM32
2521 fn encode_arm_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
2522 let mut bytes = Vec::new();
2523
2524 // Use S0 as intermediate: VMOV S0, Rm
2525 let vmov = encode_vmov_core_sreg(true, &VfpReg::S0, rm)?;
2526 bytes.extend_from_slice(&vmov.to_le_bytes());
2527
2528 // VCVT.F64.S32 Dd, S0 (signed) or VCVT.F64.U32 Dd, S0 (unsigned)
2529 // Base: 0xEEB80B40 (signed) or 0xEEB80BC0 (unsigned)
2530 let dd_num = vfp_dreg_to_num(dd)?;
2531 let (vd, d) = encode_dreg(dd_num);
2532 let base = if signed { 0xEEB80B40 } else { 0xEEB80BC0 };
2533 // S0 is register 0: Vm=0, M=0
2534 let vcvt = base | (d << 22) | (vd << 12);
2535 bytes.extend_from_slice(&vcvt.to_le_bytes());
2536
2537 Ok(bytes)
2538 }
2539
2540 /// Encode VCVT.F64.F32 Dd, Sm as ARM32 (f32 to f64 promotion)
2541 fn encode_arm_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
2542 let dd_num = vfp_dreg_to_num(dd)?;
2543 let sm_num = vfp_sreg_to_num(sm)?;
2544 let (vd, d) = encode_dreg(dd_num);
2545 let (vm, m) = encode_sreg(sm_num);
2546
2547 // VCVT.F64.F32 Dd, Sm: 0xEEB70AC0
2548 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
2549 Ok(vcvt.to_le_bytes().to_vec())
2550 }
2551
2552 /// Encode VCVT.S32/U32.F64 Sd, Dm + VMOV Rd, Sd as ARM32
2553 fn encode_arm_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2554 let mut bytes = Vec::new();
2555 let dm_num = vfp_dreg_to_num(dm)?;
2556 let (vm, m) = encode_dreg(dm_num);
2557
2558 // VCVT.S32.F64 S0, Dm (toward zero) or VCVT.U32.F64 S0, Dm
2559 // S0: Vd=0, D=0
2560 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
2561 let vcvt = base | (m << 5) | vm;
2562 bytes.extend_from_slice(&vcvt.to_le_bytes());
2563
2564 // VMOV Rd, S0
2565 let vmov = encode_vmov_core_sreg(false, &VfpReg::S0, rd)?;
2566 bytes.extend_from_slice(&vmov.to_le_bytes());
2567
2568 Ok(bytes)
2569 }
2570
2571 /// Encode F64 rounding pseudo-op as ARM32 via VCVT to integer and back.
2572 /// Encode F64 rounding as ARM32.
2573 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
2574 ///
2575 /// For trunc: uses VCVTR.S32.F64 (always truncates).
2576 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F64 (non-R variant),
2577 /// then restores FPSCR.
2578 fn encode_arm_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
2579 let mut bytes = Vec::new();
2580 let dm_num = vfp_dreg_to_num(dm)?;
2581 let dd_num = vfp_dreg_to_num(dd)?;
2582 let (vm, m) = encode_dreg(dm_num);
2583 let (vd, d) = encode_dreg(dd_num);
2584
2585 if mode == 0b11 {
2586 // Trunc (toward zero): VCVTR.S32.F64 — bit[7]=1, always truncates
2587 let vcvt_to_int = 0xEEBD0BC0 | (m << 5) | vm;
2588 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2589 } else {
2590 // ceil/floor/nearest: manipulate FPSCR rounding mode
2591 let rt: u32 = 12;
2592
2593 // VMRS R12, FPSCR
2594 let vmrs = 0xEEF10A10 | (rt << 12);
2595 bytes.extend_from_slice(&vmrs.to_le_bytes());
2596
2597 // BIC R12, R12, #(3 << 22)
2598 let bic = 0xE3CC0000 | (rt << 12) | (0x05 << 8) | 0x03;
2599 bytes.extend_from_slice(&bic.to_le_bytes());
2600
2601 // ORR R12, R12, #(mode << 22)
2602 if mode != 0 {
2603 let orr = 0xE38C0000 | (rt << 12) | (0x05 << 8) | (mode as u32);
2604 bytes.extend_from_slice(&orr.to_le_bytes());
2605 }
2606
2607 // VMSR FPSCR, R12
2608 let vmsr = 0xEEE10A10 | (rt << 12);
2609 bytes.extend_from_slice(&vmsr.to_le_bytes());
2610
2611 // VCVT.S32.F64 S0, Dm — non-R variant (bit[7]=0), uses FPSCR rmode
2612 let vcvt_to_int = 0xEEBD0B40 | (m << 5) | vm;
2613 bytes.extend_from_slice(&vcvt_to_int.to_le_bytes());
2614
2615 // Restore FPSCR
2616 bytes.extend_from_slice(&vmrs.to_le_bytes());
2617 bytes.extend_from_slice(&bic.to_le_bytes());
2618 bytes.extend_from_slice(&vmsr.to_le_bytes());
2619 }
2620
2621 // VCVT.F64.S32 Dd, S0 (convert back to double)
2622 let vcvt_to_float = 0xEEB80B40 | (d << 22) | (vd << 12);
2623 bytes.extend_from_slice(&vcvt_to_float.to_le_bytes());
2624
2625 Ok(bytes)
2626 }
2627
2628 /// Encode F64 min/max as ARM32: VMOV + VCMP + VMRS + conditional VMOV
2629 fn encode_arm_f64_minmax(
2630 &self,
2631 dd: &VfpReg,
2632 dn: &VfpReg,
2633 dm: &VfpReg,
2634 is_min: bool,
2635 ) -> Result<Vec<u8>> {
2636 let mut bytes = Vec::new();
2637 let dn_num = vfp_dreg_to_num(dn)?;
2638 let dm_num = vfp_dreg_to_num(dm)?;
2639 let dd_num = vfp_dreg_to_num(dd)?;
2640
2641 // VMOV.F64 Dd, Dn (start with first operand)
2642 let (vd, d) = encode_dreg(dd_num);
2643 let (vn, n) = encode_dreg(dn_num);
2644 let vmov_dn = 0xEEB00B40 | (d << 22) | (vd << 12) | (n << 5) | vn;
2645 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2646
2647 // VCMP.F64 Dn, Dm
2648 let (vm, m) = encode_dreg(dm_num);
2649 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
2650 bytes.extend_from_slice(&vcmp.to_le_bytes());
2651
2652 // VMRS APSR_nzcv, FPSCR
2653 bytes.extend_from_slice(&0xEEF1FA10u32.to_le_bytes());
2654
2655 let cond = if is_min { 0xCu32 } else { 0x4u32 };
2656 let vmov_cond = (cond << 28) | 0x0EB00B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
2657 bytes.extend_from_slice(&vmov_cond.to_le_bytes());
2658
2659 Ok(bytes)
2660 }
2661
2662 /// Encode F64 copysign as ARM32
2663 fn encode_arm_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
2664 let mut bytes = Vec::new();
2665
2666 // VMOV R0, R12, Dm (get sign source bits)
2667 let vmov_dm = encode_vmov_core_dreg(false, dm, &Reg::R0, &Reg::R12)?;
2668 bytes.extend_from_slice(&vmov_dm.to_le_bytes());
2669
2670 // VMOV R1, R2, Dn (get magnitude source bits)
2671 // We use R1 (lo) and R2 (hi) for the magnitude
2672 let vmov_dn = encode_vmov_core_dreg(false, dn, &Reg::R1, &Reg::R2)?;
2673 bytes.extend_from_slice(&vmov_dn.to_le_bytes());
2674
2675 // AND R12, R12, #0x80000000 (keep only sign bit from hi word)
2676 let and_sign = 0xE2000000u32 | (12 << 16) | (12 << 12) | (1 << 8) | 0x02;
2677 bytes.extend_from_slice(&and_sign.to_le_bytes());
2678
2679 // BIC R2, R2, #0x80000000 (clear sign bit from magnitude hi word)
2680 let bic_sign = 0xE3C00000u32 | (2 << 16) | (2 << 12) | (1 << 8) | 0x02;
2681 bytes.extend_from_slice(&bic_sign.to_le_bytes());
2682
2683 // ORR R2, R2, R12 (combine sign + magnitude)
2684 let orr = 0xE1800000u32 | (2 << 16) | (2 << 12) | 12;
2685 bytes.extend_from_slice(&orr.to_le_bytes());
2686
2687 // VMOV Dd, R1, R2
2688 let vmov_result = encode_vmov_core_dreg(true, dd, &Reg::R1, &Reg::R2)?;
2689 bytes.extend_from_slice(&vmov_result.to_le_bytes());
2690
2691 Ok(bytes)
2692 }
2693
2694 /// Encode VCVT.S32/U32.F32 + VMOV as ARM32
2695 fn encode_arm_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
2696 let mut bytes = Vec::new();
2697
2698 // VCVT.S32.F32 Sd, Sm (toward zero) or VCVT.U32.F32 Sd, Sm
2699 // We use Sm as both source and destination for the intermediate result
2700 let sm_num = vfp_sreg_to_num(sm)?;
2701 let (vd, d) = encode_sreg(sm_num);
2702 let (vm, m) = encode_sreg(sm_num);
2703 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
2704 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
2705 bytes.extend_from_slice(&vcvt.to_le_bytes());
2706
2707 // VMOV Rd, Sm — move result back to core register
2708 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
2709 bytes.extend_from_slice(&vmov.to_le_bytes());
2710
2711 Ok(bytes)
2712 }
2713
2714 /// Encode an ARM instruction in Thumb-2 mode (16-bit or 32-bit instructions)
2715 fn encode_thumb(&self, op: &ArmOp) -> Result<Vec<u8>> {
2716 // Thumb-2 supports both 16-bit and 32-bit instructions
2717 // 32-bit instructions are encoded as two 16-bit halfwords (big-endian order)
2718 match op {
2719 // === 16-bit Thumb encodings ===
2720 ArmOp::Add { rd, rn, op2 } => {
2721 let rd_bits = reg_to_bits(rd) as u16;
2722 let rn_bits = reg_to_bits(rn) as u16;
2723
2724 if let Operand2::Reg(rm) = op2 {
2725 let rm_bits = reg_to_bits(rm) as u16;
2726 // 16-bit ADDS only has 3-bit register fields (R0-R7). For
2727 // high registers (e.g. R12, the MemLoad/MemStore base
2728 // scratch) the bits overflow into adjacent fields, silently
2729 // corrupting the operands — issue #178/#180: `add ip,ip,r0`
2730 // was emitted as `adds r4,r5,r1`. Guard on all three regs
2731 // being low and fall back to 32-bit ADD.W otherwise, exactly
2732 // as the Sub handler below does.
2733 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2734 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2735 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2736 Ok(instr.to_le_bytes().to_vec())
2737 } else {
2738 // ADD.W Rd, Rn, Rm (32-bit) for high registers
2739 self.encode_thumb32_add_reg_raw(
2740 rd_bits as u32,
2741 rn_bits as u32,
2742 rm_bits as u32,
2743 )
2744 }
2745 } else if let Operand2::Imm(imm) = op2 {
2746 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2747 // ADDS Rd, Rn, #imm3 (16-bit): 0001 110 imm3 Rn Rd
2748 let instr: u16 = 0x1C00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2749 Ok(instr.to_le_bytes().to_vec())
2750 } else {
2751 // Use 32-bit ADD for larger immediates
2752 self.encode_thumb32_add(rd, rn, *imm as u32)
2753 }
2754 } else {
2755 // Fallback to 32-bit encoding
2756 self.encode_thumb32_add(rd, rn, 0)
2757 }
2758 }
2759
2760 ArmOp::Sub { rd, rn, op2 } => {
2761 let rd_bits = reg_to_bits(rd) as u16;
2762 let rn_bits = reg_to_bits(rn) as u16;
2763
2764 if let Operand2::Reg(rm) = op2 {
2765 let rm_bits = reg_to_bits(rm) as u16;
2766 // 16-bit SUBS can only use low registers (R0-R7)
2767 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2768 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2769 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2770 Ok(instr.to_le_bytes().to_vec())
2771 } else {
2772 // Use 32-bit SUB.W for high registers
2773 self.encode_thumb32_sub_reg_raw(
2774 rd_bits as u32,
2775 rn_bits as u32,
2776 rm_bits as u32,
2777 )
2778 }
2779 } else if let Operand2::Imm(imm) = op2 {
2780 if *imm <= 7 && rd_bits < 8 && rn_bits < 8 {
2781 // SUBS Rd, Rn, #imm3 (16-bit): 0001 111 imm3 Rn Rd
2782 let instr: u16 = 0x1E00 | ((*imm as u16) << 6) | (rn_bits << 3) | rd_bits;
2783 Ok(instr.to_le_bytes().to_vec())
2784 } else {
2785 self.encode_thumb32_sub(rd, rn, *imm as u32)
2786 }
2787 } else {
2788 self.encode_thumb32_sub(rd, rn, 0)
2789 }
2790 }
2791
2792 ArmOp::Mov { rd, op2 } => {
2793 let rd_bits = reg_to_bits(rd) as u16;
2794
2795 if let Operand2::Imm(imm) = op2 {
2796 // #498: the old test here was the SIGNED `*imm <= 255`,
2797 // so a negative immediate (e.g. -1) fell into the 16-bit
2798 // MOVS arm and encoded the wrong VALUE (#(imm & 0xFF) =
2799 // #0xFF). A positive imm above 0xFFFF was equally wrong:
2800 // MOVW truncates to 16 bits. Split on the UNSIGNED value:
2801 // imm8 → MOVS, imm16 → MOVW, anything wider (negative or
2802 // >0xFFFF) → the full-value MOVW+MOVT pair. No emitter
2803 // produces the wide shape today (both selectors
2804 // materialize wide constants as explicit Movw/Movt or
2805 // Movw+Mvn), so this is byte-identical on shipped paths —
2806 // it retires the latent wrong-value encodings the
2807 // `estimator_encoder_agreement` oracle had pinned.
2808 let uimm = *imm as u32;
2809 if uimm <= 255 && rd_bits < 8 {
2810 // MOVS Rd, #imm8 (16-bit): 0010 0 Rd imm8
2811 let imm_bits = (*imm as u16) & 0xFF;
2812 let instr: u16 = 0x2000 | (rd_bits << 8) | imm_bits;
2813 Ok(instr.to_le_bytes().to_vec())
2814 } else if uimm <= 0xFFFF {
2815 // Use 32-bit MOVW for 16-bit immediates
2816 self.encode_thumb32_movw(rd, uimm)
2817 } else {
2818 // Full 32-bit value: MOVW low16 + MOVT high16
2819 let mut bytes = self.encode_thumb32_movw(rd, uimm & 0xFFFF)?;
2820 bytes.extend(self.encode_thumb32_movt_raw(reg_to_bits(rd), uimm >> 16)?);
2821 Ok(bytes)
2822 }
2823 } else if let Operand2::Reg(rm) = op2 {
2824 let rm_bits = reg_to_bits(rm) as u16;
2825 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
2826 // D = Rd[3], Rd[2:0] in lower bits
2827 let d_bit = (rd_bits >> 3) & 1;
2828 let instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
2829 Ok(instr.to_le_bytes().to_vec())
2830 } else {
2831 let instr: u16 = 0xBF00; // NOP fallback
2832 Ok(instr.to_le_bytes().to_vec())
2833 }
2834 }
2835
2836 ArmOp::Push { regs } => {
2837 // Thumb-2 PUSH encoding:
2838 // If all regs in R0-R7 + LR, use 16-bit: 1011 010 M rrrrrrrr
2839 // Otherwise use 32-bit: STMDB SP!, {regs} = 1110 1001 0010 1101 | 0M0 reglist(13)
2840 let mut reg_list: u16 = 0;
2841 let mut need_32bit = false;
2842 for r in regs {
2843 let bit = reg_to_bits(r);
2844 if bit >= 8 && *r != Reg::LR {
2845 need_32bit = true;
2846 }
2847 reg_list |= 1 << bit;
2848 }
2849 if !need_32bit {
2850 // 16-bit PUSH: 1011 010 M rrrrrrrr
2851 let m_bit = if reg_list & (1 << 14) != 0 {
2852 1u16
2853 } else {
2854 0u16
2855 };
2856 let low_regs = reg_list & 0xFF;
2857 let instr: u16 = 0xB400 | (m_bit << 8) | low_regs;
2858 Ok(instr.to_le_bytes().to_vec())
2859 } else {
2860 // 32-bit STMDB SP!, {regs}: E92D | reglist(16)
2861 let hw1: u16 = 0xE92D;
2862 let hw2: u16 = reg_list;
2863 let mut bytes = hw1.to_le_bytes().to_vec();
2864 bytes.extend_from_slice(&hw2.to_le_bytes());
2865 Ok(bytes)
2866 }
2867 }
2868
2869 ArmOp::Pop { regs } => {
2870 // Thumb-2 POP encoding:
2871 // If all regs in R0-R7 + PC, use 16-bit: 1011 110 P rrrrrrrr
2872 // Otherwise use 32-bit: LDMIA SP!, {regs} = 1110 1000 1011 1101 | PM0 reglist(13)
2873 let mut reg_list: u16 = 0;
2874 let mut need_32bit = false;
2875 for r in regs {
2876 let bit = reg_to_bits(r);
2877 if bit >= 8 && *r != Reg::PC {
2878 need_32bit = true;
2879 }
2880 reg_list |= 1 << bit;
2881 }
2882 if !need_32bit {
2883 // 16-bit POP: 1011 110 P rrrrrrrr
2884 let p_bit = if reg_list & (1 << 15) != 0 {
2885 1u16
2886 } else {
2887 0u16
2888 };
2889 let low_regs = reg_list & 0xFF;
2890 let instr: u16 = 0xBC00 | (p_bit << 8) | low_regs;
2891 Ok(instr.to_le_bytes().to_vec())
2892 } else {
2893 // 32-bit LDMIA SP!, {regs}: E8BD | reglist(16)
2894 let hw1: u16 = 0xE8BD;
2895 let hw2: u16 = reg_list;
2896 let mut bytes = hw1.to_le_bytes().to_vec();
2897 bytes.extend_from_slice(&hw2.to_le_bytes());
2898 Ok(bytes)
2899 }
2900 }
2901
2902 ArmOp::Nop => {
2903 let instr: u16 = 0xBF00; // NOP in Thumb-2
2904 Ok(instr.to_le_bytes().to_vec())
2905 }
2906
2907 ArmOp::Udf { imm } => {
2908 // UDF (Undefined) in Thumb-2: 16-bit encoding is 0xDE00 | imm8
2909 // This triggers UsageFault/HardFault, used for WASM traps
2910 let instr: u16 = 0xDE00 | (*imm as u16);
2911 let bytes = instr.to_le_bytes().to_vec();
2912 encoding_contracts::verify_thumb16(&bytes);
2913 Ok(bytes)
2914 }
2915
2916 // i64 support: ADDS, ADC, SUBS, SBC for register pair arithmetic
2917 // ADDS sets flags (carry), ADC uses carry from previous ADDS
2918 ArmOp::Adds { rd, rn, op2 } => {
2919 let rd_bits = reg_to_bits(rd) as u16;
2920 let rn_bits = reg_to_bits(rn) as u16;
2921
2922 if let Operand2::Reg(rm) = op2 {
2923 let rm_bits = reg_to_bits(rm) as u16;
2924 // 16-bit ADDS is R0-R7 only; i64 pair allocation can place
2925 // operands in R8-R11, which would overflow the 3-bit fields
2926 // and corrupt the operands (#178/#180 class). Guard and fall
2927 // back to 32-bit ADDS.W for high registers.
2928 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2929 // ADDS Rd, Rn, Rm (16-bit): 0001 100 Rm Rn Rd
2930 let instr: u16 = 0x1800 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2931 Ok(instr.to_le_bytes().to_vec())
2932 } else {
2933 self.encode_thumb32_adds_reg_raw(
2934 rd_bits as u32,
2935 rn_bits as u32,
2936 rm_bits as u32,
2937 )
2938 }
2939 } else {
2940 // 32-bit Thumb-2 ADDS with immediate
2941 self.encode_thumb32_adds(rd, rn, 0)
2942 }
2943 }
2944
2945 // ADC: Add with Carry (Thumb-2 32-bit)
2946 // ADC.W Rd, Rn, Rm: EB40 Rn | 00 Rd 00 Rm
2947 ArmOp::Adc { rd, rn, op2 } => {
2948 let rd_bits = reg_to_bits(rd);
2949 let rn_bits = reg_to_bits(rn);
2950
2951 if let Operand2::Reg(rm) = op2 {
2952 let rm_bits = reg_to_bits(rm);
2953 // ADC.W Rd, Rn, Rm (T2): 1110 1011 0100 Rn | 0 000 Rd 00 00 Rm
2954 let hw1: u16 = (0xEB40 | rn_bits) as u16;
2955 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
2956
2957 let mut bytes = hw1.to_le_bytes().to_vec();
2958 bytes.extend_from_slice(&hw2.to_le_bytes());
2959 Ok(bytes)
2960 } else {
2961 // ADC with immediate - use 32-bit encoding
2962 let hw1: u16 = (0xF140 | rn_bits) as u16;
2963 let hw2: u16 = (rd_bits << 8) as u16;
2964 let mut bytes = hw1.to_le_bytes().to_vec();
2965 bytes.extend_from_slice(&hw2.to_le_bytes());
2966 Ok(bytes)
2967 }
2968 }
2969
2970 // SUBS sets flags (borrow), SBC uses borrow from previous SUBS
2971 ArmOp::Subs { rd, rn, op2 } => {
2972 let rd_bits = reg_to_bits(rd) as u16;
2973 let rn_bits = reg_to_bits(rn) as u16;
2974
2975 if let Operand2::Reg(rm) = op2 {
2976 let rm_bits = reg_to_bits(rm) as u16;
2977 // 16-bit SUBS is R0-R7 only; high-register i64 pair operands
2978 // would overflow the 3-bit fields (#178/#180 class). Guard
2979 // and fall back to 32-bit SUBS.W for high registers.
2980 if rd_bits < 8 && rn_bits < 8 && rm_bits < 8 {
2981 // SUBS Rd, Rn, Rm (16-bit): 0001 101 Rm Rn Rd
2982 let instr: u16 = 0x1A00 | (rm_bits << 6) | (rn_bits << 3) | rd_bits;
2983 Ok(instr.to_le_bytes().to_vec())
2984 } else {
2985 self.encode_thumb32_subs_reg_raw(
2986 rd_bits as u32,
2987 rn_bits as u32,
2988 rm_bits as u32,
2989 )
2990 }
2991 } else {
2992 // 32-bit Thumb-2 SUBS with immediate
2993 self.encode_thumb32_subs(rd, rn, 0)
2994 }
2995 }
2996
2997 // SBC: Subtract with Carry (Thumb-2 32-bit)
2998 // SBC.W Rd, Rn, Rm: EB60 Rn | 00 Rd 00 Rm
2999 ArmOp::Sbc { rd, rn, op2 } => {
3000 let rd_bits = reg_to_bits(rd);
3001 let rn_bits = reg_to_bits(rn);
3002
3003 if let Operand2::Reg(rm) = op2 {
3004 let rm_bits = reg_to_bits(rm);
3005 // SBC.W Rd, Rn, Rm (T2): 1110 1011 0110 Rn | 0 000 Rd 00 00 Rm
3006 let hw1: u16 = (0xEB60 | rn_bits) as u16;
3007 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3008
3009 let mut bytes = hw1.to_le_bytes().to_vec();
3010 bytes.extend_from_slice(&hw2.to_le_bytes());
3011 Ok(bytes)
3012 } else {
3013 // SBC with immediate - use 32-bit encoding
3014 let hw1: u16 = (0xF160 | rn_bits) as u16;
3015 let hw2: u16 = (rd_bits << 8) as u16;
3016 let mut bytes = hw1.to_le_bytes().to_vec();
3017 bytes.extend_from_slice(&hw2.to_le_bytes());
3018 Ok(bytes)
3019 }
3020 }
3021
3022 // === 32-bit Thumb-2 encodings ===
3023
3024 // SDIV: 11111011 1001 Rn 1111 Rd 1111 Rm
3025 ArmOp::Sdiv { rd, rn, rm } => {
3026 let rd_bits = reg_to_bits(rd);
3027 let rn_bits = reg_to_bits(rn);
3028 let rm_bits = reg_to_bits(rm);
3029 reg_bits_checked(rd_bits)?;
3030 reg_bits_checked(rn_bits)?;
3031 reg_bits_checked(rm_bits)?;
3032
3033 // Thumb-2 SDIV: FB90 F0F0 | Rn<<16 | Rd<<8 | Rm
3034 // First halfword: 1111 1011 1001 Rn = 0xFB90 | Rn
3035 // Second halfword: 1111 Rd 1111 Rm = 0xF0F0 | Rd<<8 | Rm
3036 let hw1: u16 = (0xFB90 | rn_bits) as u16;
3037 let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3038
3039 // Thumb-2 32-bit instructions: first halfword, then second halfword (little-endian each)
3040 let mut bytes = hw1.to_le_bytes().to_vec();
3041 bytes.extend_from_slice(&hw2.to_le_bytes());
3042 encoding_contracts::verify_thumb32(&bytes);
3043 Ok(bytes)
3044 }
3045
3046 // UDIV: 11111011 1011 Rn 1111 Rd 1111 Rm
3047 ArmOp::Udiv { rd, rn, rm } => {
3048 let rd_bits = reg_to_bits(rd);
3049 let rn_bits = reg_to_bits(rn);
3050 let rm_bits = reg_to_bits(rm);
3051 reg_bits_checked(rd_bits)?;
3052 reg_bits_checked(rn_bits)?;
3053 reg_bits_checked(rm_bits)?;
3054
3055 // Thumb-2 UDIV: FBB0 F0F0 | Rn<<16 | Rd<<8 | Rm
3056 let hw1: u16 = (0xFBB0 | rn_bits) as u16;
3057 let hw2: u16 = (0xF0F0 | (rd_bits << 8) | rm_bits) as u16;
3058
3059 let mut bytes = hw1.to_le_bytes().to_vec();
3060 bytes.extend_from_slice(&hw2.to_le_bytes());
3061 encoding_contracts::verify_thumb32(&bytes);
3062 Ok(bytes)
3063 }
3064
3065 ArmOp::Umull { rdlo, rdhi, rn, rm } => {
3066 let rdlo_bits = reg_to_bits(rdlo);
3067 let rdhi_bits = reg_to_bits(rdhi);
3068 let rn_bits = reg_to_bits(rn);
3069 let rm_bits = reg_to_bits(rm);
3070 reg_bits_checked(rdlo_bits)?;
3071 reg_bits_checked(rdhi_bits)?;
3072 reg_bits_checked(rn_bits)?;
3073 reg_bits_checked(rm_bits)?;
3074
3075 // Thumb-2 UMULL: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm
3076 let hw1: u16 = (0xFBA0 | rn_bits) as u16;
3077 let hw2: u16 = ((rdlo_bits << 12) | (rdhi_bits << 8) | rm_bits) as u16;
3078
3079 let mut bytes = hw1.to_le_bytes().to_vec();
3080 bytes.extend_from_slice(&hw2.to_le_bytes());
3081 encoding_contracts::verify_thumb32(&bytes);
3082 Ok(bytes)
3083 }
3084
3085 // MUL (Thumb-2 32-bit): MUL Rd, Rn, Rm
3086 ArmOp::Mul { rd, rn, rm } => {
3087 let rd_bits = reg_to_bits(rd);
3088 let rn_bits = reg_to_bits(rn);
3089 let rm_bits = reg_to_bits(rm);
3090
3091 // Thumb-2 MUL: FB00 F000 | Rn | Rd<<8 | Rm
3092 // 11111011 0000 Rn | 1111 Rd 0000 Rm
3093 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3094 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
3095
3096 let mut bytes = hw1.to_le_bytes().to_vec();
3097 bytes.extend_from_slice(&hw2.to_le_bytes());
3098 Ok(bytes)
3099 }
3100
3101 // MLS: Rd = Ra - Rn * Rm
3102 ArmOp::Mls { rd, rn, rm, ra } => {
3103 let rd_bits = reg_to_bits(rd);
3104 let rn_bits = reg_to_bits(rn);
3105 let rm_bits = reg_to_bits(rm);
3106 let ra_bits = reg_to_bits(ra);
3107
3108 // Thumb-2 MLS: FB00 Rn | Ra Rd 0001 Rm
3109 // 11111011 0000 Rn | Ra Rd 0001 Rm
3110 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3111 let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | 0x10 | rm_bits) as u16;
3112
3113 let mut bytes = hw1.to_le_bytes().to_vec();
3114 bytes.extend_from_slice(&hw2.to_le_bytes());
3115 Ok(bytes)
3116 }
3117
3118 ArmOp::Mla { rd, rn, rm, ra } => {
3119 let rd_bits = reg_to_bits(rd);
3120 let rn_bits = reg_to_bits(rn);
3121 let rm_bits = reg_to_bits(rm);
3122 let ra_bits = reg_to_bits(ra);
3123
3124 // Thumb-2 MLA: FB00 Rn | Ra Rd 0000 Rm — same as MLS without the
3125 // bit-4 (0x10) op flag. rd = ra + rn*rm.
3126 let hw1: u16 = (0xFB00 | rn_bits) as u16;
3127 let hw2: u16 = ((ra_bits << 12) | (rd_bits << 8) | rm_bits) as u16;
3128
3129 let mut bytes = hw1.to_le_bytes().to_vec();
3130 bytes.extend_from_slice(&hw2.to_le_bytes());
3131 Ok(bytes)
3132 }
3133
3134 // AND (Thumb-2 32-bit)
3135 ArmOp::And { rd, rn, op2 } => {
3136 if let Operand2::Reg(rm) = op2 {
3137 let rd_bits = reg_to_bits(rd);
3138 let rn_bits = reg_to_bits(rn);
3139 let rm_bits = reg_to_bits(rm);
3140
3141 // Thumb-2 AND register: EA00 Rn | 0 Rd 00 00 Rm
3142 let hw1: u16 = (0xEA00 | rn_bits) as u16;
3143 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3144
3145 let mut bytes = hw1.to_le_bytes().to_vec();
3146 bytes.extend_from_slice(&hw2.to_le_bytes());
3147 Ok(bytes)
3148 } else if let Operand2::Imm(imm) = op2 {
3149 let rd_bits = reg_to_bits(rd);
3150 let rn_bits = reg_to_bits(rn);
3151
3152 // Thumb-2 AND.W immediate T1: 11110 i 0 0000 S Rn | 0 imm3 Rd imm8.
3153 // The i:imm3:imm8 field is a ThumbExpandImm modified immediate —
3154 // encode it correctly (or error on an un-encodable value)
3155 // rather than packing raw bits, closing the silent-miscompile
3156 // class for AND alongside ORR/EOR (#251) / ADD/SUB (#253) /
3157 // CMP (#255).
3158 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3159 synth_core::Error::synthesis(
3160 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
3161 )
3162 })?;
3163 let i_bit = (field >> 11) & 1;
3164 let imm3 = (field >> 8) & 0x7;
3165 let imm8 = field & 0xFF;
3166
3167 let hw1: u16 = (0xF000 | (i_bit << 10) | rn_bits) as u16;
3168 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3169
3170 let mut bytes = hw1.to_le_bytes().to_vec();
3171 bytes.extend_from_slice(&hw2.to_le_bytes());
3172 Ok(bytes)
3173 } else {
3174 // RegShift variant - fallback to NOP
3175 let instr: u16 = 0xBF00;
3176 Ok(instr.to_le_bytes().to_vec())
3177 }
3178 }
3179
3180 // ORR (Thumb-2 32-bit)
3181 ArmOp::Orr { rd, rn, op2 } => {
3182 if let Operand2::Reg(rm) = op2 {
3183 let rd_bits = reg_to_bits(rd);
3184 let rn_bits = reg_to_bits(rn);
3185 let rm_bits = reg_to_bits(rm);
3186
3187 // Thumb-2 ORR: EA40 Rn | 0 Rd 00 00 Rm
3188 let hw1: u16 = (0xEA40 | rn_bits) as u16;
3189 let hw2: u16 = ((rd_bits << 8) | rm_bits) 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 if let Operand2::Imm(imm) = op2 {
3195 // ORR.W immediate T1: 11110 i 0 0010 S Rn | 0 imm3 Rd imm8.
3196 // Only the zero-extended byte form (imm <= 0xFF) is encoded;
3197 // larger modified immediates need ThumbExpandImm — return an
3198 // error rather than silently emit a NOP (Ok-or-Err, #180/#185).
3199 let imm_val = *imm as u32;
3200 if imm_val > 0xFF {
3201 return Err(synth_core::Error::synthesis(
3202 "ORR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3203 ));
3204 }
3205 let rd_bits = reg_to_bits(rd);
3206 let rn_bits = reg_to_bits(rn);
3207 let hw1: u16 = (0xF040 | rn_bits) as u16;
3208 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3209 let mut bytes = hw1.to_le_bytes().to_vec();
3210 bytes.extend_from_slice(&hw2.to_le_bytes());
3211 Ok(bytes)
3212 } else {
3213 let instr: u16 = 0xBF00;
3214 Ok(instr.to_le_bytes().to_vec())
3215 }
3216 }
3217
3218 // EOR (Thumb-2 32-bit)
3219 ArmOp::Eor { rd, rn, op2 } => {
3220 if let Operand2::Reg(rm) = op2 {
3221 let rd_bits = reg_to_bits(rd);
3222 let rn_bits = reg_to_bits(rn);
3223 let rm_bits = reg_to_bits(rm);
3224
3225 // Thumb-2 EOR: EA80 Rn | 0 Rd 00 00 Rm
3226 let hw1: u16 = (0xEA80 | rn_bits) as u16;
3227 let hw2: u16 = ((rd_bits << 8) | rm_bits) as u16;
3228
3229 let mut bytes = hw1.to_le_bytes().to_vec();
3230 bytes.extend_from_slice(&hw2.to_le_bytes());
3231 Ok(bytes)
3232 } else if let Operand2::Imm(imm) = op2 {
3233 // EOR.W immediate T1: 11110 i 0 0100 S Rn | 0 imm3 Rd imm8.
3234 // Byte form only (imm <= 0xFF); larger needs ThumbExpandImm —
3235 // error, not a silent NOP (Ok-or-Err, #180/#185).
3236 let imm_val = *imm as u32;
3237 if imm_val > 0xFF {
3238 return Err(synth_core::Error::synthesis(
3239 "EOR immediate > 0xFF requires ThumbExpandImm (not yet implemented)",
3240 ));
3241 }
3242 let rd_bits = reg_to_bits(rd);
3243 let rn_bits = reg_to_bits(rn);
3244 let hw1: u16 = (0xF080 | rn_bits) as u16;
3245 let hw2: u16 = ((rd_bits << 8) | (imm_val & 0xFF)) as u16;
3246 let mut bytes = hw1.to_le_bytes().to_vec();
3247 bytes.extend_from_slice(&hw2.to_le_bytes());
3248 Ok(bytes)
3249 } else {
3250 let instr: u16 = 0xBF00;
3251 Ok(instr.to_le_bytes().to_vec())
3252 }
3253 }
3254
3255 // Shift operations (16-bit for low registers)
3256 ArmOp::Lsl { rd, rn, shift } => {
3257 let rd_bits = reg_to_bits(rd) as u16;
3258 let rn_bits = reg_to_bits(rn) as u16;
3259 let shift_bits = (*shift as u16) & 0x1F;
3260
3261 if rd_bits < 8 && rn_bits < 8 {
3262 // LSLS Rd, Rm, #imm5 (16-bit): 0000 0 imm5 Rm Rd
3263 let instr: u16 = (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3264 Ok(instr.to_le_bytes().to_vec())
3265 } else {
3266 // Use 32-bit encoding for high registers
3267 self.encode_thumb32_shift(rd, rn, *shift, 0b00) // LSL type
3268 }
3269 }
3270
3271 ArmOp::Lsr { rd, rn, shift } => {
3272 let rd_bits = reg_to_bits(rd) as u16;
3273 let rn_bits = reg_to_bits(rn) as u16;
3274 let shift_bits = (*shift as u16) & 0x1F;
3275
3276 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3277 // LSRS Rd, Rm, #imm5 (16-bit): 0000 1 imm5 Rm Rd
3278 let instr: u16 = 0x0800 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3279 Ok(instr.to_le_bytes().to_vec())
3280 } else {
3281 self.encode_thumb32_shift(rd, rn, *shift, 0b01) // LSR type
3282 }
3283 }
3284
3285 ArmOp::Asr { rd, rn, shift } => {
3286 let rd_bits = reg_to_bits(rd) as u16;
3287 let rn_bits = reg_to_bits(rn) as u16;
3288 let shift_bits = (*shift as u16) & 0x1F;
3289
3290 if rd_bits < 8 && rn_bits < 8 && shift_bits > 0 {
3291 // ASRS Rd, Rm, #imm5 (16-bit): 0001 0 imm5 Rm Rd
3292 let instr: u16 = 0x1000 | (shift_bits << 6) | (rn_bits << 3) | rd_bits;
3293 Ok(instr.to_le_bytes().to_vec())
3294 } else {
3295 self.encode_thumb32_shift(rd, rn, *shift, 0b10) // ASR type
3296 }
3297 }
3298
3299 ArmOp::Ror { rd, rn, shift } => {
3300 // ROR doesn't have a 16-bit immediate form, use 32-bit
3301 self.encode_thumb32_shift(rd, rn, *shift, 0b11) // ROR type
3302 }
3303
3304 // Register-based shifts (Thumb-2 32-bit)
3305 // Encoding: 11111010 0xxS Rn 1111 Rd 0000 Rm
3306 // xx = shift type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
3307 ArmOp::LslReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b00),
3308 ArmOp::LsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b01),
3309 ArmOp::AsrReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b10),
3310 ArmOp::RorReg { rd, rn, rm } => self.encode_thumb32_shift_reg(rd, rn, rm, 0b11),
3311
3312 // RSB (Reverse Subtract): Rd = imm - Rn
3313 // Thumb-2 T2 encoding: 11110 i 0 1110 S Rn | 0 imm3 Rd imm8
3314 ArmOp::Rsb { rd, rn, imm } => {
3315 let rd_bits = reg_to_bits(rd);
3316 let rn_bits = reg_to_bits(rn);
3317
3318 // #681 class audit: the T2 `i:imm3:imm8` field is a
3319 // ThumbExpandImm modified immediate and RSB has NO plain-imm12
3320 // (T4-style) form — packing a raw value > 0xFF silently encodes
3321 // a different constant (#253/#255 class). All current emitters
3322 // use imm 32 (shift complement), which expands to itself, so
3323 // this gate is byte-identical for existing codegen.
3324 let field = try_thumb_expand_imm(*imm).ok_or_else(|| {
3325 synth_core::Error::synthesis(
3326 "RSB immediate is not a valid ThumbExpandImm — materialize into a register",
3327 )
3328 })?;
3329 let i_bit = (field >> 11) & 1;
3330 let imm3 = (field >> 8) & 0x7;
3331 let imm8 = field & 0xFF;
3332
3333 // hw1: 11110 i 01110 0 Rn (S=0)
3334 let hw1: u16 = (0xF1C0 | (i_bit << 10) | rn_bits) as u16;
3335 // hw2: 0 imm3 Rd imm8
3336 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
3337
3338 let mut bytes = hw1.to_le_bytes().to_vec();
3339 bytes.extend_from_slice(&hw2.to_le_bytes());
3340 Ok(bytes)
3341 }
3342
3343 // CLZ (Thumb-2 32-bit)
3344 ArmOp::Clz { rd, rm } => {
3345 let rd_bits = reg_to_bits(rd);
3346 let rm_bits = reg_to_bits(rm);
3347
3348 // Thumb-2 CLZ: FAB0 Rm | F8 Rd Rm
3349 // 11111010 1011 Rm | 1111 1000 Rd Rm
3350 let hw1: u16 = (0xFAB0 | rm_bits) as u16;
3351 let hw2: u16 = (0xF080 | (rd_bits << 8) | rm_bits) as u16;
3352
3353 let mut bytes = hw1.to_le_bytes().to_vec();
3354 bytes.extend_from_slice(&hw2.to_le_bytes());
3355 Ok(bytes)
3356 }
3357
3358 // RBIT (Thumb-2 32-bit)
3359 ArmOp::Rbit { rd, rm } => {
3360 let rd_bits = reg_to_bits(rd);
3361 let rm_bits = reg_to_bits(rm);
3362
3363 // Thumb-2 RBIT: FA90 Rm | F0 Rd A0 Rm
3364 // 11111010 1001 Rm | 1111 Rd 1010 Rm
3365 let hw1: u16 = (0xFA90 | rm_bits) as u16;
3366 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rm_bits) as u16;
3367
3368 let mut bytes = hw1.to_le_bytes().to_vec();
3369 bytes.extend_from_slice(&hw2.to_le_bytes());
3370 Ok(bytes)
3371 }
3372
3373 // SXTB (16-bit for low registers)
3374 ArmOp::Sxtb { rd, rm } => {
3375 let rd_bits = reg_to_bits(rd) as u16;
3376 let rm_bits = reg_to_bits(rm) as u16;
3377
3378 if rd_bits < 8 && rm_bits < 8 {
3379 // SXTB Rd, Rm (16-bit): 1011 0010 01 Rm Rd
3380 let instr: u16 = 0xB240 | (rm_bits << 3) | rd_bits;
3381 Ok(instr.to_le_bytes().to_vec())
3382 } else {
3383 // Thumb-2 SXTB.W: FA4F F(rd)80 (rm)
3384 // 11111010 0100 1111 | 1111 Rd 10 rotate Rm
3385 let rd_bits32 = rd_bits as u32;
3386 let rm_bits32 = rm_bits as u32;
3387 let hw1: u16 = 0xFA4F;
3388 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
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
3395 // SXTH (16-bit for low registers)
3396 ArmOp::Sxth { rd, rm } => {
3397 let rd_bits = reg_to_bits(rd) as u16;
3398 let rm_bits = reg_to_bits(rm) as u16;
3399
3400 if rd_bits < 8 && rm_bits < 8 {
3401 // SXTH Rd, Rm (16-bit): 1011 0010 00 Rm Rd
3402 let instr: u16 = 0xB200 | (rm_bits << 3) | rd_bits;
3403 Ok(instr.to_le_bytes().to_vec())
3404 } else {
3405 // Thumb-2 SXTH.W: FA0F F(rd)80 (rm)
3406 // 11111010 0000 1111 | 1111 Rd 10 rotate Rm
3407 let rd_bits32 = rd_bits as u32;
3408 let rm_bits32 = rm_bits as u32;
3409 let hw1: u16 = 0xFA0F;
3410 let hw2: u16 = (0xF080 | (rd_bits32 << 8) | rm_bits32) as u16;
3411 let mut bytes = hw1.to_le_bytes().to_vec();
3412 bytes.extend_from_slice(&hw2.to_le_bytes());
3413 Ok(bytes)
3414 }
3415 }
3416
3417 // UXTB Rd,Rm — zero-extend byte (rd = rm & 0xff)
3418 ArmOp::Uxtb { rd, rm } => {
3419 let rd_bits = reg_to_bits(rd) as u16;
3420 let rm_bits = reg_to_bits(rm) as u16;
3421 if rd_bits < 8 && rm_bits < 8 {
3422 // UXTB Rd, Rm (16-bit): 1011 0010 11 Rm Rd
3423 let instr: u16 = 0xB2C0 | (rm_bits << 3) | rd_bits;
3424 Ok(instr.to_le_bytes().to_vec())
3425 } else {
3426 // Thumb-2 UXTB.W: FA5F F(rd)80 (rm)
3427 let hw1: u16 = 0xFA5F;
3428 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3429 let mut bytes = hw1.to_le_bytes().to_vec();
3430 bytes.extend_from_slice(&hw2.to_le_bytes());
3431 Ok(bytes)
3432 }
3433 }
3434
3435 // UXTH Rd,Rm — zero-extend halfword (rd = rm & 0xffff)
3436 ArmOp::Uxth { rd, rm } => {
3437 let rd_bits = reg_to_bits(rd) as u16;
3438 let rm_bits = reg_to_bits(rm) as u16;
3439 if rd_bits < 8 && rm_bits < 8 {
3440 // UXTH Rd, Rm (16-bit): 1011 0010 10 Rm Rd
3441 let instr: u16 = 0xB280 | (rm_bits << 3) | rd_bits;
3442 Ok(instr.to_le_bytes().to_vec())
3443 } else {
3444 // Thumb-2 UXTH.W: FA1F F(rd)80 (rm)
3445 let hw1: u16 = 0xFA1F;
3446 let hw2: u16 = (0xF080 | ((rd_bits as u32) << 8) | rm_bits as u32) as u16;
3447 let mut bytes = hw1.to_le_bytes().to_vec();
3448 bytes.extend_from_slice(&hw2.to_le_bytes());
3449 Ok(bytes)
3450 }
3451 }
3452
3453 // CMP (can be 16-bit for low registers)
3454 ArmOp::Cmp { rn, op2 } => {
3455 let rn_bits = reg_to_bits(rn) as u16;
3456
3457 if let Operand2::Imm(imm) = op2 {
3458 // Only use 16-bit encoding for non-negative immediates 0-255
3459 // Negative immediates must use 32-bit encoding
3460 if *imm >= 0 && *imm <= 255 && rn_bits < 8 {
3461 // CMP Rn, #imm8 (16-bit): 0010 1 Rn imm8
3462 let instr: u16 = 0x2800 | (rn_bits << 8) | (*imm as u16 & 0xFF);
3463 Ok(instr.to_le_bytes().to_vec())
3464 } else {
3465 self.encode_thumb32_cmp_imm(rn, *imm as u32)
3466 }
3467 } else if let Operand2::Reg(rm) = op2 {
3468 let rm_bits = reg_to_bits(rm) as u16;
3469 if rn_bits < 8 && rm_bits < 8 {
3470 // CMP Rn, Rm (16-bit low): 0100 0010 10 Rm Rn
3471 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
3472 Ok(instr.to_le_bytes().to_vec())
3473 } else {
3474 // CMP Rn, Rm (16-bit high): 0100 0101 N Rm Rn[2:0]
3475 let n_bit = (rn_bits >> 3) & 1;
3476 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
3477 Ok(instr.to_le_bytes().to_vec())
3478 }
3479 } else {
3480 let instr: u16 = 0xBF00;
3481 Ok(instr.to_le_bytes().to_vec())
3482 }
3483 }
3484
3485 // CMN (Compare Negative) - computes Rn + op2 and sets flags
3486 // CMN Rn, #1 sets Z flag if Rn == -1 (since -1 + 1 = 0)
3487 ArmOp::Cmn { rn, op2 } => {
3488 let rn_bits = reg_to_bits(rn) as u16;
3489
3490 if let Operand2::Imm(imm) = op2 {
3491 // CMN.W Rn, #imm (32-bit): i:imm3:imm8 is a ThumbExpandImm
3492 // modified immediate (the field sits in imm3=hw2[14:12],
3493 // imm8=hw2[7:0], i=hw1[10]). Encode it correctly, or error on
3494 // an un-encodable value — replacing the old silent `0xBF00`
3495 // NOP (the last of the silent-miscompile data-proc encoders).
3496 let field = try_thumb_expand_imm(*imm as u32).ok_or_else(|| {
3497 synth_core::Error::synthesis(
3498 "CMN immediate is not a valid ThumbExpandImm — materialize into a register",
3499 )
3500 })?;
3501 let i_bit = (field >> 11) & 1;
3502 let imm3 = (field >> 8) & 0x7;
3503 let imm8 = field & 0xFF;
3504 let hw1: u16 = (0xF110 | (i_bit << 10) as u16) | rn_bits;
3505 let hw2: u16 = (imm3 << 12) as u16 | 0x0F00 | imm8 as u16;
3506 let mut bytes = hw1.to_le_bytes().to_vec();
3507 bytes.extend_from_slice(&hw2.to_le_bytes());
3508 Ok(bytes)
3509 } else if let Operand2::Reg(rm) = op2 {
3510 let rm_bits = reg_to_bits(rm) as u16;
3511 // 16-bit CMN (T1) only encodes R0-R7; high registers overflow
3512 // the 3-bit fields and corrupt the operands (#184, the #180
3513 // class). CMN has no high-register 16-bit form, so fall back
3514 // to 32-bit CMN.W (T2): EB10 Rn | 0F00 Rm (ADD.W with S=1 and
3515 // Rd discarded as PC/1111).
3516 if rn_bits < 8 && rm_bits < 8 {
3517 // CMN Rn, Rm (16-bit): 0100 0010 11 Rm Rn
3518 let instr: u16 = 0x42C0 | (rm_bits << 3) | rn_bits;
3519 Ok(instr.to_le_bytes().to_vec())
3520 } else {
3521 let hw1: u16 = 0xEB10 | rn_bits;
3522 let hw2: u16 = 0x0F00 | rm_bits;
3523 let mut bytes = hw1.to_le_bytes().to_vec();
3524 bytes.extend_from_slice(&hw2.to_le_bytes());
3525 Ok(bytes)
3526 }
3527 } else {
3528 Ok(vec![0xBF, 0x00])
3529 }
3530 }
3531
3532 // LDR (can be 16-bit for simple cases)
3533 ArmOp::Ldr { rd, addr } => {
3534 let rd_bits = reg_to_bits(rd);
3535 let base_bits = reg_to_bits(&addr.base);
3536
3537 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3538 if let Some(offset_reg) = &addr.offset_reg {
3539 let rm_bits = reg_to_bits(offset_reg);
3540
3541 // If there's also an immediate offset, we need to ADD it first
3542 if addr.offset != 0 {
3543 // Use R12 (IP) as scratch to avoid clobbering the address register
3544 // ADD R12, Rm, #offset; LDR Rd, [base, R12]
3545 let scratch = Reg::R12;
3546 let mut bytes =
3547 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3548 bytes.extend(self.encode_thumb32_ldr_reg(rd, &addr.base, &scratch)?);
3549 return Ok(bytes);
3550 }
3551
3552 // Simple register offset: LDR Rd, [Rn, Rm]
3553 // 16-bit: only if Rd, Rn, Rm < R8
3554 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3555 // LDR Rd, [Rn, Rm] (16-bit): 0101 100 Rm Rn Rd
3556 let instr: u16 = 0x5800
3557 | ((rm_bits as u16) << 6)
3558 | ((base_bits as u16) << 3)
3559 | (rd_bits as u16);
3560 return Ok(instr.to_le_bytes().to_vec());
3561 }
3562
3563 // 32-bit register offset
3564 return self.encode_thumb32_ldr_reg(rd, &addr.base, offset_reg);
3565 }
3566
3567 // Immediate offset mode [base, #imm]
3568 let offset = addr.offset as u32;
3569
3570 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3571 // LDR Rd, [Rn, #imm5*4] (16-bit): 0110 1 imm5 Rn Rd
3572 let imm5 = (offset >> 2) as u16;
3573 let instr: u16 =
3574 0x6800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3575 Ok(instr.to_le_bytes().to_vec())
3576 } else {
3577 self.encode_thumb32_ldr(rd, &addr.base, offset)
3578 }
3579 }
3580
3581 // STR (can be 16-bit for simple cases)
3582 ArmOp::Str { rd, addr } => {
3583 let rd_bits = reg_to_bits(rd);
3584 let base_bits = reg_to_bits(&addr.base);
3585
3586 // Handle register offset mode [base, Roff] or [base, Roff, #imm]
3587 if let Some(offset_reg) = &addr.offset_reg {
3588 let rm_bits = reg_to_bits(offset_reg);
3589
3590 // If there's also an immediate offset, we need to ADD it first
3591 if addr.offset != 0 {
3592 // Use R12 (IP) as scratch to avoid clobbering the address register
3593 // ADD R12, Rm, #offset; STR Rd, [base, R12]
3594 let scratch = Reg::R12;
3595 let mut bytes =
3596 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3597 bytes.extend(self.encode_thumb32_str_reg(rd, &addr.base, &scratch)?);
3598 return Ok(bytes);
3599 }
3600
3601 // Simple register offset: STR Rd, [Rn, Rm]
3602 // 16-bit: only if Rd, Rn, Rm < R8
3603 if rd_bits < 8 && base_bits < 8 && rm_bits < 8 {
3604 // STR Rd, [Rn, Rm] (16-bit): 0101 000 Rm Rn Rd
3605 let instr: u16 = 0x5000
3606 | ((rm_bits as u16) << 6)
3607 | ((base_bits as u16) << 3)
3608 | (rd_bits as u16);
3609 return Ok(instr.to_le_bytes().to_vec());
3610 }
3611
3612 // 32-bit register offset
3613 return self.encode_thumb32_str_reg(rd, &addr.base, offset_reg);
3614 }
3615
3616 // Immediate offset mode [base, #imm]
3617 let offset = addr.offset as u32;
3618
3619 if rd_bits < 8 && base_bits < 8 && (offset & 0x3) == 0 && offset <= 124 {
3620 // STR Rd, [Rn, #imm5*4] (16-bit): 0110 0 imm5 Rn Rd
3621 let imm5 = (offset >> 2) as u16;
3622 let instr: u16 =
3623 0x6000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3624 Ok(instr.to_le_bytes().to_vec())
3625 } else {
3626 self.encode_thumb32_str(rd, &addr.base, offset)
3627 }
3628 }
3629
3630 // LDRB (Thumb-2)
3631 ArmOp::Ldrb { rd, addr } => {
3632 let rd_bits = reg_to_bits(rd);
3633 let base_bits = reg_to_bits(&addr.base);
3634
3635 if let Some(offset_reg) = &addr.offset_reg {
3636 if addr.offset != 0 {
3637 let scratch = Reg::R12;
3638 let mut bytes =
3639 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3640 bytes.extend(self.encode_thumb32_ldrb_reg(rd, &addr.base, &scratch)?);
3641 return Ok(bytes);
3642 }
3643 return self.encode_thumb32_ldrb_reg(rd, &addr.base, offset_reg);
3644 }
3645
3646 let offset = addr.offset as u32;
3647 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3648 // LDRB Rd, [Rn, #imm5] (16-bit): 0111 1 imm5 Rn Rd
3649 let instr: u16 = 0x7800
3650 | ((offset as u16) << 6)
3651 | ((base_bits as u16) << 3)
3652 | (rd_bits as u16);
3653 Ok(instr.to_le_bytes().to_vec())
3654 } else {
3655 self.encode_thumb32_ldrb_imm(rd, &addr.base, offset)
3656 }
3657 }
3658
3659 // LDRSB (Thumb-2)
3660 ArmOp::Ldrsb { rd, addr } => {
3661 let rd_bits = reg_to_bits(rd);
3662 let base_bits = reg_to_bits(&addr.base);
3663
3664 if let Some(offset_reg) = &addr.offset_reg {
3665 if addr.offset != 0 {
3666 let scratch = Reg::R12;
3667 let mut bytes =
3668 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3669 bytes.extend(self.encode_thumb32_ldrsb_reg(rd, &addr.base, &scratch)?);
3670 return Ok(bytes);
3671 }
3672 return self.encode_thumb32_ldrsb_reg(rd, &addr.base, offset_reg);
3673 }
3674
3675 let offset = addr.offset as u32;
3676 // LDRSB has no 16-bit immediate form (only register)
3677 // For 16-bit reg form: only if Rd, Rn, Rm < R8
3678 if rd_bits < 8 && base_bits < 8 && offset == 0 {
3679 // No immediate 16-bit encoding for LDRSB; use 32-bit
3680 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3681 } else {
3682 self.encode_thumb32_ldrsb_imm(rd, &addr.base, offset)
3683 }
3684 }
3685
3686 // LDRH (Thumb-2)
3687 ArmOp::Ldrh { rd, addr } => {
3688 let rd_bits = reg_to_bits(rd);
3689 let base_bits = reg_to_bits(&addr.base);
3690
3691 if let Some(offset_reg) = &addr.offset_reg {
3692 if addr.offset != 0 {
3693 let scratch = Reg::R12;
3694 let mut bytes =
3695 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3696 bytes.extend(self.encode_thumb32_ldrh_reg(rd, &addr.base, &scratch)?);
3697 return Ok(bytes);
3698 }
3699 return self.encode_thumb32_ldrh_reg(rd, &addr.base, offset_reg);
3700 }
3701
3702 let offset = addr.offset as u32;
3703 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3704 // LDRH Rd, [Rn, #imm5*2] (16-bit): 1000 1 imm5 Rn Rd
3705 let imm5 = (offset >> 1) as u16;
3706 let instr: u16 =
3707 0x8800 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3708 Ok(instr.to_le_bytes().to_vec())
3709 } else {
3710 self.encode_thumb32_ldrh_imm(rd, &addr.base, offset)
3711 }
3712 }
3713
3714 // LDRSH (Thumb-2)
3715 ArmOp::Ldrsh { rd, addr } => {
3716 if let Some(offset_reg) = &addr.offset_reg {
3717 if addr.offset != 0 {
3718 let scratch = Reg::R12;
3719 let mut bytes =
3720 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3721 bytes.extend(self.encode_thumb32_ldrsh_reg(rd, &addr.base, &scratch)?);
3722 return Ok(bytes);
3723 }
3724 return self.encode_thumb32_ldrsh_reg(rd, &addr.base, offset_reg);
3725 }
3726
3727 let offset = addr.offset as u32;
3728 self.encode_thumb32_ldrsh_imm(rd, &addr.base, offset)
3729 }
3730
3731 // STRB (Thumb-2)
3732 ArmOp::Strb { rd, addr } => {
3733 let rd_bits = reg_to_bits(rd);
3734 let base_bits = reg_to_bits(&addr.base);
3735
3736 if let Some(offset_reg) = &addr.offset_reg {
3737 if addr.offset != 0 {
3738 let scratch = Reg::R12;
3739 let mut bytes =
3740 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3741 bytes.extend(self.encode_thumb32_strb_reg(rd, &addr.base, &scratch)?);
3742 return Ok(bytes);
3743 }
3744 return self.encode_thumb32_strb_reg(rd, &addr.base, offset_reg);
3745 }
3746
3747 let offset = addr.offset as u32;
3748 if rd_bits < 8 && base_bits < 8 && offset <= 31 {
3749 // STRB Rd, [Rn, #imm5] (16-bit): 0111 0 imm5 Rn Rd
3750 let instr: u16 = 0x7000
3751 | ((offset as u16) << 6)
3752 | ((base_bits as u16) << 3)
3753 | (rd_bits as u16);
3754 Ok(instr.to_le_bytes().to_vec())
3755 } else {
3756 self.encode_thumb32_strb_imm(rd, &addr.base, offset)
3757 }
3758 }
3759
3760 // STRH (Thumb-2)
3761 ArmOp::Strh { rd, addr } => {
3762 let rd_bits = reg_to_bits(rd);
3763 let base_bits = reg_to_bits(&addr.base);
3764
3765 if let Some(offset_reg) = &addr.offset_reg {
3766 if addr.offset != 0 {
3767 let scratch = Reg::R12;
3768 let mut bytes =
3769 self.encode_thumb32_add_imm(&scratch, offset_reg, addr.offset as u32)?;
3770 bytes.extend(self.encode_thumb32_strh_reg(rd, &addr.base, &scratch)?);
3771 return Ok(bytes);
3772 }
3773 return self.encode_thumb32_strh_reg(rd, &addr.base, offset_reg);
3774 }
3775
3776 let offset = addr.offset as u32;
3777 if rd_bits < 8 && base_bits < 8 && (offset & 0x1) == 0 && offset <= 62 {
3778 // STRH Rd, [Rn, #imm5*2] (16-bit): 1000 0 imm5 Rn Rd
3779 let imm5 = (offset >> 1) as u16;
3780 let instr: u16 =
3781 0x8000 | (imm5 << 6) | ((base_bits as u16) << 3) | (rd_bits as u16);
3782 Ok(instr.to_le_bytes().to_vec())
3783 } else {
3784 self.encode_thumb32_strh_imm(rd, &addr.base, offset)
3785 }
3786 }
3787
3788 // MemorySize (Thumb-2)
3789 ArmOp::MemorySize { rd } => {
3790 // LSR rd, R10, #16 — memory size in bytes / 65536 = pages
3791 // Thumb-2 16-bit: LSRS Rd, Rm, #imm5 — 0000 1 imm5 Rm Rd
3792 let rd_bits = reg_to_bits(rd);
3793 let r10_bits = reg_to_bits(&Reg::R10);
3794 if rd_bits < 8 && r10_bits < 8 {
3795 let instr: u16 =
3796 0x0800 | (16u16 << 6) | ((r10_bits as u16) << 3) | (rd_bits as u16);
3797 Ok(instr.to_le_bytes().to_vec())
3798 } else {
3799 // Thumb-2 32-bit LSR: 1110 1010 010 0 1111 | 0 imm3 Rd imm2 01 Rm
3800 let imm5: u32 = 16;
3801 let imm3 = (imm5 >> 2) & 0x7;
3802 let imm2 = imm5 & 0x3;
3803 let hw1: u16 = 0xEA4F;
3804 let hw2: u16 =
3805 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | 0x10 | r10_bits) as u16;
3806 let mut bytes = hw1.to_le_bytes().to_vec();
3807 bytes.extend_from_slice(&hw2.to_le_bytes());
3808 Ok(bytes)
3809 }
3810 }
3811
3812 // MemoryGrow (Thumb-2)
3813 ArmOp::MemoryGrow { rd, .. } => {
3814 // On embedded with fixed memory, always return -1 (failure)
3815 // MVN rd, #0 → MOV rd, #-1
3816 // Thumb-2 32-bit: MVN: 1111 0 i 0 0 0 1 1 0 1111 | 0 imm3 Rd imm8
3817 let rd_bits = reg_to_bits(rd);
3818 let hw1: u16 = 0xF06F; // MVN with i=0
3819 let hw2: u16 = (rd_bits << 8) as u16; // imm8=0 → ~0 = 0xFFFFFFFF = -1
3820 let mut bytes = hw1.to_le_bytes().to_vec();
3821 bytes.extend_from_slice(&hw2.to_le_bytes());
3822 Ok(bytes)
3823 }
3824
3825 // BX (16-bit)
3826 ArmOp::Bx { rm } => {
3827 let rm_bits = reg_to_bits(rm) as u16;
3828 // BX Rm (16-bit): 0100 0111 0 Rm 000
3829 let instr: u16 = 0x4700 | (rm_bits << 3);
3830 Ok(instr.to_le_bytes().to_vec())
3831 }
3832
3833 // BLX (16-bit) - Branch with Link and Exchange
3834 // BLX Rm: 0100 0111 1 Rm 000
3835 ArmOp::Blx { rm } => {
3836 let rm_bits = reg_to_bits(rm) as u16;
3837 let instr: u16 = 0x4780 | (rm_bits << 3);
3838 Ok(instr.to_le_bytes().to_vec())
3839 }
3840
3841 // CallIndirect - indirect function call via table lookup
3842 // table_index_reg contains the table index
3843 // Generates (#642): MOVW ip,#size [; MOVT]; CMP idx,ip; BLO +1;
3844 // UDF #0; LSL R12,idx,#2; LDR R12,[R11,R12]; BLX R12
3845 // #650, table_byte_offset != 0 (a non-zero table in the contiguous
3846 // R11 region): the pointer load becomes
3847 // ADD R12,R11,R12; LDR R12,[R12,#offset]
3848 // #664, null_check (the table has null slots, linked as ZERO
3849 // words): the loaded pointer is null-checked before the BLX —
3850 // CMP.W R12,#0; BNE +1; UDF #0
3851 // #676, type_check (heterogeneous table — runtime §4.4.8 type
3852 // check against the type-id sidecar at R11+off): after the
3853 // bounds guard —
3854 // LSL R12,idx,#2; ADD R12,R11,R12;
3855 // LDR R12,[R12,#type_off]; CMP.W R12,#id;
3856 // BEQ +1; UDF #0
3857 // (the dispatch tail then recomputes idx*4 — idx stays live).
3858 ArmOp::CallIndirect {
3859 rd: _,
3860 type_idx: _,
3861 table_index_reg,
3862 table_size,
3863 table_byte_offset,
3864 null_check,
3865 type_check,
3866 } => {
3867 let idx_reg = reg_to_bits(table_index_reg);
3868 let mut bytes = Vec::new();
3869
3870 // The expansion:
3871 // 1. Bounds guard (#642): trap (UDF #0, WASM Core §4.4.8) when
3872 // index >= table size. Without it an out-of-bounds index
3873 // reads past the table and BLXes whatever word lies there —
3874 // an uncontrolled indirect branch instead of a trap.
3875 // 2. Multiplies index by 4 (function pointer size)
3876 // 3. Loads function pointer from table (table base in R11)
3877 // 4. Calls the function via BLX
3878 //
3879 // Table base setup must be done by caller/runtime. The type
3880 // check §4.4.8 also requires is discharged at COMPILE time:
3881 // the selector only emits this op after verifying the closed-
3882 // world property that every table entry's signature equals the
3883 // expected type (the raw code-pointer table carries no runtime
3884 // type ids to compare) — see the #642 selector guard.
3885
3886 // MOVW R12, #(size & 0xFFFF) — Thumb-2 T3:
3887 // 11110 i 100100 imm4 | 0 imm3 Rd imm8 (Rd=R12).
3888 let size_lo = *table_size & 0xFFFF;
3889 let hw1: u16 =
3890 (0xF240 | (((size_lo >> 11) & 1) << 10) | ((size_lo >> 12) & 0xF)) as u16;
3891 let hw2: u16 =
3892 ((((size_lo >> 8) & 0x7) << 12) | (12 << 8) | (size_lo & 0xFF)) as u16;
3893 bytes.extend_from_slice(&hw1.to_le_bytes());
3894 bytes.extend_from_slice(&hw2.to_le_bytes());
3895 // MOVT R12, #(size >> 16) — only when the table size exceeds
3896 // 16 bits (never in practice, but the guard must not compare
3897 // against a truncated size).
3898 let size_hi = *table_size >> 16;
3899 if size_hi != 0 {
3900 let hw1: u16 =
3901 (0xF2C0 | (((size_hi >> 11) & 1) << 10) | ((size_hi >> 12) & 0xF)) as u16;
3902 let hw2: u16 =
3903 ((((size_hi >> 8) & 0x7) << 12) | (12 << 8) | (size_hi & 0xFF)) as u16;
3904 bytes.extend_from_slice(&hw1.to_le_bytes());
3905 bytes.extend_from_slice(&hw2.to_le_bytes());
3906 }
3907 // CMP idx, R12 — 16-bit T2 (high-register capable):
3908 // 010001 01 N Rm(4) Rn(3), Rn full = N:Rn3.
3909 let cmp: u16 = (0x4500 | ((idx_reg & 8) << 4) | (12 << 3) | (idx_reg & 7)) as u16;
3910 bytes.extend_from_slice(&cmp.to_le_bytes());
3911 // BLO +1 insn (skip the UDF when index < size) — B<cond>.N
3912 // imm8=0: target = branch + 4. LO = unsigned lower.
3913 bytes.extend_from_slice(&0xD300u16.to_le_bytes());
3914 // UDF #0 — call_indirect out-of-bounds trap (same trap idiom as
3915 // the div-by-zero guards).
3916 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3917
3918 // #676: runtime type check — ONLY for a heterogeneous table
3919 // (mixed signatures, closed-world verdict impossible). Load
3920 // the indexed slot's structural class id from the type-id
3921 // sidecar (`R11 + type_off + idx*4`; `type_off` = sidecar
3922 // base + this table's base offset, a compile-time constant)
3923 // and compare it against the expected type's class id — a
3924 // mismatch is the WASM Core §4.4.8 type trap. Null slots
3925 // carry the reserved id 0, so this compare subsumes the
3926 // #664 null trap (the selector passes `null_check: false`).
3927 // `None` emits NOTHING: every homogeneous table keeps the
3928 // pre-#676 bytes identical BY CONSTRUCTION. R12 stays the
3929 // only scratch (#212); the dispatch tail below recomputes
3930 // idx*4 — the index register is never clobbered here.
3931 if let Some((expected_id, type_off)) = type_check {
3932 debug_assert!(*expected_id <= 255, "selector enforces the CMP imm8 range");
3933 debug_assert!(*type_off <= 4095, "selector enforces the LDR imm12 range");
3934 // MOV.W R12, idx, LSL #2 (same encoding as the dispatch
3935 // tail's index scale below).
3936 bytes.extend_from_slice(&0xEA4Fu16.to_le_bytes());
3937 bytes.extend_from_slice(
3938 &(((0x0C00 | (0b10 << 6)) | idx_reg) as u16).to_le_bytes(),
3939 );
3940 // ADD.W R12, R11, R12 (the #650 base-add form).
3941 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3942 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
3943 // LDR.W R12, [R12, #type_off] — T3 LDR (immediate):
3944 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12.
3945 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
3946 bytes.extend_from_slice(
3947 &(0xC000u16 | (*type_off as u16 & 0x0FFF)).to_le_bytes(),
3948 );
3949 // CMP.W R12, #expected_id — T2 CMP (immediate), imm8
3950 // (same form as the #664 null check's CMP.W R12, #0).
3951 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
3952 bytes.extend_from_slice(
3953 &(0x0F00u16 | (*expected_id as u16 & 0xFF)).to_le_bytes(),
3954 );
3955 // BEQ +1 insn (skip the UDF when the class id matches) —
3956 // B<cond>.N imm8=0: target = branch + 4. EQ.
3957 bytes.extend_from_slice(&0xD000u16.to_le_bytes());
3958 // UDF #0 — the §4.4.8 type-mismatch trap (same idiom as
3959 // the bounds guard above).
3960 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
3961 }
3962
3963 // LSL R12, idx_reg, #2 (multiply index by 4)
3964 // Thumb-2 MOV with shift: 11101010 010 S 1111 | 0 imm3 Rd imm2 type Rm
3965 // LSL: type=00 (bits 5:4), imm5=2 -> imm3=000, imm2=10 (bits 7:6)
3966 // #597: the shift amount was previously shifted into bits 5:4 —
3967 // the TYPE field — encoding `mov.w ip, rm, ASR #32`, which
3968 // destroyed the index and dispatched table entry 0 for every
3969 // call. imm2 lives at bits 7:6.
3970 let hw1: u16 = 0xEA4F_u16; // MOV.W R12, Rm, LSL #2
3971 let hw2: u16 = ((0x0C00 | (0b10 << 6)) | idx_reg) as u16;
3972 bytes.extend_from_slice(&hw1.to_le_bytes());
3973 bytes.extend_from_slice(&hw2.to_le_bytes());
3974
3975 if *table_byte_offset == 0 {
3976 // Table 0 (base = R11 itself): the pre-#650 single-load
3977 // form — a single-table module's bytes stay identical BY
3978 // CONSTRUCTION.
3979 // LDR R12, [R11, R12] - load function pointer
3980 // Thumb-2 LDR (register): 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
3981 // Rn=R11, Rt=R12, Rm=R12, imm2=00 (no shift)
3982 let ldr_hw1: u16 = 0xF85B; // LDR.W Rt, [R11, Rm]
3983 let ldr_hw2: u16 = 0xC00C; // Rt=R12, imm2=00, Rm=R12
3984 bytes.extend_from_slice(&ldr_hw1.to_le_bytes());
3985 bytes.extend_from_slice(&ldr_hw2.to_le_bytes());
3986 } else {
3987 // #650: table N of the contiguous R11 region — fold the
3988 // compile-time base offset into the pointer load via the
3989 // LDR imm12 form (R12 stays the only scratch, per the
3990 // #212 convention).
3991 assert!(
3992 *table_byte_offset <= 4095,
3993 "call_indirect table base offset {table_byte_offset} exceeds \
3994 LDR imm12 — the selector must have declined this (#650)"
3995 );
3996 // ADD.W R12, R11, R12 — T3 ADD (register):
3997 // 11101011000 S=0 Rn=1011 | 0 imm3=000 Rd=1100 imm2=00 type=00 Rm=1100
3998 bytes.extend_from_slice(&0xEB0Bu16.to_le_bytes());
3999 bytes.extend_from_slice(&0x0C0Cu16.to_le_bytes());
4000 // LDR.W R12, [R12, #offset] — T3 LDR (immediate):
4001 // 1111 1000 1101 Rn=1100 | Rt=1100 imm12
4002 bytes.extend_from_slice(&0xF8DCu16.to_le_bytes());
4003 bytes.extend_from_slice(
4004 &((0xC000u16) | (*table_byte_offset as u16 & 0x0FFF)).to_le_bytes(),
4005 );
4006 }
4007
4008 // #664: null-slot trap — ONLY when the table image carries
4009 // null (uninitialized) slots, which the layout contract
4010 // requires to be linked as ZERO words. A fully-initialized
4011 // table skips this branch entirely, keeping the pre-#664
4012 // expansion byte-identical BY CONSTRUCTION (the #650
4013 // offset-0 trick).
4014 if *null_check {
4015 // CMP.W R12, #0 — T2 CMP (immediate): 11110 i 0 1101 1
4016 // Rn(4) | 0 imm3 1111 imm8, Rn=R12, imm=0.
4017 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
4018 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
4019 // BNE +1 insn (skip the UDF when the pointer is non-null)
4020 // — B<cond>.N imm8=0: target = branch + 4. NE.
4021 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
4022 // UDF #0 — call_indirect null-funcref trap (WASM Core
4023 // §4.4.8: calling an uninitialized element traps; same
4024 // trap idiom as the bounds guard above).
4025 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
4026 }
4027
4028 // BLX R12 (call function indirectly)
4029 // BLX Rm (16-bit): 0100 0111 1 Rm 000
4030 let blx: u16 = 0x47E0; // BLX R12
4031 bytes.extend_from_slice(&blx.to_le_bytes());
4032
4033 Ok(bytes)
4034 }
4035
4036 // Label pseudo-instruction: emits no machine code
4037 ArmOp::Label { .. } => Ok(Vec::new()),
4038
4039 // Conditional branch to label (generic) - offset 0, will be patched
4040 ArmOp::Bcc { cond, label: _ } => {
4041 use synth_synthesis::Condition;
4042 let cond_bits: u16 = match cond {
4043 Condition::EQ => 0x0,
4044 Condition::NE => 0x1,
4045 Condition::HS => 0x2,
4046 Condition::LO => 0x3,
4047 Condition::HI => 0x8,
4048 Condition::LS => 0x9,
4049 Condition::GE => 0xA,
4050 Condition::LT => 0xB,
4051 Condition::GT => 0xC,
4052 Condition::LE => 0xD,
4053 };
4054 // 16-bit B<cond> with offset 0: 1101 cond imm8
4055 let instr: u16 = 0xD000 | (cond_bits << 8);
4056 Ok(instr.to_le_bytes().to_vec())
4057 }
4058
4059 // Branch instructions
4060 ArmOp::B { label: _ } => {
4061 // Simplified: B.N with offset 0
4062 // For real usage, would need label resolution
4063 let instr: u16 = 0xE000; // B.N #0
4064 Ok(instr.to_le_bytes().to_vec())
4065 }
4066
4067 // BHS (Branch if Higher or Same) - used for bounds checking
4068 // Condition code: 0x2 (C set)
4069 ArmOp::Bhs { label: _ } => {
4070 // 16-bit B<cond> with offset 0: 1101 cond imm8
4071 // cond = 0x2 (HS)
4072 let instr: u16 = 0xD200; // BHS.N #0
4073 Ok(instr.to_le_bytes().to_vec())
4074 }
4075
4076 // BLO (Branch if Lower) - complementary to BHS
4077 // Condition code: 0x3 (C clear)
4078 ArmOp::Blo { label: _ } => {
4079 // 16-bit B<cond> with offset 0: 1101 cond imm8
4080 // cond = 0x3 (LO)
4081 let instr: u16 = 0xD300; // BLO.N #0
4082 Ok(instr.to_le_bytes().to_vec())
4083 }
4084
4085 // Branch with numeric offset (Thumb-2)
4086 // Thumb-2 B.W instruction: 32-bit with +-16MB range
4087 ArmOp::BOffset { offset } => {
4088 // offset is already the halfword displacement: (target - branch - 4) / 2
4089 // This is the raw encoded value, accounting for variable-length instructions
4090 let halfword_offset = *offset;
4091
4092 // 16-bit B.N encoding: 1110 0 imm11 (11-bit signed halfword offset)
4093 // Range: -1024 to +1022 halfwords
4094 if (-1024..=1022).contains(&halfword_offset) {
4095 // 16-bit B.N encoding: 1110 0 imm11
4096 let imm11 = (halfword_offset as u16) & 0x7FF;
4097 let instr: u16 = 0xE000 | imm11;
4098 Ok(instr.to_le_bytes().to_vec())
4099 } else {
4100 // 32-bit B.W encoding for larger offsets
4101 // First halfword: 1111 0 S imm10
4102 // Second halfword: 10 J1 0 J2 imm11
4103 // Total offset = SignExtend(S:I1:I2:imm10:imm11:0)
4104 // where I1 = NOT(J1 XOR S), I2 = NOT(J2 XOR S)
4105
4106 // The B.W (T4) encoding packs the signed offset as:
4107 // S:I1:I2:imm10:imm11:0 (25-bit signed, halfword-aligned)
4108 // where J1 = NOT(I1 XOR S), J2 = NOT(I2 XOR S)
4109 // Input halfword_offset already equals (target - PC - 4) / 2,
4110 // so the full byte offset = halfword_offset << 1.
4111 // The encoding fields split that 25-bit signed value (including the
4112 // implicit trailing zero) as: S | imm10 | imm11
4113 // with I1 = bit 23 and I2 = bit 22 of the signed offset.
4114 let signed_offset = halfword_offset << 1; // byte offset
4115 let s = if signed_offset < 0 { 1u32 } else { 0u32 };
4116 let uoffset = signed_offset as u32;
4117 let imm10 = (uoffset >> 12) & 0x3FF; // bits [21:12]
4118 let imm11 = (uoffset >> 1) & 0x7FF; // bits [11:1]
4119 let i1 = (uoffset >> 23) & 1; // bit 23
4120 let i2 = (uoffset >> 22) & 1; // bit 22
4121 let j1 = (!(i1 ^ s)) & 1; // J1 = NOT(I1 XOR S)
4122 let j2 = (!(i2 ^ s)) & 1; // J2 = NOT(I2 XOR S)
4123
4124 let hw1: u16 = (0xF000 | (s << 10) | imm10) as u16;
4125 let hw2: u16 = (0x9000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4126
4127 let mut bytes = hw1.to_le_bytes().to_vec();
4128 bytes.extend_from_slice(&hw2.to_le_bytes());
4129 Ok(bytes)
4130 }
4131 }
4132
4133 // Conditional branch with numeric offset (Thumb-2)
4134 ArmOp::BCondOffset { cond, offset } => {
4135 use synth_synthesis::Condition;
4136 let cond_bits: u16 = match cond {
4137 Condition::EQ => 0x0,
4138 Condition::NE => 0x1,
4139 Condition::HS => 0x2,
4140 Condition::LO => 0x3,
4141 Condition::HI => 0x8,
4142 Condition::LS => 0x9,
4143 Condition::GE => 0xA,
4144 Condition::LT => 0xB,
4145 Condition::GT => 0xC,
4146 Condition::LE => 0xD,
4147 };
4148
4149 // offset is already the halfword displacement: (target - branch - 4) / 2
4150 // This is the raw imm8 value for 16-bit B<cond> encoding
4151 let halfword_offset = *offset;
4152
4153 // 16-bit B<cond> encoding: 1101 cond imm8
4154 // Range: -256 to +254 halfwords (imm8 is sign-extended and shifted left 1)
4155 if (-128..=127).contains(&halfword_offset) {
4156 let imm8 = (halfword_offset as u16) & 0xFF;
4157 let instr: u16 = 0xD000 | (cond_bits << 8) | imm8;
4158 Ok(instr.to_le_bytes().to_vec())
4159 } else {
4160 // 32-bit B<cond>.W (encoding T3) for larger offsets
4161 // First halfword: 1111 0 S cond(4) imm6
4162 // Second halfword: 10 J1 0 J2 imm11
4163 //
4164 // Per ARMv7-M, the branch BYTE offset is
4165 // SignExtend(S:J2:J1:imm6:imm11:'0'), i.e. the field value
4166 // S:J2:J1:imm6:imm11 IS the signed 20-bit HALFWORD offset —
4167 // imm11/imm6/J1/J2/S take `halfword_offset` bits [10:0],
4168 // [16:11], 17, 18 and 19 directly (mirroring the T4
4169 // unconditional arm above).
4170 //
4171 // #740: this arm previously packed `halfword_offset >> 1`
4172 // into imm6:imm11 — HALVING the displacement — so every
4173 // wide conditional branch (span > 254 bytes) landed at half
4174 // its intended offset: gust_poll's loop-head `br_if` to an
4175 // outer block end jumped mid-shape. Narrow (16-bit) B<cond>
4176 // encodings were unaffected, which is why short-range CF
4177 // fixtures never caught it.
4178 if !(-(1 << 19)..(1 << 19)).contains(&halfword_offset) {
4179 return Err(synth_core::Error::synthesis(format!(
4180 "B<cond>.W (T3) halfword offset {halfword_offset} exceeds \
4181 the signed 20-bit encoding range (±1 MB) — refusing to \
4182 emit a truncated branch"
4183 )));
4184 }
4185 let u = halfword_offset as u32;
4186 let imm11 = u & 0x7FF; // halfword offset bits [10:0]
4187 let imm6 = (u >> 11) & 0x3F; // bits [16:11]
4188 let j1 = (u >> 17) & 1; // bit 17
4189 let j2 = (u >> 18) & 1; // bit 18
4190 let s = (u >> 19) & 1; // sign (range-checked above)
4191
4192 let hw1: u16 = (0xF000 | (s << 10) | ((cond_bits as u32) << 6) | imm6) as u16;
4193 let hw2: u16 = (0x8000 | (j1 << 13) | (j2 << 11) | imm11) as u16;
4194
4195 let mut bytes = hw1.to_le_bytes().to_vec();
4196 bytes.extend_from_slice(&hw2.to_le_bytes());
4197 Ok(bytes)
4198 }
4199 }
4200
4201 ArmOp::Bl { label: _ } => {
4202 // BL is always 32-bit in Thumb-2, encoded here as a relocatable
4203 // placeholder; an R_ARM_THM_CALL relocation patches the target
4204 // (see arm_backend.rs). The placeholder must carry an embedded
4205 // addend of -4 so the relocation nets to exactly the symbol S.
4206 //
4207 // Thumb BL computes `target = (P + 4) + signed_offset`. Under
4208 // R_ARM_THM_CALL the linker resolves using the in-place addend;
4209 // a 0xF800 placeholder (addend 0) lands at S+4 — every call one
4210 // instruction past the callee entry (#174). The correct
4211 // placeholder is what `gas` emits for `bl <extern>`:
4212 // f7ff fffe -> `bl <self>` (S=1, J1=J2=1, imm = -4 addend),
4213 // i.e. hw1=0xF7FF, hw2=0xFFFE. This nets to S, not S+4.
4214 // (The earlier 0xD000 was worse still — a ~+0x600000 addend,
4215 // the garbage `bl c0000c` and "truncated to fit" of #167.)
4216 let hw1: u16 = 0xF7FF;
4217 let hw2: u16 = 0xFFFE;
4218 let mut bytes = hw1.to_le_bytes().to_vec();
4219 bytes.extend_from_slice(&hw2.to_le_bytes());
4220 Ok(bytes)
4221 }
4222
4223 // MVN
4224 ArmOp::Mvn { rd, op2 } => {
4225 if let Operand2::Reg(rm) = op2 {
4226 let rd_bits = reg_to_bits(rd) as u16;
4227 let rm_bits = reg_to_bits(rm) as u16;
4228
4229 if rd_bits < 8 && rm_bits < 8 {
4230 // MVNS Rd, Rm (16-bit): 0100 0011 11 Rm Rd
4231 let instr: u16 = 0x43C0 | (rm_bits << 3) | rd_bits;
4232 Ok(instr.to_le_bytes().to_vec())
4233 } else {
4234 // 32-bit MVN
4235 let hw1: u16 = 0xEA6F_u16;
4236 let hw2: u16 = ((reg_to_bits(rd) << 8) | reg_to_bits(rm)) as u16;
4237 let mut bytes = hw1.to_le_bytes().to_vec();
4238 bytes.extend_from_slice(&hw2.to_le_bytes());
4239 Ok(bytes)
4240 }
4241 } else {
4242 let instr: u16 = 0xBF00;
4243 Ok(instr.to_le_bytes().to_vec())
4244 }
4245 }
4246
4247 // MOVW - Move Wide (Thumb-2 32-bit)
4248 ArmOp::Movw { rd, imm16 } => {
4249 self.encode_thumb32_movw_raw(reg_to_bits(rd), *imm16 as u32)
4250 }
4251
4252 // MOVT - Move Top (Thumb-2 32-bit)
4253 ArmOp::Movt { rd, imm16 } => {
4254 self.encode_thumb32_movt_raw(reg_to_bits(rd), *imm16 as u32)
4255 }
4256
4257 // #237: symbol-relative MOVW/MOVT. Encode the addend's low/high 16
4258 // bits in place; the backend records an R_ARM_MOVW_ABS_NC /
4259 // R_ARM_MOVT_ABS relocation against `symbol`, so the linker adds the
4260 // symbol's final address to the in-place addend (REL semantics).
4261 ArmOp::MovwSym { rd, addend, .. } => {
4262 self.encode_thumb32_movw_raw(reg_to_bits(rd), (*addend as u32) & 0xffff)
4263 }
4264 ArmOp::MovtSym { rd, addend, .. } => {
4265 self.encode_thumb32_movt_raw(reg_to_bits(rd), ((*addend as u32) >> 16) & 0xffff)
4266 }
4267
4268 // #345: literal-pool address load — emit a PLACEHOLDER `LDR.W rd,
4269 // [pc, #0]` (U=1, imm12=0). The backend (arm_backend.rs) places the
4270 // 4-byte pool word at the end of the function, records the R_ARM_ABS32
4271 // relocation against `symbol+addend`, and patches the imm12 with the
4272 // real PC-relative distance once the pool offset is known.
4273 // Encoding T2: 1111 1000 1101 1111 | Rt(4) imm12(12), with the literal
4274 // base = Align(PC,4) and PC = address of this instruction + 4.
4275 ArmOp::LdrSym { rd, .. } => {
4276 let rt = reg_to_bits(rd) as u16;
4277 let hw1: u16 = 0xF8DF; // LDR.W (literal), U=1
4278 let hw2: u16 = rt << 12; // imm12 = 0 placeholder
4279 let mut bytes = Vec::with_capacity(4);
4280 bytes.extend_from_slice(&hw1.to_le_bytes());
4281 bytes.extend_from_slice(&hw2.to_le_bytes());
4282 Ok(bytes)
4283 }
4284
4285 // SetCond: Materialize condition flag into register (0 or 1)
4286 // Strategy: ITE <cond>; MOV Rd, #1; MOV Rd, #0
4287 // IMPORTANT: Must use ITE (If-Then-Else) because 16-bit Thumb MOV
4288 // always sets flags (MOVS). We need to evaluate the condition BEFORE
4289 // any MOV instruction clobbers the flags from CMP.
4290 ArmOp::SetCond { rd, cond } => {
4291 let rd_bits = reg_to_bits(rd) as u16;
4292
4293 // Condition code encoding for IT block
4294 use synth_synthesis::Condition;
4295 let cond_bits: u16 = match cond {
4296 Condition::EQ => 0x0,
4297 Condition::NE => 0x1,
4298 Condition::LT => 0xB,
4299 Condition::LE => 0xD,
4300 Condition::GT => 0xC,
4301 Condition::GE => 0xA,
4302 Condition::LO => 0x3, // CC/LO (unsigned <)
4303 Condition::LS => 0x9, // LS (unsigned <=)
4304 Condition::HI => 0x8, // HI (unsigned >)
4305 Condition::HS => 0x2, // CS/HS (unsigned >=)
4306 };
4307
4308 // ITE <cond>: encodes If-Then-Else block
4309 // The mask field depends on firstcond[0]:
4310 // - If firstcond[0] = 0: mask = 0xC for TE pattern (ITE EQ = BF0C)
4311 // - If firstcond[0] = 1: mask = 0x4 for TE pattern (ITE NE = BF14)
4312 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4313 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4314
4315 // Materialize 0/1 into Rd. The 16-bit MOVS (T1) encodes Rd in a
4316 // 3-bit field (bits[10:8]) — only R0–R7. For a high register
4317 // (R8–R12) `rd_bits << 8` overflows into bit 11 and silently
4318 // turns MOVS into CMP (00100 → 00101), corrupting the result
4319 // (this mis-materialized gale's `has_waiter`, so its `local.set`
4320 // stored a stale register → the binary-sem WAKE dispatch read
4321 // garbage). Use the 32-bit MOV.W (T2) for high registers, which
4322 // has a 4-bit Rd field. MOV.W with S=0 doesn't set flags, which
4323 // is fine inside the ITE (the materialized value is the result;
4324 // the flags are not consumed afterwards).
4325 let mut bytes = ite_instr.to_le_bytes().to_vec();
4326 let push_mov = |bytes: &mut Vec<u8>, imm: u16| {
4327 if rd_bits <= 7 {
4328 let m: u16 = 0x2000 | (rd_bits << 8) | imm; // 16-bit MOVS Rd,#imm
4329 bytes.extend_from_slice(&m.to_le_bytes());
4330 } else {
4331 // 32-bit MOV.W Rd, #imm (T2): F04F | (Rd<<8) | imm8
4332 let hw1: u16 = 0xF04F;
4333 let hw2: u16 = (rd_bits << 8) | imm;
4334 bytes.extend_from_slice(&hw1.to_le_bytes());
4335 bytes.extend_from_slice(&hw2.to_le_bytes());
4336 }
4337 };
4338 push_mov(&mut bytes, 1); // Then branch (condition true) → 1
4339 push_mov(&mut bytes, 0); // Else branch (condition false) → 0
4340 Ok(bytes)
4341 }
4342
4343 // I64SetCond: Compare two i64 register pairs, result 0/1 in rd
4344 // EQ/NE: CMP lo,lo; IT EQ; CMPEQ hi,hi; ITE <cond>; MOV 1; MOV 0
4345 // LT: CMP lo,lo; SBCS rd,hi,hi; ITE LT; MOV 1; MOV 0
4346 // GT: CMP lo,lo (swapped); SBCS rd,hi,hi (swapped); ITE LT; MOV 1; MOV 0
4347 ArmOp::I64SetCond {
4348 rd,
4349 rn_lo,
4350 rn_hi,
4351 rm_lo,
4352 rm_hi,
4353 cond,
4354 } => {
4355 use synth_synthesis::Condition;
4356 let rd_bits = reg_to_bits(rd) as u16;
4357 let mut bytes = Vec::new();
4358
4359 // Helper: encode CMP Rn, Rm (16-bit)
4360 let encode_cmp_reg = |rn: &synth_synthesis::Reg,
4361 rm: &synth_synthesis::Reg|
4362 -> Vec<u8> {
4363 let rn_bits = reg_to_bits(rn) as u16;
4364 let rm_bits = reg_to_bits(rm) as u16;
4365 if rn_bits < 8 && rm_bits < 8 {
4366 let instr: u16 = 0x4280 | (rm_bits << 3) | rn_bits;
4367 instr.to_le_bytes().to_vec()
4368 } else {
4369 let n_bit = (rn_bits >> 3) & 1;
4370 let instr: u16 = 0x4500 | (n_bit << 7) | (rm_bits << 3) | (rn_bits & 0x7);
4371 instr.to_le_bytes().to_vec()
4372 }
4373 };
4374
4375 // Helper: encode ITE <cond> (2 bytes)
4376 let encode_ite = |cond_bits: u16| -> Vec<u8> {
4377 let mask = if (cond_bits & 1) == 0 { 0xC } else { 0x4 };
4378 let ite_instr: u16 = 0xBF00 | (cond_bits << 4) | mask;
4379 ite_instr.to_le_bytes().to_vec()
4380 };
4381
4382 // Helper: encode SetCond (ITE + MOV #1 + MOV #0) for given condition
4383 let encode_setcond = |cond_bits: u16, rd_bits: u16| -> Vec<u8> {
4384 let mut b = encode_ite(cond_bits);
4385 if rd_bits < 8 {
4386 let mov_one: u16 = 0x2001 | (rd_bits << 8);
4387 let mov_zero: u16 = 0x2000 | (rd_bits << 8);
4388 b.extend_from_slice(&mov_one.to_le_bytes());
4389 b.extend_from_slice(&mov_zero.to_le_bytes());
4390 } else {
4391 // #311: rd >= R8 — the 16-bit MOV imm8 form has a 3-bit
4392 // rd field; rd_bits<<8 overflows into bit 11 and
4393 // TRANSMUTES the MOV into CMP (0x2001|0x0800 = 0x2801 =
4394 // CMP r0,#1): the boolean dies in the flags and the
4395 // consumer reads a stale register. Use the 32-bit
4396 // MOV.W (T2: F04F 0000|rd<<8|imm8) — IT-legal,
4397 // flag-preserving. Same class as H-CODE-9 / #180.
4398 for imm in [1u16, 0u16] {
4399 let hw1: u16 = 0xF04F;
4400 let hw2: u16 = (rd_bits << 8) | imm;
4401 b.extend_from_slice(&hw1.to_le_bytes());
4402 b.extend_from_slice(&hw2.to_le_bytes());
4403 }
4404 }
4405 b
4406 };
4407
4408 match cond {
4409 Condition::EQ | Condition::NE => {
4410 // CMP rn_lo, rm_lo (compare low words)
4411 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4412
4413 // IT EQ (execute next instruction only if Z=1)
4414 let it_eq: u16 = 0xBF08; // IT EQ: cond=0000, mask=1000
4415 bytes.extend_from_slice(&it_eq.to_le_bytes());
4416
4417 // CMPEQ rn_hi, rm_hi (compare high words, only if low equal)
4418 bytes.extend_from_slice(&encode_cmp_reg(rn_hi, rm_hi));
4419
4420 // ITE <cond>; MOV rd, #1; MOV rd, #0
4421 let cond_bits: u16 = match cond {
4422 Condition::EQ => 0x0,
4423 Condition::NE => 0x1,
4424 _ => unreachable!(),
4425 };
4426 bytes.extend_from_slice(&encode_setcond(cond_bits, rd_bits));
4427 }
4428
4429 Condition::LT => {
4430 // CMP rn_lo, rm_lo (sets C flag for borrow)
4431 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4432
4433 // SBCS rd, rn_hi, rm_hi (subtract with carry, sets N,V flags)
4434 // SBCS.W Rd, Rn, Rm: EB70 Rn | 0000 Rd 0000 Rm
4435 let rn_hi_bits = reg_to_bits(rn_hi);
4436 let rm_hi_bits = reg_to_bits(rm_hi);
4437 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4438 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4439 bytes.extend_from_slice(&hw1.to_le_bytes());
4440 bytes.extend_from_slice(&hw2.to_le_bytes());
4441
4442 // ITE LT; MOV rd, #1; MOV rd, #0
4443 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4444 }
4445
4446 Condition::GT => {
4447 // GT(a,b) = LT(b,a): swap operands
4448 // CMP rm_lo, rn_lo (swapped)
4449 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4450
4451 // SBCS rd, rm_hi, rn_hi (swapped)
4452 let rm_hi_bits = reg_to_bits(rm_hi);
4453 let rn_hi_bits = reg_to_bits(rn_hi);
4454 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4455 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4456 bytes.extend_from_slice(&hw1.to_le_bytes());
4457 bytes.extend_from_slice(&hw2.to_le_bytes());
4458
4459 // ITE LT; MOV rd, #1; MOV rd, #0
4460 bytes.extend_from_slice(&encode_setcond(0xB, rd_bits)); // LT = 0xB
4461 }
4462
4463 Condition::LE => {
4464 // LE(a,b) = !GT(a,b): use GT logic but invert result
4465 // GT(a,b) = LT(b,a): so we do CMP(b,a) and check LT, then invert
4466 // CMP rm_lo, rn_lo (swapped, same as GT)
4467 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4468
4469 // SBCS rd, rm_hi, rn_hi (swapped)
4470 let rm_hi_bits = reg_to_bits(rm_hi);
4471 let rn_hi_bits = reg_to_bits(rn_hi);
4472 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4473 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4474 bytes.extend_from_slice(&hw1.to_le_bytes());
4475 bytes.extend_from_slice(&hw2.to_le_bytes());
4476
4477 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT, so inverting GT result)
4478 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4479 }
4480
4481 Condition::GE => {
4482 // GE(a,b) = !LT(a,b): use LT logic but invert result
4483 // CMP rn_lo, rm_lo (same as LT)
4484 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4485
4486 // SBCS rd, rn_hi, rm_hi (same as LT)
4487 let rn_hi_bits = reg_to_bits(rn_hi);
4488 let rm_hi_bits = reg_to_bits(rm_hi);
4489 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4490 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4491 bytes.extend_from_slice(&hw1.to_le_bytes());
4492 bytes.extend_from_slice(&hw2.to_le_bytes());
4493
4494 // ITE GE; MOV rd, #1; MOV rd, #0 (GE is !LT)
4495 bytes.extend_from_slice(&encode_setcond(0xA, rd_bits)); // GE = 0xA
4496 }
4497
4498 // Unsigned comparisons - same instruction sequence, different conditions
4499 Condition::LO => {
4500 // LO (unsigned LT): CMP lo, SBCS hi, check C=0
4501 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4502 let rn_hi_bits = reg_to_bits(rn_hi);
4503 let rm_hi_bits = reg_to_bits(rm_hi);
4504 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4505 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4506 bytes.extend_from_slice(&hw1.to_le_bytes());
4507 bytes.extend_from_slice(&hw2.to_le_bytes());
4508 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4509 }
4510
4511 Condition::HI => {
4512 // HI (unsigned GT): swap operands and check LO
4513 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4514 let rm_hi_bits = reg_to_bits(rm_hi);
4515 let rn_hi_bits = reg_to_bits(rn_hi);
4516 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4517 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4518 bytes.extend_from_slice(&hw1.to_le_bytes());
4519 bytes.extend_from_slice(&hw2.to_le_bytes());
4520 bytes.extend_from_slice(&encode_setcond(0x3, rd_bits)); // LO = 0x3 (CC)
4521 }
4522
4523 Condition::LS => {
4524 // LS (unsigned LE): !(a > b) = !(HI), so do HI and invert
4525 bytes.extend_from_slice(&encode_cmp_reg(rm_lo, rn_lo));
4526 let rm_hi_bits = reg_to_bits(rm_hi);
4527 let rn_hi_bits = reg_to_bits(rn_hi);
4528 let hw1: u16 = (0xEB70 | rm_hi_bits) as u16;
4529 let hw2: u16 = ((rd_bits as u32) << 8 | rn_hi_bits) as u16;
4530 bytes.extend_from_slice(&hw1.to_le_bytes());
4531 bytes.extend_from_slice(&hw2.to_le_bytes());
4532 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4533 }
4534
4535 Condition::HS => {
4536 // HS (unsigned GE): !(a < b) = !(LO)
4537 bytes.extend_from_slice(&encode_cmp_reg(rn_lo, rm_lo));
4538 let rn_hi_bits = reg_to_bits(rn_hi);
4539 let rm_hi_bits = reg_to_bits(rm_hi);
4540 let hw1: u16 = (0xEB70 | rn_hi_bits) as u16;
4541 let hw2: u16 = ((rd_bits as u32) << 8 | rm_hi_bits) as u16;
4542 bytes.extend_from_slice(&hw1.to_le_bytes());
4543 bytes.extend_from_slice(&hw2.to_le_bytes());
4544 bytes.extend_from_slice(&encode_setcond(0x2, rd_bits)); // HS = 0x2 (CS) = !LO
4545 }
4546 }
4547
4548 Ok(bytes)
4549 }
4550
4551 // I64SetCondZ: Test if i64 register pair is zero, result 0/1 in rd
4552 // ORR.W rd, rn_lo, rn_hi; CMP rd, #0; ITE EQ; MOV 1; MOV 0
4553 ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => {
4554 let rd_bits = reg_to_bits(rd);
4555 let rn_lo_bits = reg_to_bits(rn_lo);
4556 let rn_hi_bits = reg_to_bits(rn_hi);
4557 let mut bytes = Vec::new();
4558
4559 // ORR.W rd, rn_lo, rn_hi: EA40 rn_lo | 0000 rd 0000 rn_hi
4560 let hw1: u16 = (0xEA40 | rn_lo_bits) as u16;
4561 let hw2: u16 = ((rd_bits << 8) | rn_hi_bits) as u16;
4562 bytes.extend_from_slice(&hw1.to_le_bytes());
4563 bytes.extend_from_slice(&hw2.to_le_bytes());
4564
4565 // CMP rd, #0 — 16-bit form only for r0-r7 (3-bit rd field);
4566 // high registers take CMP.W (T2: F1B0|rn 0F00|imm8). This was
4567 // H-CODE-9: rd_bits<<8 overflowing the field compared the
4568 // WRONG register. Same hardening as the #311 SetCond fix.
4569 if rd_bits < 8 {
4570 let cmp_instr: u16 = 0x2800 | ((rd_bits as u16) << 8);
4571 bytes.extend_from_slice(&cmp_instr.to_le_bytes());
4572 } else {
4573 let hw1: u16 = 0xF1B0 | (rd_bits as u16);
4574 let hw2: u16 = 0x0F00;
4575 bytes.extend_from_slice(&hw1.to_le_bytes());
4576 bytes.extend_from_slice(&hw2.to_le_bytes());
4577 }
4578
4579 // ITE EQ; MOV rd, #1; MOV rd, #0 (32-bit MOV.W for rd >= R8,
4580 // #311 — see I64SetCond)
4581 let mask = 0xC_u16; // ITE EQ mask: firstcond[0]=0, mask=0xC
4582 let ite_instr: u16 = 0xBF00 | mask;
4583 bytes.extend_from_slice(&ite_instr.to_le_bytes());
4584 if rd_bits < 8 {
4585 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
4586 let mov_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
4587 bytes.extend_from_slice(&mov_one.to_le_bytes());
4588 bytes.extend_from_slice(&mov_zero.to_le_bytes());
4589 } else {
4590 for imm in [1u16, 0u16] {
4591 let hw1: u16 = 0xF04F;
4592 let hw2: u16 = ((rd_bits as u16) << 8) | imm;
4593 bytes.extend_from_slice(&hw1.to_le_bytes());
4594 bytes.extend_from_slice(&hw2.to_le_bytes());
4595 }
4596 }
4597
4598 Ok(bytes)
4599 }
4600
4601 // I64Mul: 64-bit multiply using UMULL + MLA cross products
4602 // Formula: result = (a_lo * b_lo) + ((a_lo * b_hi + a_hi * b_lo) << 32)
4603 // Uses R12 as scratch register
4604 ArmOp::I64Mul {
4605 rd_lo,
4606 rd_hi,
4607 rn_lo,
4608 rn_hi,
4609 rm_lo,
4610 rm_hi,
4611 } => {
4612 let rd_lo_bits = reg_to_bits(rd_lo);
4613 let rd_hi_bits = reg_to_bits(rd_hi);
4614 let rn_lo_bits = reg_to_bits(rn_lo);
4615 let rn_hi_bits = reg_to_bits(rn_hi);
4616 let rm_lo_bits = reg_to_bits(rm_lo);
4617 let rm_hi_bits = reg_to_bits(rm_hi);
4618 let r12: u32 = 12; // IP scratch register
4619 let mut bytes = Vec::new();
4620
4621 // 1. MUL R12, rn_lo, rm_hi (R12 = a_lo * b_hi)
4622 // Thumb-2 MUL: hw1=0xFB00|Rn, hw2=0xF000|(Rd<<8)|Rm
4623 let hw1: u16 = (0xFB00 | rn_lo_bits) as u16;
4624 let hw2: u16 = (0xF000 | (r12 << 8) | rm_hi_bits) as u16;
4625 bytes.extend_from_slice(&hw1.to_le_bytes());
4626 bytes.extend_from_slice(&hw2.to_le_bytes());
4627
4628 // 2. MLA R12, rn_hi, rm_lo, R12 (R12 += a_hi * b_lo)
4629 // Thumb-2 MLA: hw1=0xFB00|Rn, hw2=(Ra<<12)|(Rd<<8)|Rm
4630 let hw1: u16 = (0xFB00 | rn_hi_bits) as u16;
4631 let hw2: u16 = ((r12 << 12) | (r12 << 8) | rm_lo_bits) as u16;
4632 bytes.extend_from_slice(&hw1.to_le_bytes());
4633 bytes.extend_from_slice(&hw2.to_le_bytes());
4634
4635 // 3. UMULL rd_lo, rd_hi, rn_lo, rm_lo (rd_lo:rd_hi = a_lo * b_lo)
4636 // Thumb-2 UMULL: hw1=0xFBA0|Rn, hw2=(RdLo<<12)|(RdHi<<8)|Rm
4637 let hw1: u16 = (0xFBA0 | rn_lo_bits) as u16;
4638 let hw2: u16 = ((rd_lo_bits << 12) | (rd_hi_bits << 8) | rm_lo_bits) as u16;
4639 bytes.extend_from_slice(&hw1.to_le_bytes());
4640 bytes.extend_from_slice(&hw2.to_le_bytes());
4641
4642 // 4. ADD rd_hi, R12 (rd_hi += cross products)
4643 // 16-bit high reg ADD: 01000100 D Rm Rdn[2:0]
4644 let d_bit = (rd_hi_bits >> 3) & 1;
4645 let add_instr: u16 =
4646 (0x4400 | (d_bit << 7) | (r12 << 3) | (rd_hi_bits & 0x7)) as u16;
4647 bytes.extend_from_slice(&add_instr.to_le_bytes());
4648
4649 Ok(bytes)
4650 }
4651
4652 // I64Shl: 64-bit shift left with branch for n<32 vs n>=32.
4653 //
4654 // #1048: the expansion must NEVER write its own input operands.
4655 // The pre-#1048 sequence masked the amount IN PLACE
4656 // (`AND.W rm_lo, rm_lo, #63`) and used the amount's home high
4657 // register `rm_hi` as scratch (`SUBS.W rm_hi, rm_lo, #32`, RSB,
4658 // LSR) — so re-reading the amount after the shift returned a
4659 // mangled value (amt=64 read back 0, amt=67 read back 3). The
4660 // rewrite uses R12 — encoder scratch, never allocatable (#212) —
4661 // as the ONLY temporary, re-deriving the masked amount from the
4662 // untouched rm_lo whenever a second live temp would otherwise be
4663 // needed. This matches the Rocq/SMT pseudo-op models
4664 // (I64ShlPseudo writes rd_lo/rd_hi ONLY), which were proven over
4665 // exactly this non-clobbering contract all along.
4666 ArmOp::I64Shl {
4667 rd_lo,
4668 rd_hi,
4669 rn_lo,
4670 rn_hi,
4671 rm_lo,
4672 rm_hi: _,
4673 } => {
4674 let rd_lo_bits = reg_to_bits(rd_lo);
4675 let rd_hi_bits = reg_to_bits(rd_hi);
4676 let rn_lo_bits = reg_to_bits(rn_lo);
4677 let rn_hi_bits = reg_to_bits(rn_hi);
4678 let rm_lo_bits = reg_to_bits(rm_lo);
4679 let r12: u32 = 12; // the only scratch — never allocatable
4680 let mut bytes = Vec::new();
4681
4682 // #1039 house style: refuse a destination that would collide
4683 // with an input still needed after the destination is first
4684 // written, loudly — never misassemble. rd_hi is written before
4685 // rn_lo and rm_lo are last read; the in-place form
4686 // rd == rn (select_default) has rd_hi == rn_hi and stays legal.
4687 if rd_hi_bits == rn_lo_bits || rd_hi_bits == rm_lo_bits {
4688 return Err(synth_core::Error::synthesis(format!(
4689 "I64Shl: rd_hi {rd_hi:?} aliases an input ({rn_lo:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4690 )));
4691 }
4692
4693 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4694 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4695 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4696 bytes.extend_from_slice(&hw1.to_le_bytes());
4697 bytes.extend_from_slice(&hw2.to_le_bytes());
4698
4699 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4700 let hw1: u16 = (0xF1B0 | r12) as u16;
4701 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4702 bytes.extend_from_slice(&hw1.to_le_bytes());
4703 bytes.extend_from_slice(&hw2.to_le_bytes());
4704
4705 // BPL .large (branch if n >= 32, offset = +14 halfwords)
4706 let bpl: u16 = 0xD50E;
4707 bytes.extend_from_slice(&bpl.to_le_bytes());
4708
4709 // --- Small shift (n < 32) ---
4710 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4711 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4712 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4713 bytes.extend_from_slice(&hw1.to_le_bytes());
4714 bytes.extend_from_slice(&hw2.to_le_bytes());
4715
4716 // LSL.W rd_hi, rn_hi, R12 (hi << n; rn_hi's last read)
4717 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4718 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4719 bytes.extend_from_slice(&hw1.to_le_bytes());
4720 bytes.extend_from_slice(&hw2.to_le_bytes());
4721
4722 // RSB.W R12, R12, #32 (R12 = 32-n)
4723 let hw1: u16 = (0xF1C0 | r12) as u16;
4724 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4725 bytes.extend_from_slice(&hw1.to_le_bytes());
4726 bytes.extend_from_slice(&hw2.to_le_bytes());
4727
4728 // LSR.W R12, rn_lo, R12 (overflow = lo >> (32-n); n=0 gives
4729 // a register shift by 32 which yields 0 — exact)
4730 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4731 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4732 bytes.extend_from_slice(&hw1.to_le_bytes());
4733 bytes.extend_from_slice(&hw2.to_le_bytes());
4734
4735 // ORR.W rd_hi, rd_hi, R12 (hi |= overflow bits from lo)
4736 let hw1: u16 = (0xEA40 | rd_hi_bits) as u16;
4737 let hw2: u16 = ((rd_hi_bits << 8) | r12) as u16;
4738 bytes.extend_from_slice(&hw1.to_le_bytes());
4739 bytes.extend_from_slice(&hw2.to_le_bytes());
4740
4741 // AND.W R12, rm_lo, #63 (n once more for the low half)
4742 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4743 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4744 bytes.extend_from_slice(&hw1.to_le_bytes());
4745 bytes.extend_from_slice(&hw2.to_le_bytes());
4746
4747 // LSL.W rd_lo, rn_lo, R12 (lo << n)
4748 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4749 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4750 bytes.extend_from_slice(&hw1.to_le_bytes());
4751 bytes.extend_from_slice(&hw2.to_le_bytes());
4752
4753 // B .done — `.done` is the END of the expansion, i.e. PAST the
4754 // large-shift arm's trailing zero-fill. #916: that zero-fill is
4755 // 1 halfword for a low rd_lo but 2 for R8-R12 (MOV.W), so the
4756 // displacement is DERIVED from its real width instead of the
4757 // hard-coded 0xE002 — widening the MOV without this would
4758 // overshoot `.done` and turn a data miscompile into a
4759 // control-flow one. Thumb `B` reads PC as its own address + 4
4760 // (= +2 halfwords), so imm11 = (large-arm halfwords) - 1.
4761 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_lo_bits);
4762 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4763 bytes.extend_from_slice(&b_done.to_le_bytes());
4764
4765 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4766 // LSL.W rd_hi, rn_lo, R12 (hi = lo << (n-32))
4767 let hw1: u16 = (0xFA00 | rn_lo_bits) as u16;
4768 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4769 bytes.extend_from_slice(&hw1.to_le_bytes());
4770 bytes.extend_from_slice(&hw2.to_le_bytes());
4771
4772 // MOV rd_lo, #0 (#916: MOV.W for rd_lo >= R8). NOTE the order
4773 // is load-bearing — zeroing rd_lo BEFORE the LSL.W would
4774 // destroy rn_lo in the in-place case rd_lo == rn_lo, so this
4775 // cannot be reordered to dodge the displacement change.
4776 emit_thumb_zero_fill(&mut bytes, rd_lo_bits);
4777
4778 Ok(bytes) // 46 bytes (48 when rd_lo >= R8 takes MOV.W)
4779 }
4780
4781 // I64ShrU: 64-bit logical shift right with branch for n<32 vs
4782 // n>=32. #1048: R12-only scratch, operands never written — see
4783 // the I64Shl comment for the full rationale.
4784 ArmOp::I64ShrU {
4785 rd_lo,
4786 rd_hi,
4787 rn_lo,
4788 rn_hi,
4789 rm_lo,
4790 rm_hi: _,
4791 } => {
4792 let rd_lo_bits = reg_to_bits(rd_lo);
4793 let rd_hi_bits = reg_to_bits(rd_hi);
4794 let rn_lo_bits = reg_to_bits(rn_lo);
4795 let rn_hi_bits = reg_to_bits(rn_hi);
4796 let rm_lo_bits = reg_to_bits(rm_lo);
4797 let r12: u32 = 12; // the only scratch — never allocatable
4798 let mut bytes = Vec::new();
4799
4800 // #1039 house style: rd_lo is written before rn_hi and rm_lo
4801 // are last read — refuse the collision loudly. The in-place
4802 // form rd == rn (select_default) has rd_lo == rn_lo and stays
4803 // legal.
4804 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4805 return Err(synth_core::Error::synthesis(format!(
4806 "I64ShrU: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4807 )));
4808 }
4809
4810 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4811 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4812 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4813 bytes.extend_from_slice(&hw1.to_le_bytes());
4814 bytes.extend_from_slice(&hw2.to_le_bytes());
4815
4816 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4817 let hw1: u16 = (0xF1B0 | r12) as u16;
4818 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4819 bytes.extend_from_slice(&hw1.to_le_bytes());
4820 bytes.extend_from_slice(&hw2.to_le_bytes());
4821
4822 // BPL .large (+14 halfwords)
4823 let bpl: u16 = 0xD50E;
4824 bytes.extend_from_slice(&bpl.to_le_bytes());
4825
4826 // --- Small shift (n < 32) ---
4827 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4828 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4829 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4830 bytes.extend_from_slice(&hw1.to_le_bytes());
4831 bytes.extend_from_slice(&hw2.to_le_bytes());
4832
4833 // LSR.W rd_lo, rn_lo, R12 (lo >> n; rn_lo's last read)
4834 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4835 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4836 bytes.extend_from_slice(&hw1.to_le_bytes());
4837 bytes.extend_from_slice(&hw2.to_le_bytes());
4838
4839 // RSB.W R12, R12, #32 (R12 = 32-n)
4840 let hw1: u16 = (0xF1C0 | r12) as u16;
4841 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4842 bytes.extend_from_slice(&hw1.to_le_bytes());
4843 bytes.extend_from_slice(&hw2.to_le_bytes());
4844
4845 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
4846 // a register shift by 32 which yields 0 — exact)
4847 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4848 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4849 bytes.extend_from_slice(&hw1.to_le_bytes());
4850 bytes.extend_from_slice(&hw2.to_le_bytes());
4851
4852 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
4853 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4854 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4855 bytes.extend_from_slice(&hw1.to_le_bytes());
4856 bytes.extend_from_slice(&hw2.to_le_bytes());
4857
4858 // AND.W R12, rm_lo, #63 (n once more for the high half)
4859 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4860 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4861 bytes.extend_from_slice(&hw1.to_le_bytes());
4862 bytes.extend_from_slice(&hw2.to_le_bytes());
4863
4864 // LSR.W rd_hi, rn_hi, R12 (hi >> n, logical)
4865 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4866 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4867 bytes.extend_from_slice(&hw1.to_le_bytes());
4868 bytes.extend_from_slice(&hw2.to_le_bytes());
4869
4870 // B .done — see I64Shl: `.done` is the END of the expansion,
4871 // past the trailing zero-fill, so the displacement is derived
4872 // from that zero-fill's real width (#916).
4873 let large_arm_hw = 2 + thumb_zero_fill_halfwords(rd_hi_bits);
4874 let b_done: u16 = 0xE000 | (large_arm_hw - 1);
4875 bytes.extend_from_slice(&b_done.to_le_bytes());
4876
4877 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4878 // LSR.W rd_lo, rn_hi, R12 (lo = hi >> (n-32))
4879 let hw1: u16 = (0xFA20 | rn_hi_bits) as u16;
4880 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4881 bytes.extend_from_slice(&hw1.to_le_bytes());
4882 bytes.extend_from_slice(&hw2.to_le_bytes());
4883
4884 // MOV rd_hi, #0 (#916: MOV.W for rd_hi >= R8). Order is
4885 // load-bearing: the LSR.W above reads rn_hi, which may BE
4886 // rd_hi in the in-place case.
4887 emit_thumb_zero_fill(&mut bytes, rd_hi_bits);
4888
4889 Ok(bytes) // 46 bytes (48 when rd_hi >= R8 takes MOV.W)
4890 }
4891
4892 // I64ShrS: 64-bit arithmetic shift right with branch for n<32 vs
4893 // n>=32. #1048: R12-only scratch, operands never written — see
4894 // the I64Shl comment for the full rationale.
4895 ArmOp::I64ShrS {
4896 rd_lo,
4897 rd_hi,
4898 rn_lo,
4899 rn_hi,
4900 rm_lo,
4901 rm_hi: _,
4902 } => {
4903 let rd_lo_bits = reg_to_bits(rd_lo);
4904 let rd_hi_bits = reg_to_bits(rd_hi);
4905 let rn_lo_bits = reg_to_bits(rn_lo);
4906 let rn_hi_bits = reg_to_bits(rn_hi);
4907 let rm_lo_bits = reg_to_bits(rm_lo);
4908 let r12: u32 = 12; // the only scratch — never allocatable
4909 let mut bytes = Vec::new();
4910
4911 // #1039 house style: rd_lo is written before rn_hi and rm_lo
4912 // are last read (on BOTH arms of the diamond — the large arm's
4913 // trailing `ASR rd_hi, rn_hi, #31` also reads rn_hi after
4914 // rd_lo is written). The in-place form rd == rn stays legal.
4915 if rd_lo_bits == rn_hi_bits || rd_lo_bits == rm_lo_bits {
4916 return Err(synth_core::Error::synthesis(format!(
4917 "I64ShrS: rd_lo {rd_lo:?} aliases an input ({rn_hi:?}/{rm_lo:?}) still live inside the expansion (#1048)"
4918 )));
4919 }
4920
4921 // AND.W R12, rm_lo, #63 (n — the amount register is only READ)
4922 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4923 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4924 bytes.extend_from_slice(&hw1.to_le_bytes());
4925 bytes.extend_from_slice(&hw2.to_le_bytes());
4926
4927 // SUBS.W R12, R12, #32 (R12 = n-32, sets flags)
4928 let hw1: u16 = (0xF1B0 | r12) as u16;
4929 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4930 bytes.extend_from_slice(&hw1.to_le_bytes());
4931 bytes.extend_from_slice(&hw2.to_le_bytes());
4932
4933 // BPL .large (+14 halfwords)
4934 let bpl: u16 = 0xD50E;
4935 bytes.extend_from_slice(&bpl.to_le_bytes());
4936
4937 // --- Small shift (n < 32) ---
4938 // AND.W R12, rm_lo, #63 (n again — R12 held n-32)
4939 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4940 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4941 bytes.extend_from_slice(&hw1.to_le_bytes());
4942 bytes.extend_from_slice(&hw2.to_le_bytes());
4943
4944 // LSR.W rd_lo, rn_lo, R12 (lo >> n, logical for lo word)
4945 let hw1: u16 = (0xFA20 | rn_lo_bits) as u16;
4946 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4947 bytes.extend_from_slice(&hw1.to_le_bytes());
4948 bytes.extend_from_slice(&hw2.to_le_bytes());
4949
4950 // RSB.W R12, R12, #32 (R12 = 32-n)
4951 let hw1: u16 = (0xF1C0 | r12) as u16;
4952 let hw2: u16 = ((r12 << 8) | 0x20) as u16;
4953 bytes.extend_from_slice(&hw1.to_le_bytes());
4954 bytes.extend_from_slice(&hw2.to_le_bytes());
4955
4956 // LSL.W R12, rn_hi, R12 (overflow = hi << (32-n); n=0 gives
4957 // a register shift by 32 which yields 0 — exact)
4958 let hw1: u16 = (0xFA00 | rn_hi_bits) as u16;
4959 let hw2: u16 = (0xF000 | (r12 << 8) | r12) as u16;
4960 bytes.extend_from_slice(&hw1.to_le_bytes());
4961 bytes.extend_from_slice(&hw2.to_le_bytes());
4962
4963 // ORR.W rd_lo, rd_lo, R12 (lo |= overflow from hi)
4964 let hw1: u16 = (0xEA40 | rd_lo_bits) as u16;
4965 let hw2: u16 = ((rd_lo_bits << 8) | r12) as u16;
4966 bytes.extend_from_slice(&hw1.to_le_bytes());
4967 bytes.extend_from_slice(&hw2.to_le_bytes());
4968
4969 // AND.W R12, rm_lo, #63 (n once more for the high half)
4970 let hw1: u16 = (0xF000 | rm_lo_bits) as u16;
4971 let hw2: u16 = ((r12 << 8) | 0x3F) as u16;
4972 bytes.extend_from_slice(&hw1.to_le_bytes());
4973 bytes.extend_from_slice(&hw2.to_le_bytes());
4974
4975 // ASR.W rd_hi, rn_hi, R12 (hi >> n, arithmetic/sign-extending)
4976 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4977 let hw2: u16 = (0xF000 | (rd_hi_bits << 8) | r12) as u16;
4978 bytes.extend_from_slice(&hw1.to_le_bytes());
4979 bytes.extend_from_slice(&hw2.to_le_bytes());
4980
4981 // B .done (+3 halfwords, large shift is 8 bytes)
4982 let b_done: u16 = 0xE003;
4983 bytes.extend_from_slice(&b_done.to_le_bytes());
4984
4985 // --- Large shift (n >= 32) --- (R12 still holds n-32)
4986 // ASR.W rd_lo, rn_hi, R12 (lo = hi >>> (n-32))
4987 let hw1: u16 = (0xFA40 | rn_hi_bits) as u16;
4988 let hw2: u16 = (0xF000 | (rd_lo_bits << 8) | r12) as u16;
4989 bytes.extend_from_slice(&hw1.to_le_bytes());
4990 bytes.extend_from_slice(&hw2.to_le_bytes());
4991
4992 // ASR.W rd_hi, rn_hi, #31 (hi = sign extension, all 0s or all 1s)
4993 // Thumb-2 ASR immediate: hw1=0xEA4F, hw2=imm3:Rd:imm2:10:Rm
4994 // imm5=31=11111 → imm3=111, imm2=11
4995 let hw1: u16 = 0xEA4F;
4996 let hw2: u16 = (0x7000 | (rd_hi_bits << 8) | 0x00E0 | rn_hi_bits) as u16;
4997 bytes.extend_from_slice(&hw1.to_le_bytes());
4998 bytes.extend_from_slice(&hw2.to_le_bytes());
4999
5000 Ok(bytes) // Total: 48 bytes
5001 }
5002
5003 // I64Rotl: 64-bit rotate left (#610 rewrite).
5004 // For n < 32: new_hi = (hi << n) | (lo >> (32-n)), new_lo = (lo << n) | (hi >> (32-n))
5005 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5006 //
5007 // Fixed-reg core: value in R0:R1, amount in R2, scratch R3 + R12
5008 // (all four saved/marshaled by the #610 fixed-ABI wrapper; the
5009 // pre-#610 expansion wrote through the selector's registers with
5010 // colliding R3/R4 scratch and restored the saved R4 OVER the
5011 // result). Relies on ARM register-shift semantics: amounts >= 32
5012 // yield 0 for LSL/LSR, which makes n = 0 and n = 32 exact.
5013 ArmOp::I64Rotl {
5014 rdlo,
5015 rdhi,
5016 rnlo,
5017 rnhi,
5018 shift,
5019 } => {
5020 let mut bytes = Vec::new();
5021 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5022
5023 let core: [u16; 35] = [
5024 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5025 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5026 0xD50E, // BPL .large (n >= 32)
5027 // --- small rotation (n < 32) ---
5028 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5029 0xFA20, 0xFC03, // LSR.W R12, R0, R3 (lo >> (32-n))
5030 0xFA21, 0xF303, // LSR.W R3, R1, R3 (hi >> (32-n))
5031 0xFA01, 0xF102, // LSL.W R1, R1, R2 (hi << n)
5032 0xEA41, 0x010C, // ORR.W R1, R1, R12 (new_hi)
5033 0xFA00, 0xF002, // LSL.W R0, R0, R2 (lo << n)
5034 0xEA40, 0x0003, // ORR.W R0, R0, R3 (new_lo)
5035 0xE00E, // B .done
5036 // --- large rotation (n >= 32), R3 = m = n-32 ---
5037 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5038 0xFA21, 0xFC02, // LSR.W R12, R1, R2 (hi >> (64-n))
5039 0xFA20, 0xF202, // LSR.W R2, R0, R2 (lo >> (64-n))
5040 0xFA00, 0xF003, // LSL.W R0, R0, R3 (lo << m)
5041 0xFA01, 0xF103, // LSL.W R1, R1, R3 (hi << m)
5042 0xEA40, 0x0C0C, // ORR.W R12, R0, R12 (new_hi = (lo<<m)|(hi>>(64-n)))
5043 0xEA41, 0x0002, // ORR.W R0, R1, R2 (new_lo = (hi<<m)|(lo>>(64-n)))
5044 0x4661, // MOV R1, R12 (new_hi into place)
5045 // .done: result in R0:R1
5046 ];
5047 for hw in core {
5048 bytes.extend_from_slice(&hw.to_le_bytes());
5049 }
5050
5051 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5052 Ok(bytes) // Total: 102 bytes
5053 }
5054
5055 // I64Rotr: 64-bit rotate right (#610 rewrite).
5056 // For n < 32: new_lo = (lo >> n) | (hi << (32-n)), new_hi = (hi >> n) | (lo << (32-n))
5057 // For n >= 32: same formula with lo/hi swapped, shift by m = n-32.
5058 //
5059 // Same fixed-reg core contract as I64Rotl: value in R0:R1, amount
5060 // in R2, scratch R3 + R12, all covered by the fixed-ABI wrapper.
5061 ArmOp::I64Rotr {
5062 rdlo,
5063 rdhi,
5064 rnlo,
5065 rnhi,
5066 shift,
5067 } => {
5068 let mut bytes = Vec::new();
5069 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, shift]);
5070
5071 let core: [u16; 35] = [
5072 0xF002, 0x023F, // AND.W R2, R2, #63 (mask amount mod 64)
5073 0xF1B2, 0x0320, // SUBS.W R3, R2, #32 (R3 = n-32, sets N)
5074 0xD50E, // BPL .large (n >= 32)
5075 // --- small rotation (n < 32) ---
5076 0xF1C2, 0x0320, // RSB.W R3, R2, #32 (R3 = 32-n)
5077 0xFA01, 0xFC03, // LSL.W R12, R1, R3 (hi << (32-n))
5078 0xFA00, 0xF303, // LSL.W R3, R0, R3 (lo << (32-n))
5079 0xFA20, 0xF002, // LSR.W R0, R0, R2 (lo >> n)
5080 0xEA40, 0x000C, // ORR.W R0, R0, R12 (new_lo)
5081 0xFA21, 0xF102, // LSR.W R1, R1, R2 (hi >> n)
5082 0xEA41, 0x0103, // ORR.W R1, R1, R3 (new_hi)
5083 0xE00E, // B .done
5084 // --- large rotation (n >= 32), R3 = m = n-32 ---
5085 0xF1C3, 0x0220, // RSB.W R2, R3, #32 (R2 = 32-m = 64-n)
5086 0xFA00, 0xFC02, // LSL.W R12, R0, R2 (lo << (64-n))
5087 0xFA01, 0xF202, // LSL.W R2, R1, R2 (hi << (64-n))
5088 0xFA21, 0xF103, // LSR.W R1, R1, R3 (hi >> m)
5089 0xEA41, 0x0C0C, // ORR.W R12, R1, R12 (new_lo = (hi>>m)|(lo<<(64-n)))
5090 0xFA20, 0xF103, // LSR.W R1, R0, R3 (lo >> m)
5091 0xEA41, 0x0102, // ORR.W R1, R1, R2 (new_hi = (lo>>m)|(hi<<(64-n)))
5092 0x4660, // MOV R0, R12 (new_lo into place)
5093 // .done: result in R0:R1
5094 ];
5095 for hw in core {
5096 bytes.extend_from_slice(&hw.to_le_bytes());
5097 }
5098
5099 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5100 Ok(bytes) // Total: 102 bytes
5101 }
5102
5103 // I64Clz: Count leading zeros in 64-bit value
5104 // If hi != 0: result = CLZ(hi)
5105 // If hi == 0: result = 32 + CLZ(lo)
5106 //
5107 // Layout (using CMP+BNE approach for consistency):
5108 // 0: CMP.W rnhi, #0 (4 bytes)
5109 // 4: BEQ .hi_zero (2 bytes) - branch forward to offset 14
5110 // 6: CLZ.W rd, rnhi (4 bytes)
5111 // 10: B .done (2 bytes) - branch forward to offset 22
5112 // 12: NOP (2 bytes) - padding for alignment
5113 // 14: .hi_zero: CLZ.W rd, rnlo (4 bytes)
5114 // 18: ADD.W rd, rd, #32 (4 bytes)
5115 // 22: .done
5116 ArmOp::I64Clz { rd, rnlo, rnhi } => {
5117 let rd_bits = reg_to_bits(rd);
5118 let rn_lo_bits = reg_to_bits(rnlo);
5119 let rn_hi_bits = reg_to_bits(rnhi);
5120 let mut bytes = Vec::new();
5121
5122 // CMP.W rnhi, #0 (4 bytes at offset 0)
5123 let hw1: u16 = (0xF1B0 | rn_hi_bits) as u16;
5124 let hw2: u16 = 0x0F00;
5125 bytes.extend_from_slice(&hw1.to_le_bytes());
5126 bytes.extend_from_slice(&hw2.to_le_bytes());
5127
5128 // BEQ .hi_zero (2 bytes at offset 4)
5129 // PC = 4 + 4 = 8, target = 14, offset = 6, imm8 = 3
5130 let beq: u16 = 0xD003;
5131 bytes.extend_from_slice(&beq.to_le_bytes());
5132
5133 // CLZ.W rd, rnhi (4 bytes at offset 6)
5134 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5135 let hw1: u16 = (0xFAB0 | rn_hi_bits) as u16;
5136 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_hi_bits) as u16;
5137 bytes.extend_from_slice(&hw1.to_le_bytes());
5138 bytes.extend_from_slice(&hw2.to_le_bytes());
5139
5140 // B .done (2 bytes at offset 10)
5141 // PC = 10 + 4 = 14, target = 22, offset = 8, imm11 = 4
5142 let b_done: u16 = 0xE004;
5143 bytes.extend_from_slice(&b_done.to_le_bytes());
5144
5145 // NOP (2 bytes at offset 12) - padding
5146 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5147
5148 // .hi_zero: (offset 14)
5149 // CLZ.W rd, rnlo (4 bytes)
5150 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5151 let hw1: u16 = (0xFAB0 | rn_lo_bits) as u16;
5152 let hw2: u16 = (0xF080 | (rd_bits << 8) | rn_lo_bits) as u16;
5153 bytes.extend_from_slice(&hw1.to_le_bytes());
5154 bytes.extend_from_slice(&hw2.to_le_bytes());
5155
5156 // ADD.W rd, rd, #32 (4 bytes at offset 18)
5157 let hw1: u16 = (0xF100 | rd_bits) as u16;
5158 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5159 bytes.extend_from_slice(&hw1.to_le_bytes());
5160 bytes.extend_from_slice(&hw2.to_le_bytes());
5161
5162 // .done: (offset 22 — the end of the expansion)
5163 //
5164 // #1048: the former trailing hi-word clear (`MOV rnhi, #0`)
5165 // is GONE. It was aimed at the RESULT's high half but wrote
5166 // the OPERAND's home high register — a real executed
5167 // miscompile on the direct selector, which allocates a fresh
5168 // destination pair and zeroes its own dst_hi, leaving the
5169 // operand's hi limb destroyed for any later re-read. The
5170 // Rocq/SMT models of I64ClzPseudo always said "writes rd
5171 // ONLY"; the callers that relied on the implicit clear
5172 // (select_default, optimizer_bridge) now emit their own
5173 // explicit hi-zero op. `B .done` above targets offset 22 =
5174 // past-the-end, `BEQ` targets offset 14 — no displacement
5175 // moves.
5176
5177 Ok(bytes) // 22 bytes, register-independent
5178 }
5179
5180 // I64Ctz: Count trailing zeros in 64-bit value
5181 // If lo != 0: result = CTZ(lo) = CLZ(RBIT(lo))
5182 // If lo == 0: result = 32 + CTZ(hi) = 32 + CLZ(RBIT(hi))
5183 //
5184 // Layout:
5185 // 0: CMP.W rnlo, #0 (4 bytes)
5186 // 4: BEQ .lo_zero (2 bytes) - branch to offset 18
5187 // 6: RBIT.W rd, rnlo (4 bytes)
5188 // 10: CLZ.W rd, rd (4 bytes)
5189 // 14: B .done (2 bytes) - branch to offset 30
5190 // 16: NOP (2 bytes) - padding
5191 // 18: .lo_zero: RBIT.W rd, rnhi (4 bytes)
5192 // 22: CLZ.W rd, rd (4 bytes)
5193 // 26: ADD.W rd, rd, #32 (4 bytes)
5194 // 30: .done
5195 ArmOp::I64Ctz { rd, rnlo, rnhi } => {
5196 let rd_bits = reg_to_bits(rd);
5197 let rn_lo_bits = reg_to_bits(rnlo);
5198 let rn_hi_bits = reg_to_bits(rnhi);
5199 let mut bytes = Vec::new();
5200
5201 // CMP.W rnlo, #0 (4 bytes at offset 0)
5202 let hw1: u16 = (0xF1B0 | rn_lo_bits) as u16;
5203 let hw2: u16 = 0x0F00;
5204 bytes.extend_from_slice(&hw1.to_le_bytes());
5205 bytes.extend_from_slice(&hw2.to_le_bytes());
5206
5207 // BEQ .lo_zero (2 bytes at offset 4)
5208 // PC = 4 + 4 = 8, target = 18, offset = 10, imm8 = 5
5209 let beq: u16 = 0xD005;
5210 bytes.extend_from_slice(&beq.to_le_bytes());
5211
5212 // RBIT.W rd, rnlo (4 bytes at offset 6)
5213 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5214 let hw1: u16 = (0xFA90 | rn_lo_bits) as u16;
5215 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_lo_bits) as u16;
5216 bytes.extend_from_slice(&hw1.to_le_bytes());
5217 bytes.extend_from_slice(&hw2.to_le_bytes());
5218
5219 // CLZ.W rd, rd (4 bytes at offset 10)
5220 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5221 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5222 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5223 bytes.extend_from_slice(&hw1.to_le_bytes());
5224 bytes.extend_from_slice(&hw2.to_le_bytes());
5225
5226 // B .done (2 bytes at offset 14)
5227 // PC = 14 + 4 = 18, target = 30, offset = 12, imm11 = 6
5228 let b_done: u16 = 0xE006;
5229 bytes.extend_from_slice(&b_done.to_le_bytes());
5230
5231 // NOP (2 bytes at offset 16) - padding
5232 bytes.extend_from_slice(&0xBF00u16.to_le_bytes());
5233
5234 // .lo_zero: (offset 18)
5235 // RBIT.W rd, rnhi (4 bytes)
5236 // RBIT T1: hw1 = 0xFA9<Rm>, hw2 = 0xF<Rd>A<Rm>
5237 let hw1: u16 = (0xFA90 | rn_hi_bits) as u16;
5238 let hw2: u16 = (0xF0A0 | (rd_bits << 8) | rn_hi_bits) as u16;
5239 bytes.extend_from_slice(&hw1.to_le_bytes());
5240 bytes.extend_from_slice(&hw2.to_le_bytes());
5241
5242 // CLZ.W rd, rd (4 bytes at offset 22)
5243 // CLZ T1: hw1 = 0xFAB<Rm>, hw2 = 0xF<Rd>8<Rm>
5244 let hw1: u16 = (0xFAB0 | rd_bits) as u16;
5245 let hw2: u16 = (0xF080 | (rd_bits << 8) | rd_bits) as u16;
5246 bytes.extend_from_slice(&hw1.to_le_bytes());
5247 bytes.extend_from_slice(&hw2.to_le_bytes());
5248
5249 // ADD.W rd, rd, #32 (4 bytes at offset 26)
5250 let hw1: u16 = (0xF100 | rd_bits) as u16;
5251 let hw2: u16 = ((rd_bits << 8) | 0x20) as u16;
5252 bytes.extend_from_slice(&hw1.to_le_bytes());
5253 bytes.extend_from_slice(&hw2.to_le_bytes());
5254
5255 // .done: (offset 30 — the end of the expansion)
5256 // #1048: the former trailing `MOV rnhi, #0` is GONE — it
5257 // wrote the OPERAND's home high register (see the I64Clz
5258 // comment above). `B .done` targets offset 30 = past-the-end,
5259 // `BEQ` targets offset 18 — no displacement moves.
5260
5261 Ok(bytes) // 30 bytes, register-independent
5262 }
5263
5264 // I64Popcnt: Population count of 64-bit value
5265 // result = POPCNT(lo) + POPCNT(hi)
5266 // Using SIMD-style parallel bit counting algorithm
5267 ArmOp::I64Popcnt { rd, rnlo, rnhi } => {
5268 let rd_bits = reg_to_bits(rd);
5269 let rn_lo_bits = reg_to_bits(rnlo);
5270 let rn_hi_bits = reg_to_bits(rnhi);
5271 let r12: u32 = 12; // IP scratch
5272 let r3: u32 = 3; // Scratch for hi popcnt result
5273 let mut bytes = Vec::new();
5274
5275 // PUSH {R3, R4, R5} - save scratch registers
5276 bytes.extend_from_slice(&0xB438u16.to_le_bytes());
5277
5278 // Strategy: compute popcnt(lo) -> R4, popcnt(hi) -> R5, add them -> rd
5279 // Using lookup table approach for each byte would be too large
5280 // Using shift-and-add approach instead
5281
5282 // For simplicity and correctness, use the efficient parallel algorithm
5283 // but implement it as a series of inline operations
5284
5285 // Marshal the operand pair into the fixed scratch regs, routing
5286 // rnlo through R12 (#632 audit): writing R4 first corrupted the
5287 // rnhi read for a pair living at (R3,R4) — every source is read
5288 // before any scratch register it could occupy is written.
5289 // MOV R12, rnlo
5290 let mov: u16 = (0x4600 | (1 << 7) | (rn_lo_bits << 3) | 4) as u16;
5291 bytes.extend_from_slice(&mov.to_le_bytes());
5292 // MOV R5, rnhi (R4 untouched so far; rnhi == R5 is a no-op)
5293 let mov: u16 = (0x4600 | (rn_hi_bits << 3) | 5) as u16;
5294 bytes.extend_from_slice(&mov.to_le_bytes());
5295 // MOV R4, R12
5296 bytes.extend_from_slice(&0x4664u16.to_le_bytes());
5297
5298 // --- POPCNT for R4 (lo word) ---
5299 // Step 1: x = x - ((x >> 1) & 0x55555555)
5300 // LSR.W R12, R4, #1
5301 let hw1: u16 = 0xEA4F;
5302 let hw2: u16 = ((r12 << 8) | 0x50 | 4) as u16;
5303 bytes.extend_from_slice(&hw1.to_le_bytes());
5304 bytes.extend_from_slice(&hw2.to_le_bytes());
5305
5306 // Load 0x55555555 into R3 using MOVW/MOVT
5307 // MOVW R3, #0x5555
5308 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5309 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5310 // MOVT R3, #0x5555
5311 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5312 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5313
5314 // AND.W R12, R12, R3
5315 let hw1: u16 = (0xEA00 | r12) as u16;
5316 let hw2: u16 = ((r12 << 8) | r3) as u16;
5317 bytes.extend_from_slice(&hw1.to_le_bytes());
5318 bytes.extend_from_slice(&hw2.to_le_bytes());
5319
5320 // SUB.W R4, R4, R12
5321 let hw1: u16 = (0xEBA0 | 4) as u16;
5322 let hw2: u16 = ((4 << 8) | r12) as u16;
5323 bytes.extend_from_slice(&hw1.to_le_bytes());
5324 bytes.extend_from_slice(&hw2.to_le_bytes());
5325
5326 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5327 // Load 0x33333333 into R3
5328 // MOVW R3, #0x3333
5329 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5330 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5331 // MOVT R3, #0x3333
5332 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5333 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5334
5335 // AND.W R12, R4, R3
5336 let hw1: u16 = (0xEA00 | 4) as u16;
5337 let hw2: u16 = ((r12 << 8) | r3) as u16;
5338 bytes.extend_from_slice(&hw1.to_le_bytes());
5339 bytes.extend_from_slice(&hw2.to_le_bytes());
5340
5341 // LSR.W R4, R4, #2
5342 let hw1: u16 = 0xEA4F;
5343 let hw2: u16 = ((4 << 8) | 0x90 | 4) as u16;
5344 bytes.extend_from_slice(&hw1.to_le_bytes());
5345 bytes.extend_from_slice(&hw2.to_le_bytes());
5346
5347 // AND.W R4, R4, R3
5348 let hw1: u16 = (0xEA00 | 4) as u16;
5349 let hw2: u16 = ((4 << 8) | r3) as u16;
5350 bytes.extend_from_slice(&hw1.to_le_bytes());
5351 bytes.extend_from_slice(&hw2.to_le_bytes());
5352
5353 // ADD.W R4, R4, R12
5354 let hw1: u16 = (0xEB00 | 4) as u16;
5355 let hw2: u16 = ((4 << 8) | r12) as u16;
5356 bytes.extend_from_slice(&hw1.to_le_bytes());
5357 bytes.extend_from_slice(&hw2.to_le_bytes());
5358
5359 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5360 // LSR.W R12, R4, #4
5361 // hw2 = (imm3 << 12) | (Rd << 8) | (imm2 << 6) | (type << 4) | Rm
5362 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5363 let hw1: u16 = 0xEA4F;
5364 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 4) as u16;
5365 bytes.extend_from_slice(&hw1.to_le_bytes());
5366 bytes.extend_from_slice(&hw2.to_le_bytes());
5367
5368 // ADD.W R4, R4, R12
5369 let hw1: u16 = (0xEB00 | 4) as u16;
5370 let hw2: u16 = ((4 << 8) | r12) as u16;
5371 bytes.extend_from_slice(&hw1.to_le_bytes());
5372 bytes.extend_from_slice(&hw2.to_le_bytes());
5373
5374 // Load 0x0F0F0F0F into R3
5375 // MOVW R3, #0x0F0F (imm4=0, i=1, imm3=7, imm8=0x0F)
5376 // hw1 = 11110 1 10 0100 0000 = 0xF640
5377 // hw2 = 0 111 0011 00001111 = 0x730F
5378 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5379 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5380 // MOVT R3, #0x0F0F
5381 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5382 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5383
5384 // AND.W R4, R4, R3
5385 let hw1: u16 = (0xEA00 | 4) as u16;
5386 let hw2: u16 = ((4 << 8) | r3) as u16;
5387 bytes.extend_from_slice(&hw1.to_le_bytes());
5388 bytes.extend_from_slice(&hw2.to_le_bytes());
5389
5390 // Step 4: x = x * 0x01010101 >> 24
5391 // Load 0x01010101 into R3
5392 // MOVW R3, #0x0101
5393 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5394 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5395 // MOVT R3, #0x0101
5396 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5397 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5398
5399 // MUL R4, R4, R3
5400 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5401 let hw1: u16 = (0xFB00 | 4) as u16;
5402 let hw2: u16 = (0xF000 | (4 << 8) | r3) as u16;
5403 bytes.extend_from_slice(&hw1.to_le_bytes());
5404 bytes.extend_from_slice(&hw2.to_le_bytes());
5405
5406 // LSR.W R4, R4, #24
5407 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5408 let hw1: u16 = 0xEA4F;
5409 let hw2: u16 = (0x6000 | (4 << 8) | 0x10 | 4) as u16;
5410 bytes.extend_from_slice(&hw1.to_le_bytes());
5411 bytes.extend_from_slice(&hw2.to_le_bytes());
5412
5413 // --- POPCNT for R5 (hi word) - same algorithm ---
5414 // Step 1
5415 let hw1: u16 = 0xEA4F;
5416 let hw2: u16 = ((r12 << 8) | 0x50 | 5) as u16;
5417 bytes.extend_from_slice(&hw1.to_le_bytes());
5418 bytes.extend_from_slice(&hw2.to_le_bytes());
5419
5420 // Load 0x55555555 into R3
5421 bytes.extend_from_slice(&0xF245u16.to_le_bytes());
5422 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5423 bytes.extend_from_slice(&0xF2C5u16.to_le_bytes());
5424 bytes.extend_from_slice(&0x5355u16.to_le_bytes());
5425
5426 let hw1: u16 = (0xEA00 | r12) as u16;
5427 let hw2: u16 = ((r12 << 8) | r3) as u16;
5428 bytes.extend_from_slice(&hw1.to_le_bytes());
5429 bytes.extend_from_slice(&hw2.to_le_bytes());
5430
5431 let hw1: u16 = (0xEBA0 | 5) as u16;
5432 let hw2: u16 = ((5 << 8) | r12) as u16;
5433 bytes.extend_from_slice(&hw1.to_le_bytes());
5434 bytes.extend_from_slice(&hw2.to_le_bytes());
5435
5436 // Step 2
5437 bytes.extend_from_slice(&0xF243u16.to_le_bytes());
5438 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5439 bytes.extend_from_slice(&0xF2C3u16.to_le_bytes());
5440 bytes.extend_from_slice(&0x3333u16.to_le_bytes());
5441
5442 let hw1: u16 = (0xEA00 | 5) as u16;
5443 let hw2: u16 = ((r12 << 8) | r3) as u16;
5444 bytes.extend_from_slice(&hw1.to_le_bytes());
5445 bytes.extend_from_slice(&hw2.to_le_bytes());
5446
5447 let hw1: u16 = 0xEA4F;
5448 let hw2: u16 = ((5 << 8) | 0x90 | 5) as u16;
5449 bytes.extend_from_slice(&hw1.to_le_bytes());
5450 bytes.extend_from_slice(&hw2.to_le_bytes());
5451
5452 let hw1: u16 = (0xEA00 | 5) as u16;
5453 let hw2: u16 = ((5 << 8) | r3) as u16;
5454 bytes.extend_from_slice(&hw1.to_le_bytes());
5455 bytes.extend_from_slice(&hw2.to_le_bytes());
5456
5457 let hw1: u16 = (0xEB00 | 5) as u16;
5458 let hw2: u16 = ((5 << 8) | r12) as u16;
5459 bytes.extend_from_slice(&hw1.to_le_bytes());
5460 bytes.extend_from_slice(&hw2.to_le_bytes());
5461
5462 // Step 3: LSR.W R12, R5, #4
5463 // imm5=4=00100 → imm3=1, imm2=0, type=01(LSR)
5464 let hw1: u16 = 0xEA4F;
5465 let hw2: u16 = (0x1000 | (r12 << 8) | 0x10 | 5) as u16;
5466 bytes.extend_from_slice(&hw1.to_le_bytes());
5467 bytes.extend_from_slice(&hw2.to_le_bytes());
5468
5469 let hw1: u16 = (0xEB00 | 5) as u16;
5470 let hw2: u16 = ((5 << 8) | r12) as u16;
5471 bytes.extend_from_slice(&hw1.to_le_bytes());
5472 bytes.extend_from_slice(&hw2.to_le_bytes());
5473
5474 // Load 0x0F0F0F0F into R3 (for hi-word)
5475 bytes.extend_from_slice(&0xF640u16.to_le_bytes());
5476 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5477 bytes.extend_from_slice(&0xF6C0u16.to_le_bytes());
5478 bytes.extend_from_slice(&0x730Fu16.to_le_bytes());
5479
5480 let hw1: u16 = (0xEA00 | 5) as u16;
5481 let hw2: u16 = ((5 << 8) | r3) as u16;
5482 bytes.extend_from_slice(&hw1.to_le_bytes());
5483 bytes.extend_from_slice(&hw2.to_le_bytes());
5484
5485 // Step 4
5486 bytes.extend_from_slice(&0xF240u16.to_le_bytes());
5487 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5488 bytes.extend_from_slice(&0xF2C0u16.to_le_bytes());
5489 bytes.extend_from_slice(&0x1301u16.to_le_bytes());
5490
5491 // MUL R5, R5, R3
5492 // MUL T2: hw1 = 0xFB00|Rn, hw2 = 0xF000|(Rd<<8)|Rm
5493 let hw1: u16 = (0xFB00 | 5) as u16;
5494 let hw2: u16 = (0xF000 | (5 << 8) | r3) as u16;
5495 bytes.extend_from_slice(&hw1.to_le_bytes());
5496 bytes.extend_from_slice(&hw2.to_le_bytes());
5497
5498 // LSR.W R5, R5, #24
5499 // imm5=24=11000 → imm3=6, imm2=0, type=01(LSR)
5500 let hw1: u16 = 0xEA4F;
5501 let hw2: u16 = (0x6000 | (5 << 8) | 0x10 | 5) as u16;
5502 bytes.extend_from_slice(&hw1.to_le_bytes());
5503 bytes.extend_from_slice(&hw2.to_le_bytes());
5504
5505 // #632: the count must be carried ACROSS the scratch restore
5506 // in a register the POP cannot touch. rd is allocator-assigned
5507 // (any of R0-R8) and can land inside the {R3,R4,R5} restore set
5508 // — the old `ADDS rd, R4, R5; POP {R3,R4,R5}` destroyed the
5509 // result one instruction after computing it (0 for every input
5510 // under qemu). R12 is encoder scratch: never allocatable (#212)
5511 // and never in a restore set, so no choice of rd can collide.
5512 // ADD.W R12, R4, R5
5513 bytes.extend_from_slice(&0xEB04u16.to_le_bytes());
5514 bytes.extend_from_slice(&0x0C05u16.to_le_bytes());
5515
5516 // POP {R3, R4, R5}
5517 bytes.extend_from_slice(&0xBC38u16.to_le_bytes());
5518
5519 // MOV rd, R12 — after the restore. The 4-bit Rd (D:rd) form is
5520 // also total over rd = R8, where the old ADDS T1 3-bit field
5521 // silently corrupted the encoding (#178/#180 class).
5522 let mov: u16 =
5523 (0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7)) as u16;
5524 bytes.extend_from_slice(&mov.to_le_bytes());
5525
5526 // #1048: the former trailing `MOV.W rnhi, #0` hi-word clear
5527 // is GONE — it wrote the OPERAND's home high register (see
5528 // the I64Clz comment). Callers that relied on the implicit
5529 // clear emit their own explicit hi-zero op.
5530
5531 Ok(bytes)
5532 }
5533
5534 // I64Extend8S: Sign-extend low 8 bits to 64 bits
5535 // Result: rdlo = sign_extend_8(rnlo), rdhi = rdlo >> 31
5536 ArmOp::I64Extend8S { rdlo, rdhi, rnlo } => {
5537 let rdlo_bits = reg_to_bits(rdlo);
5538 let rdhi_bits = reg_to_bits(rdhi);
5539 let rnlo_bits = reg_to_bits(rnlo);
5540 let mut bytes = Vec::new();
5541
5542 // SXTB.W rdlo, rnlo (sign-extend byte to 32-bit)
5543 // SXTB T2: hw1 = 0xFA4F, hw2 = 0xF0<Rd><Rm>
5544 let hw1: u16 = 0xFA4F_u16;
5545 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5546 bytes.extend_from_slice(&hw1.to_le_bytes());
5547 bytes.extend_from_slice(&hw2.to_le_bytes());
5548
5549 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5550 // ASR (immediate): hw1 = 0xEA4F, hw2 = imm3:Rd:imm2:type:Rm
5551 // For imm5=31: imm3=111, imm2=11, type=10 (ASR)
5552 // hw2 = (7 << 12) | (rdhi << 8) | (3 << 6) | (2 << 4) | rdlo
5553 let hw1: u16 = 0xEA4F;
5554 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5555 bytes.extend_from_slice(&hw1.to_le_bytes());
5556 bytes.extend_from_slice(&hw2.to_le_bytes());
5557
5558 Ok(bytes)
5559 }
5560
5561 // I64Extend16S: Sign-extend low 16 bits to 64 bits
5562 // Result: rdlo = sign_extend_16(rnlo), rdhi = rdlo >> 31
5563 ArmOp::I64Extend16S { rdlo, rdhi, rnlo } => {
5564 let rdlo_bits = reg_to_bits(rdlo);
5565 let rdhi_bits = reg_to_bits(rdhi);
5566 let rnlo_bits = reg_to_bits(rnlo);
5567 let mut bytes = Vec::new();
5568
5569 // SXTH.W rdlo, rnlo (sign-extend halfword to 32-bit)
5570 // SXTH T2: hw1 = 0xFA0F, hw2 = 0xF0<Rd><Rm>
5571 let hw1: u16 = 0xFA0F_u16;
5572 let hw2: u16 = (0xF080 | (rdlo_bits << 8) | rnlo_bits) as u16;
5573 bytes.extend_from_slice(&hw1.to_le_bytes());
5574 bytes.extend_from_slice(&hw2.to_le_bytes());
5575
5576 // ASR.W rdhi, rdlo, #31 (sign-extend to high word)
5577 let hw1: u16 = 0xEA4F;
5578 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rdlo_bits) as u16;
5579 bytes.extend_from_slice(&hw1.to_le_bytes());
5580 bytes.extend_from_slice(&hw2.to_le_bytes());
5581
5582 Ok(bytes)
5583 }
5584
5585 // I64Extend32S: Sign-extend low 32 bits to 64 bits
5586 // Result: rdlo = rnlo, rdhi = rnlo >> 31
5587 ArmOp::I64Extend32S { rdlo, rdhi, rnlo } => {
5588 let rdlo_bits = reg_to_bits(rdlo);
5589 let rdhi_bits = reg_to_bits(rdhi);
5590 let rnlo_bits = reg_to_bits(rnlo);
5591 let mut bytes = Vec::new();
5592
5593 // MOV rdlo, rnlo (if different)
5594 if rdlo_bits != rnlo_bits {
5595 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5596 let d_bit = ((rdlo_bits >> 3) & 1) as u16;
5597 let mov: u16 = 0x4600
5598 | (d_bit << 7)
5599 | ((rnlo_bits as u16) << 3)
5600 | ((rdlo_bits & 0x7) as u16);
5601 bytes.extend_from_slice(&mov.to_le_bytes());
5602 }
5603
5604 // ASR.W rdhi, rnlo, #31 (sign-extend to high word)
5605 let hw1: u16 = 0xEA4F;
5606 let hw2: u16 = (0x70E0 | (rdhi_bits << 8) | rnlo_bits) as u16;
5607 bytes.extend_from_slice(&hw1.to_le_bytes());
5608 bytes.extend_from_slice(&hw2.to_le_bytes());
5609
5610 Ok(bytes)
5611 }
5612
5613 // SelectMove: IT <cond>; MOV{cond} rd, rm
5614 // Conditional move: only execute MOV if condition is true
5615 ArmOp::SelectMove { rd, rm, cond } => {
5616 let rd_bits = reg_to_bits(rd) as u16;
5617 let rm_bits = reg_to_bits(rm) as u16;
5618
5619 // Condition code encoding for IT block
5620 use synth_synthesis::Condition;
5621 let cond_bits: u16 = match cond {
5622 Condition::EQ => 0x0, // Equal
5623 Condition::NE => 0x1, // Not equal
5624 Condition::HS => 0x2, // Higher or same (unsigned >=)
5625 Condition::LO => 0x3, // Lower (unsigned <)
5626 Condition::HI => 0x8, // Higher (unsigned >)
5627 Condition::LS => 0x9, // Lower or same (unsigned <=)
5628 Condition::GE => 0xA, // Greater or equal (signed)
5629 Condition::LT => 0xB, // Less than (signed)
5630 Condition::GT => 0xC, // Greater than (signed)
5631 Condition::LE => 0xD, // Less or equal (signed)
5632 };
5633
5634 // IT <cond>: single Then block (mask = 0x8 for T only)
5635 // IT instruction: 1011 1111 firstcond mask
5636 let it_instr: u16 = 0xBF00 | (cond_bits << 4) | 0x8;
5637
5638 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5639 // This MOV will only execute if condition is true due to IT block
5640 let d_bit = (rd_bits >> 3) & 1;
5641 let mov_instr: u16 = 0x4600 | (d_bit << 7) | (rm_bits << 3) | (rd_bits & 0x7);
5642
5643 // Emit: IT <cond>, MOV rd, rm
5644 let mut bytes = it_instr.to_le_bytes().to_vec();
5645 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5646 Ok(bytes)
5647 }
5648
5649 // Popcnt: Population count (count set bits)
5650 // ARM Cortex-M has no native POPCNT, so we implement the bit manipulation algorithm:
5651 // x = x - ((x >> 1) & 0x55555555);
5652 // x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
5653 // x = (x + (x >> 4)) & 0x0F0F0F0F;
5654 // x = x + (x >> 8);
5655 // x = x + (x >> 16);
5656 // return x & 0x3F;
5657 //
5658 // #1021: R12 (IP, never allocatable) is the ONLY scratch. The
5659 // previous expansion borrowed R11 as a second temp — but R11 is
5660 // the WASM linear-memory base, materialized at entry and read by
5661 // every later LDR/STR, and it is NOT in the pushed set, so the
5662 // clobber leaked to the CALLER too (a live memory-safety
5663 // miscompile: loads through `base = x >> 16`). The second temp is
5664 // eliminated the way the healthy i64.popcnt discipline implies —
5665 // never touch an unsaved register — but without its PUSH/POP
5666 // wrapper: the SWAR masks 0x55555555 / 0x33333333 / 0x0F0F0F0F
5667 // are all `0xXYXYXYXY` ThumbExpandImm modified immediates, so
5668 // each AND takes its mask from the instruction itself and R12
5669 // alone carries every intermediate. Straight-line, no branches,
5670 // no stack traffic — nothing to skip on a trap edge.
5671 ArmOp::Popcnt { rd, rm } => {
5672 let rd_bits = reg_to_bits(rd);
5673 // Defensive (#1021): rd = R11/R12/SP/PC would silently
5674 // corrupt the linear-memory base, the expansion's own
5675 // scratch, or the stack. The selector never assigns them
5676 // (pool R0-R8); refuse loudly if that ever changes.
5677 if rd_bits >= 11 {
5678 return Err(synth_core::Error::synthesis(
5679 "Popcnt destination must be R0-R10: R11 is the linear-memory \
5680 base and R12 is the expansion's scratch (#1021)",
5681 ));
5682 }
5683 let mut bytes = Vec::new();
5684
5685 // First, move rm to rd if they're different
5686 if rd != rm {
5687 let rm_bits = reg_to_bits(rm) as u16;
5688 // MOV Rd, Rm (16-bit): 0100 0110 D Rm Rd[2:0]
5689 let d_bit = ((rd_bits as u16) >> 3) & 1;
5690 let mov_instr: u16 =
5691 0x4600 | (d_bit << 7) | (rm_bits << 3) | ((rd_bits as u16) & 0x7);
5692 bytes.extend_from_slice(&mov_instr.to_le_bytes());
5693 }
5694
5695 // Step 1: x = x - ((x >> 1) & 0x55555555)
5696 // R12 = rd >> 1
5697 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 1)?);
5698 // R12 = R12 & 0x55555555 (modified immediate, no constant reg)
5699 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(12, 12, 0x5555_5555)?);
5700 // rd = rd - R12
5701 bytes.extend_from_slice(&self.encode_thumb32_sub_reg_raw(rd_bits, rd_bits, 12)?);
5702
5703 // Step 2: x = (x & 0x33333333) + ((x >> 2) & 0x33333333)
5704 // R12 = rd & 0x33333333
5705 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5706 12,
5707 rd_bits,
5708 0x3333_3333,
5709 )?);
5710 // rd = rd >> 2
5711 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(rd_bits, rd_bits, 2)?);
5712 // rd = rd & 0x33333333
5713 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5714 rd_bits,
5715 rd_bits,
5716 0x3333_3333,
5717 )?);
5718 // rd = rd + R12
5719 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5720
5721 // Step 3: x = (x + (x >> 4)) & 0x0F0F0F0F
5722 // R12 = rd >> 4
5723 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 4)?);
5724 // rd = rd + R12
5725 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5726 // rd = rd & 0x0F0F0F0F
5727 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(
5728 rd_bits,
5729 rd_bits,
5730 0x0F0F_0F0F,
5731 )?);
5732
5733 // Step 4: x = x + (x >> 8)
5734 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 8)?);
5735 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5736
5737 // Step 5: x = x + (x >> 16)
5738 bytes.extend_from_slice(&self.encode_thumb32_lsr_raw(12, rd_bits, 16)?);
5739 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rd_bits, 12)?);
5740
5741 // Step 6: return x & 0x3F
5742 bytes.extend_from_slice(&self.encode_thumb32_and_imm_raw(rd_bits, rd_bits, 0x3F)?);
5743
5744 Ok(bytes)
5745 }
5746
5747 // I64DivU: 64-bit unsigned division using binary long division
5748 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = quotient
5749 // Uses: R4-R7, R12 as loop counter (avoid R8 for Renode compatibility)
5750 //
5751 // #610: the fixed-ABI wrapper marshals the selector-assigned
5752 // operand registers into the core's fixed regs and lands the
5753 // result in rd — pre-#610 this arm IGNORED its register fields,
5754 // so the selector read its rd pair (e.g. R4:R5) after the core's
5755 // own POP restored the stale caller values over it: 0 for every
5756 // input. A zero divisor now traps (UDF #0), per WASM semantics.
5757 ArmOp::I64DivU {
5758 rdlo,
5759 rdhi,
5760 rnlo,
5761 rnhi,
5762 rmlo,
5763 rmhi,
5764 elide_zero_guard,
5765 } => {
5766 let mut bytes = Vec::new();
5767 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5768 // #494 phase 2b: elided only under a certificate-discharged
5769 // UNSAT(P ∧ divisor == 0) obligation (fact-spec pass).
5770 if !elide_zero_guard {
5771 emit_i64_divisor_zero_trap(&mut bytes);
5772 }
5773
5774 // PUSH {R4-R7} - save scratch registers (NO LR — this is inline code)
5775 // 16-bit PUSH: 1011 010 M rrrrrrrr where M=0 (no LR), r=R4-R7 = 0xF0
5776 // Encoding: 1011 0100 1111 0000 = 0xB4F0
5777 bytes.extend_from_slice(&0xB4F0u16.to_le_bytes());
5778
5779 // Initialize quotient (R4:R5) = 0
5780 bytes.extend_from_slice(&0x2400u16.to_le_bytes()); // MOV R4, #0
5781 bytes.extend_from_slice(&0x2500u16.to_le_bytes()); // MOV R5, #0
5782
5783 // Initialize remainder (R6:R7) = 0
5784 bytes.extend_from_slice(&0x2600u16.to_le_bytes()); // MOV R6, #0
5785 bytes.extend_from_slice(&0x2700u16.to_le_bytes()); // MOV R7, #0
5786
5787 // Initialize loop counter R12 = 64 (use R12 scratch instead of R8)
5788 // MOV.W R12, #64: F04F 0C40
5789 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5790 bytes.extend_from_slice(&0x0C40u16.to_le_bytes());
5791
5792 // Loop start
5793 let loop_start = bytes.len();
5794
5795 // === Loop body: process one bit ===
5796
5797 // 1. Shift quotient R4:R5 left by 1
5798 // LSLS R5, R5, #1 (16-bit: 0000 0010 1010 1101 = 0x006D -> actually 0x002D for LSL R5,R5,#1)
5799 // LSL Rd, Rm, #imm5: 000 00 imm5 Rm Rd = 000 00 00001 101 101 = 0x006D
5800 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5801 // Get carry from R4 into R5: ORR R5, R5, R4 LSR #31
5802 // Thumb-2 ORR with shifted register: EA45 75D4 = ORR.W R5, R5, R4, LSR #31
5803 // 11101010 010 S Rn | 0 imm3 Rd imm2 type Rm
5804 // type=01 (LSR), imm5=31 (imm3=111, imm2=11)
5805 bytes.extend_from_slice(&0xEA45u16.to_le_bytes());
5806 bytes.extend_from_slice(&0x75D4u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5807 // LSLS R4, R4, #1: 000 00 00001 100 100 = 0x0064
5808 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5809
5810 // 2. Shift remainder R6:R7 left by 1, OR in MSB of dividend R1
5811 // LSLS R7, R7, #1
5812 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5813 // ORR.W R7, R7, R6, LSR #31
5814 bytes.extend_from_slice(&0xEA47u16.to_le_bytes());
5815 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5816 // LSLS R6, R6, #1
5817 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5818 // ORR.W R6, R6, R1, LSR #31 (bring in MSB of dividend high)
5819 bytes.extend_from_slice(&0xEA46u16.to_le_bytes());
5820 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5821
5822 // 3. Shift dividend R0:R1 left by 1
5823 // LSLS R1, R1, #1
5824 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5825 // ORR.W R1, R1, R0, LSR #31
5826 bytes.extend_from_slice(&0xEA41u16.to_le_bytes());
5827 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5828 // LSLS R0, R0, #1
5829 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5830
5831 // 4. Compare remainder >= divisor (64-bit unsigned comparison)
5832 // Compare high words first: CMP R7, R3
5833 // CMP Rn, Rm encoding: 0x4280 | (Rm << 3) | Rn
5834 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3 (16-bit)
5835 // BHI means R7 > R3 (unsigned) - definitely subtract
5836 // BLO means R7 < R3 - definitely don't subtract
5837 // BEQ means need to check low words
5838
5839 // If high > divisor high: branch to subtract (forward +offset)
5840 // BHI.N +6 (skip CMP, skip BLO, do subtract)
5841 // BHI: 1101 1000 offset8 where cond=1000 (HI)
5842 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4 (to subtract block)
5843
5844 // If high < divisor high: branch past subtract
5845 // BLO.N +10 (skip to decrement)
5846 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BLO/BCC +12 (past subtract)
5847
5848 // High words equal, compare low: CMP R6, R2
5849 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2 (16-bit)
5850 // BLO/BCC past subtract (skip SUBS+SBC.W+ORR.W = 10 bytes = 4 halfwords from PC+4)
5851 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords (past subtract)
5852
5853 // === Subtract block: remainder -= divisor, quotient |= 1 ===
5854 // SUBS R6, R6, R2
5855 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2 (16-bit)
5856 // SBC R7, R7, R3 (with borrow)
5857 // Thumb-2 SBC.W: EB67 0703 = SBC.W R7, R7, R3
5858 bytes.extend_from_slice(&0xEB67u16.to_le_bytes());
5859 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5860 // ORR R4, R4, #1 (set bit 0 of quotient low)
5861 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5862 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
5863
5864 // === Decrement counter and loop ===
5865 // SUBS.W R12, R12, #1 (decrement loop counter)
5866 // SUBS.W R12, R12, #1: F1BC 0C01
5867 bytes.extend_from_slice(&0xF1BCu16.to_le_bytes());
5868 bytes.extend_from_slice(&0x0C01u16.to_le_bytes());
5869
5870 // BNE back to loop_start
5871 let branch_offset_bytes = bytes.len() - loop_start + 4; // +4 for pipeline
5872 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
5873 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
5874 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
5875
5876 // === Loop done, move quotient to R0:R1 ===
5877 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
5878 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
5879
5880 // POP {R4-R7} - restore scratch registers (NO PC — inline code continues)
5881 // 16-bit POP: 1011 110 P rrrrrrrr where P=0 (no PC), r=R4-R7 = 0xF0
5882 // Encoding: 1011 1100 1111 0000 = 0xBCF0
5883 bytes.extend_from_slice(&0xBCF0u16.to_le_bytes());
5884
5885 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
5886 Ok(bytes)
5887 }
5888
5889 // I64DivS: 64-bit signed division
5890 // Converts to unsigned, divides, then applies sign
5891 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
5892 // -> R0:R1 = quotient (signed)
5893 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
5894 ArmOp::I64DivS {
5895 rdlo,
5896 rdhi,
5897 rnlo,
5898 rnhi,
5899 rmlo,
5900 rmhi,
5901 elide_zero_guard,
5902 elide_overflow_guard,
5903 } => {
5904 let mut bytes = Vec::new();
5905 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
5906 // #494 phase 2b: two INDEPENDENT guards, two INDEPENDENT
5907 // obligations. The zero guard falls to UNSAT(P ∧ divisor == 0);
5908 // the #633 overflow guard falls ONLY to
5909 // UNSAT(P ∧ dividend == INT64_MIN ∧ divisor == -1) — a
5910 // divisor-nonzero fact alone must keep it.
5911 if !elide_zero_guard {
5912 emit_i64_divisor_zero_trap(&mut bytes);
5913 }
5914 if !elide_overflow_guard {
5915 // #633: INT64_MIN / -1 overflows — trap like the i32 path
5916 // (rem_s stays guard-free: rem_s(INT64_MIN, -1) == 0).
5917 emit_i64_divs_overflow_trap(&mut bytes);
5918 }
5919
5920 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
5921 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
5922 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
5923
5924 // Save result sign in R9: R9 = R1 XOR R3 (sign bit = MSB)
5925 // EOR.W R9, R1, R3
5926 bytes.extend_from_slice(&0xEA81u16.to_le_bytes());
5927 bytes.extend_from_slice(&0x0903u16.to_le_bytes());
5928
5929 // If dividend negative (R1 MSB set), negate it
5930 // TST R1, R1 (check sign)
5931 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
5932 // BPL skip_neg_dividend (+10 bytes = 5 halfwords)
5933 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5934
5935 // Negate R0:R1 (64-bit): RSBS R0, R0, #0; SBC R1, R1, R1 LSL #1
5936 // Actually: MVN R0, R0; MVN R1, R1; ADDS R0, R0, #1; ADC R1, R1, #0
5937 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
5938 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
5939 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
5940 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
5941 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
5942
5943 // If divisor negative (R3 MSB set), negate it
5944 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
5945 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
5946
5947 // Negate R2:R3
5948 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
5949 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
5950 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
5951 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
5952 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
5953
5954 // === Now do unsigned division (same as I64DivU) ===
5955 // Initialize quotient (R4:R5) = 0
5956 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
5957 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
5958 // Initialize remainder (R6:R7) = 0
5959 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
5960 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
5961 // Initialize loop counter R8 = 64
5962 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
5963 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
5964
5965 let loop_start = bytes.len();
5966
5967 // Shift quotient left
5968 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
5969 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
5970 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
5971 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
5972
5973 // Shift remainder left, OR in MSB of dividend
5974 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
5975 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
5976 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
5977 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
5978 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
5979 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
5980
5981 // Shift dividend left
5982 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
5983 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
5984 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
5985 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
5986
5987 // Compare and conditionally subtract
5988 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
5989 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
5990 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
5991 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
5992 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
5993
5994 // Subtract and set quotient bit
5995 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
5996 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
5997 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
5998 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
5999 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6000
6001 // Decrement and loop
6002 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6003 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6004
6005 let branch_offset_bytes = bytes.len() - loop_start + 4;
6006 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6007 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6008 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6009
6010 // Move quotient to R0:R1
6011 bytes.extend_from_slice(&0x4620u16.to_le_bytes()); // MOV R0, R4
6012 bytes.extend_from_slice(&0x4629u16.to_le_bytes()); // MOV R1, R5
6013
6014 // If result should be negative (R9 MSB set), negate R0:R1
6015 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9 (check MSB)
6016 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6017 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8 (skip negation)
6018
6019 // Negate result R0:R1
6020 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6021 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6022 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6023 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6024 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6025
6026 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6027 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6028 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6029
6030 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6031 Ok(bytes)
6032 }
6033
6034 // I64RemU: 64-bit unsigned remainder using binary long division
6035 // Same algorithm as I64DivU but returns remainder instead of quotient
6036 // Core: R0:R1 = dividend, R2:R3 = divisor -> R0:R1 = remainder
6037 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6038 ArmOp::I64RemU {
6039 rdlo,
6040 rdhi,
6041 rnlo,
6042 rnhi,
6043 rmlo,
6044 rmhi,
6045 elide_zero_guard,
6046 } => {
6047 let mut bytes = Vec::new();
6048 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6049 if !elide_zero_guard {
6050 emit_i64_divisor_zero_trap(&mut bytes);
6051 }
6052
6053 // PUSH {R4-R8} - save scratch registers (NO LR — inline code)
6054 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6055 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6056
6057 // Initialize quotient (R4:R5) = 0 (computed but not returned)
6058 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6059 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6060 // Initialize remainder (R6:R7) = 0
6061 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6062 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6063 // Initialize loop counter R8 = 64
6064 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6065 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6066
6067 let loop_start = bytes.len();
6068
6069 // Shift quotient left (not needed for result, but keeps algorithm same)
6070 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6071 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6072 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6073 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6074
6075 // Shift remainder left, OR in MSB of dividend
6076 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6077 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6078 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6079 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6080 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6081 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6082
6083 // Shift dividend left
6084 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6085 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6086 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6087 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6088
6089 // Compare and conditionally subtract
6090 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6091 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6092 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6093 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6094 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6095
6096 // Subtract and set quotient bit
6097 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6098 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6099 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6100 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6101 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6102
6103 // Decrement and loop
6104 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6105 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6106
6107 let branch_offset_bytes = bytes.len() - loop_start + 4;
6108 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6109 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6110 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6111
6112 // Move REMAINDER to R0:R1 (difference from I64DivU)
6113 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6114 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6115
6116 // POP {R4-R8} - restore scratch registers (NO PC — inline code continues)
6117 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6118 bytes.extend_from_slice(&0x01F0u16.to_le_bytes());
6119
6120 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6121 Ok(bytes)
6122 }
6123
6124 // I64RemS: 64-bit signed remainder
6125 // Remainder sign follows dividend sign (not quotient rule)
6126 // Core: R0:R1 = dividend (signed), R2:R3 = divisor (signed)
6127 // -> R0:R1 = remainder (signed, same sign as dividend)
6128 // #610: fixed-ABI wrapper + zero-divisor trap (see I64DivU).
6129 ArmOp::I64RemS {
6130 rdlo,
6131 rdhi,
6132 rnlo,
6133 rnhi,
6134 rmlo,
6135 rmhi,
6136 elide_zero_guard,
6137 } => {
6138 let mut bytes = Vec::new();
6139 emit_i64_fixed_abi_entry(&mut bytes, &[rnlo, rnhi, rmlo, rmhi]);
6140 if !elide_zero_guard {
6141 emit_i64_divisor_zero_trap(&mut bytes);
6142 }
6143
6144 // PUSH {R4-R11} - save scratch registers (NO LR — inline code)
6145 bytes.extend_from_slice(&0xE92Du16.to_le_bytes());
6146 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6147
6148 // Save dividend sign in R9 (remainder sign = dividend sign)
6149 // MOV R9, R1 (just need the sign bit)
6150 bytes.extend_from_slice(&0x4689u16.to_le_bytes()); // MOV R9, R1
6151
6152 // If dividend negative (R1 MSB set), negate it
6153 bytes.extend_from_slice(&0x4209u16.to_le_bytes()); // TST R1, R1
6154 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6155
6156 // Negate R0:R1
6157 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6158 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6159 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6160 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6161 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6162
6163 // If divisor negative (R3 MSB set), negate it
6164 bytes.extend_from_slice(&0x421Bu16.to_le_bytes()); // TST R3, R3
6165 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6166
6167 // Negate R2:R3
6168 bytes.extend_from_slice(&0x43D2u16.to_le_bytes()); // MVNS R2, R2
6169 bytes.extend_from_slice(&0x43DBu16.to_le_bytes()); // MVNS R3, R3
6170 bytes.extend_from_slice(&0x1C52u16.to_le_bytes()); // ADDS R2, R2, #1
6171 bytes.extend_from_slice(&0xF143u16.to_le_bytes()); // ADC.W R3, R3, #0
6172 bytes.extend_from_slice(&0x0300u16.to_le_bytes());
6173
6174 // === Unsigned division algorithm ===
6175 // Initialize quotient (R4:R5) = 0
6176 bytes.extend_from_slice(&0x2400u16.to_le_bytes());
6177 bytes.extend_from_slice(&0x2500u16.to_le_bytes());
6178 // Initialize remainder (R6:R7) = 0
6179 bytes.extend_from_slice(&0x2600u16.to_le_bytes());
6180 bytes.extend_from_slice(&0x2700u16.to_le_bytes());
6181 // Initialize loop counter R8 = 64
6182 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
6183 bytes.extend_from_slice(&0x0840u16.to_le_bytes());
6184
6185 let loop_start = bytes.len();
6186
6187 // Shift quotient left
6188 bytes.extend_from_slice(&0x006Du16.to_le_bytes()); // LSLS R5, R5, #1
6189 bytes.extend_from_slice(&0xEA45u16.to_le_bytes()); // ORR.W R5, R5, R4, LSR #31
6190 bytes.extend_from_slice(&0x75D4u16.to_le_bytes());
6191 bytes.extend_from_slice(&0x0064u16.to_le_bytes()); // LSLS R4, R4, #1
6192
6193 // Shift remainder left, OR in MSB of dividend
6194 bytes.extend_from_slice(&0x007Fu16.to_le_bytes()); // LSLS R7, R7, #1
6195 bytes.extend_from_slice(&0xEA47u16.to_le_bytes()); // ORR.W R7, R7, R6, LSR #31
6196 bytes.extend_from_slice(&0x77D6u16.to_le_bytes());
6197 bytes.extend_from_slice(&0x0076u16.to_le_bytes()); // LSLS R6, R6, #1
6198 bytes.extend_from_slice(&0xEA46u16.to_le_bytes()); // ORR.W R6, R6, R1, LSR #31
6199 bytes.extend_from_slice(&0x76D1u16.to_le_bytes());
6200
6201 // Shift dividend left
6202 bytes.extend_from_slice(&0x0049u16.to_le_bytes()); // LSLS R1, R1, #1
6203 bytes.extend_from_slice(&0xEA41u16.to_le_bytes()); // ORR.W R1, R1, R0, LSR #31
6204 bytes.extend_from_slice(&0x71D0u16.to_le_bytes());
6205 bytes.extend_from_slice(&0x0040u16.to_le_bytes()); // LSLS R0, R0, #1
6206
6207 // Compare and conditionally subtract
6208 bytes.extend_from_slice(&0x429Fu16.to_le_bytes()); // CMP R7, R3
6209 bytes.extend_from_slice(&0xD802u16.to_le_bytes()); // BHI +4
6210 bytes.extend_from_slice(&0xD306u16.to_le_bytes()); // BCC +12
6211 bytes.extend_from_slice(&0x4296u16.to_le_bytes()); // CMP R6, R2
6212 bytes.extend_from_slice(&0xD304u16.to_le_bytes()); // BCC +4 halfwords
6213
6214 // Subtract and set quotient bit
6215 bytes.extend_from_slice(&0x1AB6u16.to_le_bytes()); // SUBS R6, R6, R2
6216 bytes.extend_from_slice(&0xEB67u16.to_le_bytes()); // SBC.W R7, R7, R3
6217 bytes.extend_from_slice(&0x0703u16.to_le_bytes());
6218 bytes.extend_from_slice(&0xF044u16.to_le_bytes()); // ORR.W R4, R4, #1
6219 bytes.extend_from_slice(&0x0401u16.to_le_bytes());
6220
6221 // Decrement and loop
6222 bytes.extend_from_slice(&0xF1B8u16.to_le_bytes()); // SUB.W R8, R8, #1
6223 bytes.extend_from_slice(&0x0801u16.to_le_bytes());
6224
6225 let branch_offset_bytes = bytes.len() - loop_start + 4;
6226 let offset_halfwords = -((branch_offset_bytes / 2) as i16);
6227 let bne_encoding = 0xD100u16 | ((offset_halfwords as u16) & 0xFF);
6228 bytes.extend_from_slice(&bne_encoding.to_le_bytes());
6229
6230 // Move remainder to R0:R1
6231 bytes.extend_from_slice(&0x4630u16.to_le_bytes()); // MOV R0, R6
6232 bytes.extend_from_slice(&0x4639u16.to_le_bytes()); // MOV R1, R7
6233
6234 // If original dividend was negative (R9 MSB set), negate remainder
6235 bytes.extend_from_slice(&0xF1B9u16.to_le_bytes()); // TST.W R9, R9
6236 bytes.extend_from_slice(&0x0F00u16.to_le_bytes());
6237 bytes.extend_from_slice(&0xD504u16.to_le_bytes()); // BPL +8
6238
6239 // Negate result R0:R1
6240 bytes.extend_from_slice(&0x43C0u16.to_le_bytes()); // MVNS R0, R0
6241 bytes.extend_from_slice(&0x43C9u16.to_le_bytes()); // MVNS R1, R1
6242 bytes.extend_from_slice(&0x1C40u16.to_le_bytes()); // ADDS R0, R0, #1
6243 bytes.extend_from_slice(&0xF141u16.to_le_bytes()); // ADC.W R1, R1, #0
6244 bytes.extend_from_slice(&0x0100u16.to_le_bytes());
6245
6246 // POP {R4-R11} - restore scratch registers (NO PC — inline code continues)
6247 bytes.extend_from_slice(&0xE8BDu16.to_le_bytes());
6248 bytes.extend_from_slice(&0x0FF0u16.to_le_bytes());
6249
6250 emit_i64_fixed_abi_exit(&mut bytes, rdlo, rdhi)?;
6251 Ok(bytes)
6252 }
6253
6254 // === F32 VFP single-precision Thumb-2 encodings ===
6255 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6256 ArmOp::F32Add { sd, sn, sm } => {
6257 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A00, sd, sn, sm)?))
6258 }
6259 ArmOp::F32Sub { sd, sn, sm } => {
6260 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE300A40, sd, sn, sm)?))
6261 }
6262 ArmOp::F32Mul { sd, sn, sm } => {
6263 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE200A00, sd, sn, sm)?))
6264 }
6265 ArmOp::F32Div { sd, sn, sm } => {
6266 Ok(vfp_to_thumb_bytes(encode_vfp_3reg(0xEE800A00, sd, sn, sm)?))
6267 }
6268 ArmOp::F32Abs { sd, sm } => {
6269 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB00AC0, sd, sm)?))
6270 }
6271 ArmOp::F32Neg { sd, sm } => {
6272 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10A40, sd, sm)?))
6273 }
6274 ArmOp::F32Sqrt { sd, sm } => {
6275 Ok(vfp_to_thumb_bytes(encode_vfp_2reg(0xEEB10AC0, sd, sm)?))
6276 }
6277
6278 // f32 pseudo-ops — multi-instruction sequences
6279 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6280 ArmOp::F32Ceil { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b01),
6281 ArmOp::F32Floor { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b10),
6282 ArmOp::F32Trunc { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b11),
6283 ArmOp::F32Nearest { sd, sm } => self.encode_thumb_f32_rounding(sd, sm, 0b00),
6284 ArmOp::F32Min { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, true),
6285 ArmOp::F32Max { sd, sn, sm } => self.encode_thumb_f32_minmax(sd, sn, sm, false),
6286 ArmOp::F32Copysign { sd, sn, sm } => self.encode_thumb_f32_copysign(sd, sn, sm),
6287
6288 // f32 comparisons — VCMP + VMRS + MOV #0 + IT + MOV #1
6289 ArmOp::F32Eq { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x0),
6290 ArmOp::F32Ne { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x1),
6291 ArmOp::F32Lt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x4),
6292 ArmOp::F32Le { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0x9),
6293 ArmOp::F32Gt { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xC),
6294 ArmOp::F32Ge { rd, sn, sm } => self.encode_thumb_f32_compare(rd, sn, sm, 0xA),
6295
6296 ArmOp::F32Const { sd, value } => self.encode_thumb_f32_const(sd, *value),
6297
6298 ArmOp::F32Load { sd, addr } => {
6299 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED900A00, sd, addr)?))
6300 }
6301 ArmOp::F32Store { sd, addr } => {
6302 Ok(vfp_to_thumb_bytes(encode_vfp_ldst(0xED800A00, sd, addr)?))
6303 }
6304
6305 ArmOp::F32ConvertI32S { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, true),
6306 ArmOp::F32ConvertI32U { sd, rm } => self.encode_thumb_f32_convert_i32(sd, rm, false),
6307 ArmOp::F32ConvertI64S { .. } | ArmOp::F32ConvertI64U { .. } => {
6308 Err(synth_core::Error::synthesis(
6309 "F32 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6310 ))
6311 }
6312 ArmOp::F32ReinterpretI32 { sd, rm } => {
6313 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(true, sd, rm)?))
6314 }
6315 ArmOp::I32ReinterpretF32 { rd, sm } => {
6316 Ok(vfp_to_thumb_bytes(encode_vmov_core_sreg(false, sm, rd)?))
6317 }
6318 ArmOp::I32TruncF32S { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, true),
6319 ArmOp::I32TruncF32U { rd, sm } => self.encode_thumb_i32_trunc_f32(rd, sm, false),
6320
6321 // === F64 VFP double-precision Thumb-2 encodings ===
6322 // VFP instruction words are identical to ARM32; emit as two LE halfwords.
6323 ArmOp::F64Add { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6324 0xEE300B00, dd, dn, dm,
6325 )?)),
6326 ArmOp::F64Sub { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6327 0xEE300B40, dd, dn, dm,
6328 )?)),
6329 ArmOp::F64Mul { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6330 0xEE200B00, dd, dn, dm,
6331 )?)),
6332 ArmOp::F64Div { dd, dn, dm } => Ok(vfp_to_thumb_bytes(encode_vfp_3reg_f64(
6333 0xEE800B00, dd, dn, dm,
6334 )?)),
6335 ArmOp::F64Abs { dd, dm } => {
6336 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB00BC0, dd, dm)?))
6337 }
6338 ArmOp::F64Neg { dd, dm } => {
6339 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10B40, dd, dm)?))
6340 }
6341 ArmOp::F64Sqrt { dd, dm } => {
6342 Ok(vfp_to_thumb_bytes(encode_vfp_2reg_f64(0xEEB10BC0, dd, dm)?))
6343 }
6344
6345 // f64 pseudo-ops
6346 // FPSCR RMode: 00=nearest, 01=+inf(ceil), 10=-inf(floor), 11=zero(trunc)
6347 ArmOp::F64Ceil { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b01),
6348 ArmOp::F64Floor { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b10),
6349 ArmOp::F64Trunc { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b11),
6350 ArmOp::F64Nearest { dd, dm } => self.encode_thumb_f64_rounding(dd, dm, 0b00),
6351 ArmOp::F64Min { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, true),
6352 ArmOp::F64Max { dd, dn, dm } => self.encode_thumb_f64_minmax(dd, dn, dm, false),
6353 ArmOp::F64Copysign { dd, dn, dm } => self.encode_thumb_f64_copysign(dd, dn, dm),
6354
6355 // f64 comparisons
6356 ArmOp::F64Eq { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x0),
6357 ArmOp::F64Ne { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x1),
6358 ArmOp::F64Lt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x4),
6359 ArmOp::F64Le { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0x9),
6360 ArmOp::F64Gt { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xC),
6361 ArmOp::F64Ge { rd, dn, dm } => self.encode_thumb_f64_compare(rd, dn, dm, 0xA),
6362
6363 ArmOp::F64Const { dd, value } => self.encode_thumb_f64_const(dd, *value),
6364
6365 ArmOp::F64Load { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6366 0xED900B00, dd, addr,
6367 )?)),
6368 ArmOp::F64Store { dd, addr } => Ok(vfp_to_thumb_bytes(encode_vfp_ldst_f64(
6369 0xED800B00, dd, addr,
6370 )?)),
6371
6372 ArmOp::F64ConvertI32S { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, true),
6373 ArmOp::F64ConvertI32U { dd, rm } => self.encode_thumb_f64_convert_i32(dd, rm, false),
6374 ArmOp::F64ConvertI64S { .. } | ArmOp::F64ConvertI64U { .. } => {
6375 Err(synth_core::Error::synthesis(
6376 "F64 i64 conversion not supported (requires register pairs on 32-bit ARM)",
6377 ))
6378 }
6379 ArmOp::F64PromoteF32 { dd, sm } => self.encode_thumb_f64_promote_f32(dd, sm),
6380 ArmOp::F32DemoteF64 { sd, dm } => self.encode_thumb_f32_demote_f64(sd, dm),
6381 ArmOp::F64ReinterpretI64 { dd, rmlo, rmhi } => Ok(vfp_to_thumb_bytes(
6382 encode_vmov_core_dreg(true, dd, rmlo, rmhi)?,
6383 )),
6384 ArmOp::I64ReinterpretF64 { rdlo, rdhi, dm } => Ok(vfp_to_thumb_bytes(
6385 encode_vmov_core_dreg(false, dm, rdlo, rdhi)?,
6386 )),
6387 ArmOp::I64TruncF64S { .. } | ArmOp::I64TruncF64U { .. } => {
6388 Err(synth_core::Error::synthesis(
6389 "i64 truncation from F64 not supported (requires i64 register pairs on 32-bit ARM)",
6390 ))
6391 }
6392 ArmOp::I32TruncF64S { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, true),
6393 ArmOp::I32TruncF64U { rd, dm } => self.encode_thumb_i32_trunc_f64(rd, dm, false),
6394
6395 // ===== i64 operations: encode as multi-instruction Thumb-2 sequences =====
6396
6397 // I64Add: ADDS rdlo, rnlo, rmlo; ADC.W rdhi, rnhi, rmhi
6398 ArmOp::I64Add {
6399 rdlo,
6400 rdhi,
6401 rnlo,
6402 rnhi,
6403 rmlo,
6404 rmhi,
6405 } => {
6406 let mut bytes = Vec::new();
6407 // ADDS rdlo, rnlo, rmlo (16-bit)
6408 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adds {
6409 rd: *rdlo,
6410 rn: *rnlo,
6411 op2: Operand2::Reg(*rmlo),
6412 })?);
6413 // ADC.W rdhi, rnhi, rmhi (32-bit)
6414 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Adc {
6415 rd: *rdhi,
6416 rn: *rnhi,
6417 op2: Operand2::Reg(*rmhi),
6418 })?);
6419 Ok(bytes)
6420 }
6421
6422 // I64Sub: SUBS rdlo, rnlo, rmlo; SBC.W rdhi, rnhi, rmhi
6423 ArmOp::I64Sub {
6424 rdlo,
6425 rdhi,
6426 rnlo,
6427 rnhi,
6428 rmlo,
6429 rmhi,
6430 } => {
6431 let mut bytes = Vec::new();
6432 // SUBS rdlo, rnlo, rmlo (16-bit)
6433 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Subs {
6434 rd: *rdlo,
6435 rn: *rnlo,
6436 op2: Operand2::Reg(*rmlo),
6437 })?);
6438 // SBC.W rdhi, rnhi, rmhi (32-bit)
6439 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Sbc {
6440 rd: *rdhi,
6441 rn: *rnhi,
6442 op2: Operand2::Reg(*rmhi),
6443 })?);
6444 Ok(bytes)
6445 }
6446
6447 // I64And: AND rdlo, rnlo, rmlo; AND rdhi, rnhi, rmhi
6448 ArmOp::I64And {
6449 rdlo,
6450 rdhi,
6451 rnlo,
6452 rnhi,
6453 rmlo,
6454 rmhi,
6455 } => {
6456 let mut bytes = Vec::new();
6457 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6458 rd: *rdlo,
6459 rn: *rnlo,
6460 op2: Operand2::Reg(*rmlo),
6461 })?);
6462 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::And {
6463 rd: *rdhi,
6464 rn: *rnhi,
6465 op2: Operand2::Reg(*rmhi),
6466 })?);
6467 Ok(bytes)
6468 }
6469
6470 // I64Or: ORR rdlo, rnlo, rmlo; ORR rdhi, rnhi, rmhi
6471 ArmOp::I64Or {
6472 rdlo,
6473 rdhi,
6474 rnlo,
6475 rnhi,
6476 rmlo,
6477 rmhi,
6478 } => {
6479 let mut bytes = Vec::new();
6480 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6481 rd: *rdlo,
6482 rn: *rnlo,
6483 op2: Operand2::Reg(*rmlo),
6484 })?);
6485 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Orr {
6486 rd: *rdhi,
6487 rn: *rnhi,
6488 op2: Operand2::Reg(*rmhi),
6489 })?);
6490 Ok(bytes)
6491 }
6492
6493 // I64Xor: EOR rdlo, rnlo, rmlo; EOR rdhi, rnhi, rmhi
6494 ArmOp::I64Xor {
6495 rdlo,
6496 rdhi,
6497 rnlo,
6498 rnhi,
6499 rmlo,
6500 rmhi,
6501 } => {
6502 let mut bytes = Vec::new();
6503 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6504 rd: *rdlo,
6505 rn: *rnlo,
6506 op2: Operand2::Reg(*rmlo),
6507 })?);
6508 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Eor {
6509 rd: *rdhi,
6510 rn: *rnhi,
6511 op2: Operand2::Reg(*rmhi),
6512 })?);
6513 Ok(bytes)
6514 }
6515
6516 // I64Eqz: ORR scratch, lo, hi; ITE EQ; MOV rd, #1; MOV rd, #0
6517 ArmOp::I64Eqz { rd, rnlo, rnhi } => self.encode_thumb(&ArmOp::I64SetCondZ {
6518 rd: *rd,
6519 rn_lo: *rnlo,
6520 rn_hi: *rnhi,
6521 }),
6522
6523 // I64 comparisons: delegate to I64SetCond
6524 ArmOp::I64Eq {
6525 rd,
6526 rnlo,
6527 rnhi,
6528 rmlo,
6529 rmhi,
6530 } => self.encode_thumb(&ArmOp::I64SetCond {
6531 rd: *rd,
6532 rn_lo: *rnlo,
6533 rn_hi: *rnhi,
6534 rm_lo: *rmlo,
6535 rm_hi: *rmhi,
6536 cond: synth_synthesis::Condition::EQ,
6537 }),
6538
6539 ArmOp::I64Ne {
6540 rd,
6541 rnlo,
6542 rnhi,
6543 rmlo,
6544 rmhi,
6545 } => self.encode_thumb(&ArmOp::I64SetCond {
6546 rd: *rd,
6547 rn_lo: *rnlo,
6548 rn_hi: *rnhi,
6549 rm_lo: *rmlo,
6550 rm_hi: *rmhi,
6551 cond: synth_synthesis::Condition::NE,
6552 }),
6553
6554 ArmOp::I64LtS {
6555 rd,
6556 rnlo,
6557 rnhi,
6558 rmlo,
6559 rmhi,
6560 } => self.encode_thumb(&ArmOp::I64SetCond {
6561 rd: *rd,
6562 rn_lo: *rnlo,
6563 rn_hi: *rnhi,
6564 rm_lo: *rmlo,
6565 rm_hi: *rmhi,
6566 cond: synth_synthesis::Condition::LT,
6567 }),
6568
6569 ArmOp::I64LtU {
6570 rd,
6571 rnlo,
6572 rnhi,
6573 rmlo,
6574 rmhi,
6575 } => self.encode_thumb(&ArmOp::I64SetCond {
6576 rd: *rd,
6577 rn_lo: *rnlo,
6578 rn_hi: *rnhi,
6579 rm_lo: *rmlo,
6580 rm_hi: *rmhi,
6581 cond: synth_synthesis::Condition::LO,
6582 }),
6583
6584 ArmOp::I64LeS {
6585 rd,
6586 rnlo,
6587 rnhi,
6588 rmlo,
6589 rmhi,
6590 } => self.encode_thumb(&ArmOp::I64SetCond {
6591 rd: *rd,
6592 rn_lo: *rnlo,
6593 rn_hi: *rnhi,
6594 rm_lo: *rmlo,
6595 rm_hi: *rmhi,
6596 cond: synth_synthesis::Condition::LE,
6597 }),
6598
6599 ArmOp::I64LeU {
6600 rd,
6601 rnlo,
6602 rnhi,
6603 rmlo,
6604 rmhi,
6605 } => self.encode_thumb(&ArmOp::I64SetCond {
6606 rd: *rd,
6607 rn_lo: *rnlo,
6608 rn_hi: *rnhi,
6609 rm_lo: *rmlo,
6610 rm_hi: *rmhi,
6611 cond: synth_synthesis::Condition::LS,
6612 }),
6613
6614 ArmOp::I64GtS {
6615 rd,
6616 rnlo,
6617 rnhi,
6618 rmlo,
6619 rmhi,
6620 } => self.encode_thumb(&ArmOp::I64SetCond {
6621 rd: *rd,
6622 rn_lo: *rnlo,
6623 rn_hi: *rnhi,
6624 rm_lo: *rmlo,
6625 rm_hi: *rmhi,
6626 cond: synth_synthesis::Condition::GT,
6627 }),
6628
6629 ArmOp::I64GtU {
6630 rd,
6631 rnlo,
6632 rnhi,
6633 rmlo,
6634 rmhi,
6635 } => self.encode_thumb(&ArmOp::I64SetCond {
6636 rd: *rd,
6637 rn_lo: *rnlo,
6638 rn_hi: *rnhi,
6639 rm_lo: *rmlo,
6640 rm_hi: *rmhi,
6641 cond: synth_synthesis::Condition::HI,
6642 }),
6643
6644 ArmOp::I64GeS {
6645 rd,
6646 rnlo,
6647 rnhi,
6648 rmlo,
6649 rmhi,
6650 } => self.encode_thumb(&ArmOp::I64SetCond {
6651 rd: *rd,
6652 rn_lo: *rnlo,
6653 rn_hi: *rnhi,
6654 rm_lo: *rmlo,
6655 rm_hi: *rmhi,
6656 cond: synth_synthesis::Condition::GE,
6657 }),
6658
6659 ArmOp::I64GeU {
6660 rd,
6661 rnlo,
6662 rnhi,
6663 rmlo,
6664 rmhi,
6665 } => self.encode_thumb(&ArmOp::I64SetCond {
6666 rd: *rd,
6667 rn_lo: *rnlo,
6668 rn_hi: *rnhi,
6669 rm_lo: *rmlo,
6670 rm_hi: *rmhi,
6671 cond: synth_synthesis::Condition::HS,
6672 }),
6673
6674 // I64Const: MOVW rdlo, lo16; MOVT rdlo, hi16; MOVW rdhi, lo16_hi; MOVT rdhi, hi16_hi
6675 ArmOp::I64Const { rdlo, rdhi, value } => {
6676 let lo32 = *value as u32;
6677 let hi32 = (*value >> 32) as u32;
6678 let mut bytes = Vec::new();
6679 // Load low 32 bits into rdlo
6680 bytes.extend_from_slice(
6681 &self.encode_thumb32_movw_raw(reg_to_bits(rdlo), lo32 & 0xFFFF)?,
6682 );
6683 if lo32 > 0xFFFF {
6684 bytes.extend_from_slice(
6685 &self.encode_thumb32_movt_raw(reg_to_bits(rdlo), lo32 >> 16)?,
6686 );
6687 }
6688 // Load high 32 bits into rdhi
6689 bytes.extend_from_slice(
6690 &self.encode_thumb32_movw_raw(reg_to_bits(rdhi), hi32 & 0xFFFF)?,
6691 );
6692 if hi32 > 0xFFFF {
6693 bytes.extend_from_slice(
6694 &self.encode_thumb32_movt_raw(reg_to_bits(rdhi), hi32 >> 16)?,
6695 );
6696 }
6697 Ok(bytes)
6698 }
6699
6700 // I64Ldr: LDR rdlo, [base, offset]; LDR rdhi, [base, offset+4]
6701 ArmOp::I64Ldr { rdlo, rdhi, addr } => {
6702 let mut bytes = Vec::new();
6703 // #372/#382: a memory `i64.load` carries an index register
6704 // (`reg_imm(R11, addr_reg, offset)` = R11 + addr + offset). The
6705 // immediate `encode_thumb32_ldr` below uses only base+offset and
6706 // would SILENTLY DROP `offset_reg` — the #206 defect, here for
6707 // i64. `i64_effective_base` materializes the effective base into
6708 // `ip` (and, when `offset+4 > 0xFFF`, folds the offset in too so
6709 // the function is NOT skipped — #382), returning the residual
6710 // imm12 for the two halves. Frame i64 loads (no `offset_reg`, e.g.
6711 // a spilled local at `[SP, #off]`) keep the plain `[base,#off]`
6712 // form unchanged — so existing output is byte-identical.
6713 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6714 bytes.extend_from_slice(&self.encode_thumb32_ldr(rdlo, &base, offset)?);
6715 bytes.extend_from_slice(&self.encode_thumb32_ldr(
6716 rdhi,
6717 &base,
6718 offset.wrapping_add(4),
6719 )?);
6720 Ok(bytes)
6721 }
6722
6723 // I64Str: STR rdlo, [base, offset]; STR rdhi, [base, offset+4]
6724 ArmOp::I64Str { rdlo, rdhi, addr } => {
6725 let mut bytes = Vec::new();
6726 // #372/#382: same index-materialization + large-offset fold as
6727 // I64Ldr (see above).
6728 let (base, offset) = self.i64_effective_base(&mut bytes, addr)?;
6729 bytes.extend_from_slice(&self.encode_thumb32_str(rdlo, &base, offset)?);
6730 bytes.extend_from_slice(&self.encode_thumb32_str(
6731 rdhi,
6732 &base,
6733 offset.wrapping_add(4),
6734 )?);
6735 Ok(bytes)
6736 }
6737
6738 // I64ExtendI32S: MOV rdlo, rn; ASR rdhi, rdlo, #31 (sign-extend)
6739 ArmOp::I64ExtendI32S { rdlo, rdhi, rn } => {
6740 let mut bytes = Vec::new();
6741 if rdlo != rn {
6742 // MOV rdlo, rn (16-bit)
6743 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6744 rd: *rdlo,
6745 op2: Operand2::Reg(*rn),
6746 })?);
6747 }
6748 // ASR rdhi, rdlo, #31 (sign-extend: fill high word with sign bit)
6749 bytes.extend_from_slice(
6750 &self.encode_thumb32_shift(rdhi, rdlo, 31, 0b10)?, // ASR type
6751 );
6752 Ok(bytes)
6753 }
6754
6755 // I64ExtendI32U: MOV rdlo, rn; MOV rdhi, #0
6756 ArmOp::I64ExtendI32U { rdlo, rdhi, rn } => {
6757 let mut bytes = Vec::new();
6758 if rdlo != rn {
6759 // MOV rdlo, rn
6760 bytes.extend_from_slice(&self.encode_thumb(&ArmOp::Mov {
6761 rd: *rdlo,
6762 op2: Operand2::Reg(*rn),
6763 })?);
6764 }
6765 // MOV rdhi, #0 (#916: MOV.W for rdhi >= R8). Unconditional
6766 // site with no branches in the expansion — before the fix this
6767 // emitted the literal two-instruction stream [4608, 2800], half
6768 // of which was `CMP r0,#0` rather than the high-word clear, so
6769 // every i64.extend_i32_u into a high pair leaked stale bits.
6770 emit_thumb_zero_fill(&mut bytes, reg_to_bits(rdhi));
6771 Ok(bytes)
6772 }
6773
6774 // I32WrapI64: MOV rd, rnlo (just take low 32 bits)
6775 ArmOp::I32WrapI64 { rd, rnlo } => {
6776 if rd == rnlo {
6777 // No-op: already in the right register
6778 let instr: u16 = 0xBF00; // NOP
6779 Ok(instr.to_le_bytes().to_vec())
6780 } else {
6781 // MOV rd, rnlo
6782 self.encode_thumb(&ArmOp::Mov {
6783 rd: *rd,
6784 op2: Operand2::Reg(*rnlo),
6785 })
6786 }
6787 }
6788
6789 // ===== Helium MVE operations (Thumb-2 encoding) =====
6790 ArmOp::MveLoad { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vldrw(qd, addr))),
6791 ArmOp::MveStore { qd, addr } => Ok(vfp_to_thumb_bytes(encode_mve_vstrw(qd, addr))),
6792 ArmOp::MveConst { qd, bytes } => self.encode_thumb_mve_const(qd, bytes),
6793 ArmOp::MveAnd { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6794 0xEF000150, qd, qn, qm,
6795 ))),
6796 ArmOp::MveOrr { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6797 0xEF200150, qd, qn, qm,
6798 ))),
6799 ArmOp::MveEor { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6800 0xFF000150, qd, qn, qm,
6801 ))),
6802 ArmOp::MveMvn { qd, qm } => {
6803 // VMVN Qd, Qm: 0xFFB005C0 | Qd<<12 | Qm
6804 let qd_enc = qreg_to_num(qd);
6805 let qm_enc = qreg_to_num(qm);
6806 let instr: u32 = 0xFFB005C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6807 Ok(vfp_to_thumb_bytes(instr))
6808 }
6809 ArmOp::MveBic { qd, qn, qm } => Ok(vfp_to_thumb_bytes(encode_mve_3reg_bitwise(
6810 0xEF100150, qd, qn, qm,
6811 ))),
6812 ArmOp::MveAddI { qd, qn, qm, size } => {
6813 let sz = mve_size_bits(size);
6814 let base: u32 = 0xEF000840 | (sz << 20);
6815 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6816 }
6817 ArmOp::MveSubI { qd, qn, qm, size } => {
6818 let sz = mve_size_bits(size);
6819 let base: u32 = 0xFF000840 | (sz << 20);
6820 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6821 }
6822 ArmOp::MveMulI { qd, qn, qm, size } => {
6823 let sz = mve_size_bits(size);
6824 let base: u32 = 0xEF000950 | (sz << 20);
6825 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6826 }
6827 ArmOp::MveNegI { qd, qm, size } => {
6828 let sz = mve_size_bits(size);
6829 // VNEG.Sx Qd, Qm
6830 let qd_enc = qreg_to_num(qd);
6831 let qm_enc = qreg_to_num(qm);
6832 let base: u32 = 0xFFB103C0 | (sz << 18);
6833 let instr = base | ((qd_enc * 2) << 12) | (qm_enc * 2);
6834 Ok(vfp_to_thumb_bytes(instr))
6835 }
6836 ArmOp::MveDup { qd, rn, size } => {
6837 let sz = mve_size_bits(size);
6838 let qd_enc = qreg_to_num(qd);
6839 let rn_bits = reg_to_bits(rn);
6840 // VDUP.sz Qd, Rn: EEA0 0B10 variant
6841 // size encoding: 00=32, 01=16, 10=8
6842 let be = match sz {
6843 0 => 0b00u32, // 8-bit
6844 1 => 0b01, // 16-bit
6845 _ => 0b00, // 32-bit (default)
6846 };
6847 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12) | (be << 5);
6848 Ok(vfp_to_thumb_bytes(instr))
6849 }
6850 ArmOp::MveExtractLane { rd, qn, lane, size } => {
6851 let qn_enc = qreg_to_num(qn);
6852 let rd_bits = reg_to_bits(rd);
6853 // VMOV.sz Rd, Dn[x] — extract from Q-register lane
6854 // For 32-bit: VMOV Rd, Dn — where Dn is the appropriate D-register
6855 let d_reg = qn_enc * 2 + ((*lane as u32) >> 1);
6856 let lane_in_d = (*lane as u32) & 1;
6857 let _sz = mve_size_bits(size);
6858 // VMOV Rd, Dn[x]: EE10 0B10 for 32-bit
6859 let instr: u32 = 0xEE100B10 | (d_reg << 16) | (rd_bits << 12) | (lane_in_d << 21);
6860 Ok(vfp_to_thumb_bytes(instr))
6861 }
6862 ArmOp::MveInsertLane { qd, rn, lane, size } => {
6863 let qd_enc = qreg_to_num(qd);
6864 let rn_bits = reg_to_bits(rn);
6865 let d_reg = qd_enc * 2 + ((*lane as u32) >> 1);
6866 let lane_in_d = (*lane as u32) & 1;
6867 let _sz = mve_size_bits(size);
6868 // VMOV Dn[x], Rn: EE00 0B10 for 32-bit
6869 let instr: u32 = 0xEE000B10 | (d_reg << 16) | (rn_bits << 12) | (lane_in_d << 21);
6870 Ok(vfp_to_thumb_bytes(instr))
6871 }
6872
6873 // MVE float comparisons — emit VCMP + VPSEL sequence (simplified: just VCMP)
6874 ArmOp::MveCmpEqI { qd, qn, qm, size }
6875 | ArmOp::MveCmpNeI { qd, qn, qm, size }
6876 | ArmOp::MveCmpLtS { qd, qn, qm, size }
6877 | ArmOp::MveCmpLtU { qd, qn, qm, size }
6878 | ArmOp::MveCmpGtS { qd, qn, qm, size }
6879 | ArmOp::MveCmpGtU { qd, qn, qm, size }
6880 | ArmOp::MveCmpLeS { qd, qn, qm, size }
6881 | ArmOp::MveCmpLeU { qd, qn, qm, size }
6882 | ArmOp::MveCmpGeS { qd, qn, qm, size }
6883 | ArmOp::MveCmpGeU { qd, qn, qm, size } => {
6884 // Encode as VADD (placeholder encoding — real implementation
6885 // would use VCMP + VPSEL pair)
6886 let sz = mve_size_bits(size);
6887 let base: u32 = 0xEF000840 | (sz << 20);
6888 Ok(vfp_to_thumb_bytes(encode_mve_3reg(base, qd, qn, qm)))
6889 }
6890
6891 // f32x4 MVE arithmetic
6892 ArmOp::MveAddF32 { qd, qn, qm } => {
6893 // VADD.F32 Qd, Qn, Qm (MVE): 0xEF000D40
6894 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6895 }
6896 ArmOp::MveSubF32 { qd, qn, qm } => {
6897 // VSUB.F32 Qd, Qn, Qm (MVE): 0xEF200D40
6898 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF200D40, qd, qn, qm)))
6899 }
6900 ArmOp::MveMulF32 { qd, qn, qm } => {
6901 // VMUL.F32 Qd, Qn, Qm (MVE): 0xFF000D50
6902 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xFF000D50, qd, qn, qm)))
6903 }
6904 ArmOp::MveNegF32 { qd, qm } => {
6905 let qd_enc = qreg_to_num(qd);
6906 let qm_enc = qreg_to_num(qm);
6907 // VNEG.F32 Qd, Qm: FFB907C0
6908 let instr: u32 = 0xFFB907C0 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6909 Ok(vfp_to_thumb_bytes(instr))
6910 }
6911 ArmOp::MveAbsF32 { qd, qm } => {
6912 let qd_enc = qreg_to_num(qd);
6913 let qm_enc = qreg_to_num(qm);
6914 // VABS.F32 Qd, Qm: FFB90740
6915 let instr: u32 = 0xFFB90740 | ((qd_enc * 2) << 12) | (qm_enc * 2);
6916 Ok(vfp_to_thumb_bytes(instr))
6917 }
6918 ArmOp::MveCmpEqF32 { qd, qn, qm }
6919 | ArmOp::MveCmpNeF32 { qd, qn, qm }
6920 | ArmOp::MveCmpLtF32 { qd, qn, qm }
6921 | ArmOp::MveCmpLeF32 { qd, qn, qm }
6922 | ArmOp::MveCmpGtF32 { qd, qn, qm }
6923 | ArmOp::MveCmpGeF32 { qd, qn, qm } => {
6924 // Placeholder: encode as VADD.F32 (real impl needs VCMP.F32 + VPSEL)
6925 Ok(vfp_to_thumb_bytes(encode_mve_3reg(0xEF000D40, qd, qn, qm)))
6926 }
6927 ArmOp::MveDupF32 { qd, rn } => {
6928 let qd_enc = qreg_to_num(qd);
6929 let rn_bits = reg_to_bits(rn);
6930 // VDUP.32 Qd, Rn (same encoding as integer VDUP.32)
6931 let instr: u32 = 0xEEA00B10 | ((qd_enc * 2) << 16) | (rn_bits << 12);
6932 Ok(vfp_to_thumb_bytes(instr))
6933 }
6934 ArmOp::MveExtractLaneF32 { rd, qn, lane } => {
6935 let qn_enc = qreg_to_num(qn);
6936 let rd_bits = reg_to_bits(rd);
6937 // VMOV Rd, Sn where Sn = Q*4 + lane
6938 let s_num = qn_enc * 4 + (*lane as u32);
6939 let (vn, n) = encode_sreg(s_num);
6940 let instr: u32 = 0xEE100A10 | (vn << 16) | (rd_bits << 12) | (n << 7);
6941 Ok(vfp_to_thumb_bytes(instr))
6942 }
6943 ArmOp::MveReplaceLaneF32 { qd, rn, lane } => {
6944 let qd_enc = qreg_to_num(qd);
6945 let rn_bits = reg_to_bits(rn);
6946 // VMOV Sn, Rn where Sn = Q*4 + lane
6947 let s_num = qd_enc * 4 + (*lane as u32);
6948 let (vn, n) = encode_sreg(s_num);
6949 let instr: u32 = 0xEE000A10 | (vn << 16) | (rn_bits << 12) | (n << 7);
6950 Ok(vfp_to_thumb_bytes(instr))
6951 }
6952 ArmOp::MveDivF32 { qd, qn, qm } => {
6953 // Lane-wise: extract 4 S-regs, VDIV, insert back
6954 self.encode_thumb_mve_lane_wise_f32_binop(qd, qn, qm, 0xEE800A00)
6955 }
6956 ArmOp::MveSqrtF32 { qd, qm } => {
6957 // Lane-wise: extract 4 S-regs, VSQRT, insert back
6958 self.encode_thumb_mve_lane_wise_f32_sqrt(qd, qm)
6959 }
6960
6961 // Catch-all for any remaining ops
6962 _ => {
6963 let instr: u16 = 0xBF00; // NOP
6964 Ok(instr.to_le_bytes().to_vec())
6965 }
6966 }
6967 }
6968
6969 // === Thumb-2 VFP multi-instruction helpers ===
6970
6971 /// Encode F32 comparison as Thumb-2: VCMP.F32 + VMRS + MOVS rd,#0 + IT + MOV rd,#1
6972 fn encode_thumb_f32_compare(
6973 &self,
6974 rd: &Reg,
6975 sn: &VfpReg,
6976 sm: &VfpReg,
6977 cond_code: u32,
6978 ) -> Result<Vec<u8>> {
6979 let mut bytes = Vec::new();
6980 let rd_bits = reg_to_bits(rd);
6981
6982 // #709 (bug found under #708/#709): the `MOVS Rd,#0` below is a
6983 // FLAG-SETTING 16-bit move. Emitting it AFTER `VMRS APSR_nzcv, FPSCR`
6984 // (as the original code did) clobbered the N/Z/C/V flags the VMRS just
6985 // transferred from the VFP compare, so the following `IT<cond>` read
6986 // stale flags and every f32 comparison silently returned 0 (verified:
6987 // `flt(1.0,2.0)` → 0 on Cortex-M4F). The 619 harness never caught it
6988 // because it deliberately skipped compare EXECUTION on a false premise
6989 // (unicorn DOES model VMRS→APSR). Fix: materialize the `#0` FIRST, then
6990 // VCMP+VMRS set the flags the `IT` consumes. Instruction sizes are
6991 // unchanged (pure reorder), so the estimator↔encoder oracle (#511) is
6992 // untouched — only the byte ORDER differs.
6993
6994 // MOVS Rd, #0 (16-bit): 0010 0 Rd(3) 0000 0000 — its flag side effect
6995 // is immediately overwritten by the VMRS below.
6996 if rd_bits < 8 {
6997 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
6998 bytes.extend_from_slice(&movs_zero.to_le_bytes());
6999 } else {
7000 // MOV.W Rd, #0 (32-bit Thumb-2)
7001 let hw1: u16 = 0xF04F;
7002 let hw2: u16 = (rd_bits as u16) << 8;
7003 bytes.extend_from_slice(&hw1.to_le_bytes());
7004 bytes.extend_from_slice(&hw2.to_le_bytes());
7005 }
7006
7007 // VCMP.F32 Sn, Sm
7008 let sn_num = vfp_sreg_to_num(sn)?;
7009 let sm_num = vfp_sreg_to_num(sm)?;
7010 let (vd, d) = encode_sreg(sn_num);
7011 let (vm, m) = encode_sreg(sm_num);
7012 let vcmp = 0xEEB40A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7013 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7014
7015 // VMRS APSR_nzcv, FPSCR: 0xEEF1FA10 (sets the flags IT consumes)
7016 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7017
7018 // IT<cond> — If-Then for conditional MOV
7019 // IT encoding: 1011 1111 cond(4) mask(4)
7020 // mask = 0x8 for single "then" (IT)
7021 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7022 bytes.extend_from_slice(&it.to_le_bytes());
7023
7024 // MOV Rd, #1 (16-bit, conditional due to IT): 0010 0 Rd(3) 0000 0001
7025 if rd_bits < 8 {
7026 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7027 bytes.extend_from_slice(&mov_one.to_le_bytes());
7028 } else {
7029 // MOV.W Rd, #1 (32-bit)
7030 let hw1: u16 = 0xF04F;
7031 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7032 bytes.extend_from_slice(&hw1.to_le_bytes());
7033 bytes.extend_from_slice(&hw2.to_le_bytes());
7034 }
7035
7036 Ok(bytes)
7037 }
7038
7039 /// Encode F32 constant load as Thumb-2: MOVW + MOVT + VMOV
7040 fn encode_thumb_f32_const(&self, sd: &VfpReg, value: f32) -> Result<Vec<u8>> {
7041 let mut bytes = Vec::new();
7042 let bits = value.to_bits();
7043 let rt: u32 = 12; // R12/IP as temp
7044
7045 // MOVW R12, #lo16
7046 // Thumb-2 MOVW: 11110 i 10 0100 imm4 | 0 imm3 Rd imm8
7047 let lo16 = bits & 0xFFFF;
7048 let imm4 = (lo16 >> 12) & 0xF;
7049 let i_bit = (lo16 >> 11) & 1;
7050 let imm3 = (lo16 >> 8) & 0x7;
7051 let imm8 = lo16 & 0xFF;
7052 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7053 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7054 bytes.extend_from_slice(&hw1.to_le_bytes());
7055 bytes.extend_from_slice(&hw2.to_le_bytes());
7056
7057 // MOVT R12, #hi16
7058 let hi16 = (bits >> 16) & 0xFFFF;
7059 let imm4 = (hi16 >> 12) & 0xF;
7060 let i_bit = (hi16 >> 11) & 1;
7061 let imm3 = (hi16 >> 8) & 0x7;
7062 let imm8 = hi16 & 0xFF;
7063 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
7064 let hw2: u16 = ((imm3 << 12) | (rt << 8) | imm8) as u16;
7065 bytes.extend_from_slice(&hw1.to_le_bytes());
7066 bytes.extend_from_slice(&hw2.to_le_bytes());
7067
7068 // VMOV Sd, R12
7069 let vmov = encode_vmov_core_sreg(true, sd, &Reg::R12)?;
7070 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7071
7072 Ok(bytes)
7073 }
7074
7075 /// Encode VMOV + VCVT.F32.xS32 as Thumb-2
7076 fn encode_thumb_f32_convert_i32(&self, sd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7077 let mut bytes = Vec::new();
7078
7079 // VMOV Sd, Rm
7080 let vmov = encode_vmov_core_sreg(true, sd, rm)?;
7081 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7082
7083 // VCVT.F32.S32/U32 Sd, Sd. Bit 7 (op) = 1 for signed (S32), 0 for
7084 // unsigned (U32): signed = 0xEEB80AC0, unsigned = 0xEEB80A40
7085 // (GI-FPU-002: previously swapped — see the ARM32 twin).
7086 let sd_num = vfp_sreg_to_num(sd)?;
7087 let (vd, d) = encode_sreg(sd_num);
7088 let (vm, m) = encode_sreg(sd_num);
7089 let base = if signed { 0xEEB80AC0 } else { 0xEEB80A40 };
7090 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7091 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7092
7093 Ok(bytes)
7094 }
7095
7096 /// Encode F32 rounding pseudo-op as Thumb-2 via VCVT to integer and back
7097 /// Encode F32 rounding as Thumb-2.
7098 /// `mode`: FPSCR RMode — 0b00=nearest, 0b01=+inf(ceil), 0b10=-inf(floor), 0b11=zero(trunc)
7099 ///
7100 /// For trunc: uses VCVTR.S32.F32 (always truncates).
7101 /// For ceil/floor/nearest: sets FPSCR rounding mode, uses VCVT.S32.F32 (non-R variant),
7102 /// then restores FPSCR.
7103 fn encode_thumb_f32_rounding(&self, sd: &VfpReg, sm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7104 let mut bytes = Vec::new();
7105 let sm_num = vfp_sreg_to_num(sm)?;
7106 let sd_num = vfp_sreg_to_num(sd)?;
7107 let (vd_s, d_s) = encode_sreg(sd_num);
7108 let (vm_s, m_s) = encode_sreg(sm_num);
7109
7110 if mode == 0b11 {
7111 // Trunc (toward zero): VCVTR.S32.F32 — bit[7]=1, always truncates
7112 let vcvt_to_int = 0xEEBD0AC0 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7113 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7114 } else {
7115 // ceil/floor/nearest: manipulate FPSCR rounding mode
7116 let rt: u32 = 12; // R12/IP as temp
7117
7118 // VMRS R12, FPSCR
7119 let vmrs = 0xEEF10A10 | (rt << 12);
7120 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7121
7122 // BIC.W R12, R12, #(3 << 22) — clear RMode bits [23:22]
7123 // Thumb-2 modified immediate for 3<<22 = 0x00C00000:
7124 // BIC.W encoding: 11110 i 0 0001 S Rn | 0 imm3 Rd imm8
7125 // 0x00C00000 = 0x03 shifted left by 22 => Thumb mod-imm: i=0, imm3=0b101, imm8=0x03
7126 let bic_hw1: u16 = 0xF020 | ((rt as u16) & 0xF); // BIC, Rn=R12
7127 let bic_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | 0x03;
7128 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7129 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7130
7131 // ORR.W R12, R12, #(mode << 22)
7132 if mode != 0 {
7133 let orr_hw1: u16 = 0xF040 | ((rt as u16) & 0xF); // ORR, Rn=R12
7134 let orr_hw2: u16 = (0x05 << 12) | ((rt as u16) << 8) | (mode as u16);
7135 bytes.extend_from_slice(&orr_hw1.to_le_bytes());
7136 bytes.extend_from_slice(&orr_hw2.to_le_bytes());
7137 }
7138
7139 // VMSR FPSCR, R12
7140 let vmsr = 0xEEE10A10 | (rt << 12);
7141 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7142
7143 // VCVT.S32.F32 Sd, Sm — non-R variant (bit[7]=0), uses FPSCR rmode
7144 let vcvt_to_int = 0xEEBD0A40 | (d_s << 22) | (vd_s << 12) | (m_s << 5) | vm_s;
7145 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_int));
7146
7147 // Restore FPSCR: clear rmode bits back to nearest (default)
7148 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmrs));
7149 bytes.extend_from_slice(&bic_hw1.to_le_bytes());
7150 bytes.extend_from_slice(&bic_hw2.to_le_bytes());
7151 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmsr));
7152 }
7153
7154 // VCVT.F32.S32 Sd, Sd (convert integer result back to float)
7155 let (vd2, d2) = encode_sreg(sd_num);
7156 let vcvt_to_float = 0xEEB80A40 | (d2 << 22) | (vd2 << 12) | (d_s << 5) | vd_s;
7157 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt_to_float));
7158
7159 Ok(bytes)
7160 }
7161
7162 /// Encode F32 min/max as Thumb-2: VMOV + VCMP + VMRS + IT + VMOV
7163 fn encode_thumb_f32_minmax(
7164 &self,
7165 sd: &VfpReg,
7166 sn: &VfpReg,
7167 sm: &VfpReg,
7168 is_min: bool,
7169 ) -> Result<Vec<u8>> {
7170 let mut bytes = Vec::new();
7171 let sn_num = vfp_sreg_to_num(sn)?;
7172 let sm_num = vfp_sreg_to_num(sm)?;
7173 let sd_num = vfp_sreg_to_num(sd)?;
7174
7175 // VMOV.F32 Sd, Sn
7176 let (vd, d) = encode_sreg(sd_num);
7177 let (vn, n) = encode_sreg(sn_num);
7178 let vmov_sn = 0xEEB00A40 | (d << 22) | (vd << 12) | (n << 5) | vn;
7179 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sn));
7180
7181 // VCMP.F32 Sn, Sm
7182 let (vm, m) = encode_sreg(sm_num);
7183 let vcmp = 0xEEB40A40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7184 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7185
7186 // VMRS APSR_nzcv, FPSCR
7187 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7188
7189 // IT GT (for min) or IT MI (for max)
7190 let cond: u16 = if is_min { 0xC } else { 0x4 };
7191 let it: u16 = 0xBF00 | (cond << 4) | 0x8;
7192 bytes.extend_from_slice(&it.to_le_bytes());
7193
7194 // VMOV{cond}.F32 Sd, Sm — conditional VMOV in IT block
7195 let vmov_sm = 0xEEB00A40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7196 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov_sm));
7197
7198 Ok(bytes)
7199 }
7200
7201 /// Encode F32 copysign as Thumb-2
7202 /// Encode F32 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7203 /// encoder scratch, #212), the flags, and Sd:
7204 ///
7205 /// VMOV R12, Sm ; CMP R12, #0 (N flag = the sign bit)
7206 /// VABS.F32 Sd, Sn (magnitude, sign cleared)
7207 /// IT MI ; VNEG.F32(MI) Sd, Sd
7208 ///
7209 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7210 /// The R12 capture happens BEFORE Sd is written, so Sd aliasing Sn or Sm
7211 /// is safe. (The previous sequence staged the magnitude through R0 —
7212 /// clobbering a live allocator-owned value, the #615 class; caught while
7213 /// composing the F64 twin for #369.)
7214 fn encode_thumb_f32_copysign(&self, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7215 let mut bytes = Vec::new();
7216
7217 // VMOV R12, Sm (sign source bits)
7218 bytes.extend_from_slice(&vfp_to_thumb_bytes(encode_vmov_core_sreg(
7219 false,
7220 sm,
7221 &Reg::R12,
7222 )?));
7223 // CMP.W R12, #0 — N = bit31 (the sign, incl. -0.0 / -NaN).
7224 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7225 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7226 // VABS.F32 Sd, Sn
7227 let sd_num = vfp_sreg_to_num(sd)?;
7228 let sn_num = vfp_sreg_to_num(sn)?;
7229 let (vd, d) = encode_sreg(sd_num);
7230 let (vn, n) = encode_sreg(sn_num);
7231 let vabs = 0xEEB00AC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7232 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7233 // IT MI ; VNEG.F32(MI) Sd, Sd
7234 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7235 let vneg = 0xEEB10A40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7236 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7237
7238 Ok(bytes)
7239 }
7240
7241 /// Encode F64 comparison as Thumb-2: VCMP.F64 + VMRS + MOV #0 + IT + MOV #1
7242 fn encode_thumb_f64_compare(
7243 &self,
7244 rd: &Reg,
7245 dn: &VfpReg,
7246 dm: &VfpReg,
7247 cond_code: u32,
7248 ) -> Result<Vec<u8>> {
7249 let mut bytes = Vec::new();
7250 let rd_bits = reg_to_bits(rd);
7251
7252 // #712-class fix (found at f64-phase-2 wiring, #369): the 16-bit
7253 // `MOVS Rd,#0` is FLAG-SETTING. The original order emitted it AFTER
7254 // `VMRS APSR_nzcv, FPSCR`, clobbering the N/Z/C/V flags the VMRS just
7255 // transferred, so the following `IT<cond>` read stale flags and every
7256 // f64 comparison silently returned 0 — the exact bug the f32 compare
7257 // encoder shipped with and #712 fixed. Same fix: materialize the `#0`
7258 // FIRST (its flag side effect is overwritten by the VMRS), then
7259 // VCMP+VMRS set the flags the IT consumes. Pure reorder — sizes
7260 // unchanged.
7261
7262 // MOVS Rd, #0
7263 if rd_bits < 8 {
7264 let movs_zero: u16 = 0x2000 | ((rd_bits as u16) << 8);
7265 bytes.extend_from_slice(&movs_zero.to_le_bytes());
7266 } else {
7267 let hw1: u16 = 0xF04F;
7268 let hw2: u16 = (rd_bits as u16) << 8;
7269 bytes.extend_from_slice(&hw1.to_le_bytes());
7270 bytes.extend_from_slice(&hw2.to_le_bytes());
7271 }
7272
7273 // VCMP.F64 Dn, Dm
7274 let dn_num = vfp_dreg_to_num(dn)?;
7275 let dm_num = vfp_dreg_to_num(dm)?;
7276 let (vd, d) = encode_dreg(dn_num);
7277 let (vm, m) = encode_dreg(dm_num);
7278 let vcmp = 0xEEB40B40 | (d << 22) | (vd << 12) | (m << 5) | vm;
7279 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7280
7281 // VMRS APSR_nzcv, FPSCR (sets the flags the IT consumes)
7282 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7283
7284 // IT<cond>
7285 let it: u16 = 0xBF00 | ((cond_code as u16) << 4) | 0x8;
7286 bytes.extend_from_slice(&it.to_le_bytes());
7287
7288 // MOV Rd, #1
7289 if rd_bits < 8 {
7290 let mov_one: u16 = 0x2001 | ((rd_bits as u16) << 8);
7291 bytes.extend_from_slice(&mov_one.to_le_bytes());
7292 } else {
7293 let hw1: u16 = 0xF04F;
7294 let hw2: u16 = ((rd_bits as u16) << 8) | 0x01;
7295 bytes.extend_from_slice(&hw1.to_le_bytes());
7296 bytes.extend_from_slice(&hw2.to_le_bytes());
7297 }
7298
7299 Ok(bytes)
7300 }
7301
7302 /// Encode F64 constant load as Thumb-2: MOVW+MOVT (lo32 into R0) + MOVW+MOVT (hi32 into R12) + VMOV Dd, R0, R12
7303 fn encode_thumb_f64_const(&self, dd: &VfpReg, value: f64) -> Result<Vec<u8>> {
7304 let mut bytes = Vec::new();
7305 let bits = value.to_bits();
7306 let lo32 = bits as u32;
7307 let hi32 = (bits >> 32) as u32;
7308
7309 // MOVW R0, #lo16(lo32)
7310 let lo16 = lo32 & 0xFFFF;
7311 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(0, lo16)?);
7312
7313 // MOVT R0, #hi16(lo32)
7314 let hi16 = (lo32 >> 16) & 0xFFFF;
7315 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(0, hi16)?);
7316
7317 // MOVW R12, #lo16(hi32)
7318 let lo16 = hi32 & 0xFFFF;
7319 bytes.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
7320
7321 // MOVT R12, #hi16(hi32)
7322 let hi16 = (hi32 >> 16) & 0xFFFF;
7323 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
7324
7325 // VMOV Dd, R0, R12
7326 let vmov = encode_vmov_core_dreg(true, dd, &Reg::R0, &Reg::R12)?;
7327 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7328
7329 Ok(bytes)
7330 }
7331
7332 /// Encode VMOV Sd, Rm + VCVT.F64.S32/U32 Dd, Sd as Thumb-2
7333 /// Encode i32 → f64 conversion as Thumb-2. The integer stages through the
7334 /// DESTINATION's own low S-alias (`S(2d)`) — allocator-owned by
7335 /// definition — never S0 (which may hold a live value; the previous
7336 /// pseudo-op's S0 staging was the #615 class). Also fixes the SWAPPED
7337 /// signed/unsigned VCVT bases (bit7 = 1 is SIGNED — the same swap the f32
7338 /// twin had; latent here because f64.convert_i32_* was decode-dropped
7339 /// until #369): clang-verified vcvt.f64.s32 d1,s2 = eeb8 1bc1,
7340 /// vcvt.f64.u32 d1,s2 = eeb8 1b41.
7341 fn encode_thumb_f64_convert_i32(&self, dd: &VfpReg, rm: &Reg, signed: bool) -> Result<Vec<u8>> {
7342 let dd_num = vfp_dreg_to_num(dd)?;
7343 if dd_num > 7 {
7344 return Err(synth_core::Error::synthesis(format!(
7345 "F64ConvertI32: destination {dd:?} has no S-register alias \
7346 (D8..D15) — the selector allocates only D0..D7"
7347 )));
7348 }
7349 let mut bytes = Vec::new();
7350
7351 // VMOV S(2d), Rm — stage the integer in the destination's low word.
7352 let (vn_s, n_s) = encode_sreg(2 * dd_num);
7353 let rt = reg_to_bits(rm);
7354 let vmov = 0xEE000A10 | (vn_s << 16) | (rt << 12) | (n_s << 7);
7355 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7356
7357 // VCVT.F64.S32/U32 Dd, S(2d)
7358 let (vd, d) = encode_dreg(dd_num);
7359 let (vm, m) = encode_sreg(2 * dd_num);
7360 let base = if signed { 0xEEB80BC0 } else { 0xEEB80B40 };
7361 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7362 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7363
7364 Ok(bytes)
7365 }
7366
7367 /// Encode VCVT.F64.F32 Dd, Sm as Thumb-2
7368 fn encode_thumb_f64_promote_f32(&self, dd: &VfpReg, sm: &VfpReg) -> Result<Vec<u8>> {
7369 let dd_num = vfp_dreg_to_num(dd)?;
7370 let sm_num = vfp_sreg_to_num(sm)?;
7371 let (vd, d) = encode_dreg(dd_num);
7372 let (vm, m) = encode_sreg(sm_num);
7373
7374 let vcvt = 0xEEB70AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7375 Ok(vfp_to_thumb_bytes(vcvt))
7376 }
7377
7378 /// Encode VCVT.F32.F64 Sd, Dm (f32.demote_f64) as Thumb-2 — single
7379 /// instruction, round-to-nearest-even per FPSCR default, exactly WASM
7380 /// §4.3.3 demote (clang-verified: vcvt.f32.f64 s1,d2 = eef7 0bc2).
7381 fn encode_thumb_f32_demote_f64(&self, sd: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7382 let sd_num = vfp_sreg_to_num(sd)?;
7383 let dm_num = vfp_dreg_to_num(dm)?;
7384 let (vd, d) = encode_sreg(sd_num);
7385 let (vm, m) = encode_dreg(dm_num);
7386
7387 let vcvt = 0xEEB70BC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
7388 Ok(vfp_to_thumb_bytes(vcvt))
7389 }
7390
7391 /// Encode f64 → i32 truncation as Thumb-2 (round-toward-zero VCVT).
7392 ///
7393 /// The 32-bit result stages through the SOURCE's own low S-alias, `S(2m)`,
7394 /// clobbering half of an operand the selector has already popped. The
7395 /// overlapping write is well-defined: VCVT reads its source operand before
7396 /// writing (compilers emit `vcvt.f32.f64 s0, d0` routinely).
7397 ///
7398 /// # The one precondition, and who actually provides it
7399 ///
7400 /// `dm` must be a DEAD TEMP — never a pinned param/local home. That is the
7401 /// whole safety argument, and it is worth naming the guarantor precisely
7402 /// (#946): **`select_with_stack`** provides it, by copying a home into a
7403 /// fresh D-temp first. Visible in the shipped output for
7404 /// `(func (param f64) (result i32) (i32.trunc_f64_s (local.get 0)))`:
7405 ///
7406 /// ```text
7407 /// vmov r1, r2, d0 ; read the param out of its AAPCS-VFP home D0
7408 /// vmov d1, r1, r2 ; ...into a fresh D-temp
7409 /// vcvt.s32.f64 s2, d1 ; convert from the TEMP, staging into its own S2
7410 /// ```
7411 ///
7412 /// `InstructionSelector::select` / `select_default` do NOT provide it —
7413 /// `alloc_vfp_dreg` is a bare round-robin `(n + 1) % 16` with no liveness
7414 /// or home test. That path is not reachable from `synth compile`
7415 /// (`arm_backend.rs` calls `select_with_stack` exclusively; the only
7416 /// non-test caller of `select` is `examples/compile_add.rs`), so this is
7417 /// not a live miscompile — but a caller reaching that `pub` API directly
7418 /// gets no such guarantee.
7419 ///
7420 /// # What this deliberately does NOT claim
7421 ///
7422 /// An earlier version of this comment said the staging register is "never
7423 /// S0, which may hold an unrelated live value (the #615 class)". **That is
7424 /// false**, and measurably so: for
7425 /// `(func (result i32) (i32.trunc_f64_s (f64.const 3.7)))` the shipped
7426 /// compiler emits `vcvt.s32.f64 s0, d0`.
7427 ///
7428 /// It is also unnecessary. S0 is only dangerous as an *unrelated* scratch;
7429 /// here it is always the low half of `dm` itself, which the precondition
7430 /// above already makes dead. Naming a guard the code does not have — and
7431 /// does not need — invites a future reader to lean on it. The dead-temp
7432 /// precondition is the only thing holding this up.
7433 fn encode_thumb_i32_trunc_f64(&self, rd: &Reg, dm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7434 let dm_num = vfp_dreg_to_num(dm)?;
7435 if dm_num > 7 {
7436 return Err(synth_core::Error::synthesis(format!(
7437 "I32TruncF64: source {dm:?} has no S-register alias \
7438 (D8..D15) — the selector allocates only D0..D7"
7439 )));
7440 }
7441 let mut bytes = Vec::new();
7442
7443 // VCVT.S32/U32.F64 S(2m), Dm (clang-verified:
7444 // vcvt.s32.f64 s1,d2 = eefd 0bc2 ; vcvt.u32.f64 s1,d2 = eefc 0bc2)
7445 let (vm, m) = encode_dreg(dm_num);
7446 let (vd_s, d_s) = encode_sreg(2 * dm_num);
7447 let base = if signed { 0xEEBD0BC0 } else { 0xEEBC0BC0 };
7448 let vcvt = base | (d_s << 22) | (vd_s << 12) | (m << 5) | vm;
7449 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7450
7451 // VMOV Rd, S(2m)
7452 let rt = reg_to_bits(rd);
7453 let vmov = 0xEE100A10 | (vd_s << 16) | (rt << 12) | (d_s << 7);
7454 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7455
7456 Ok(bytes)
7457 }
7458
7459 /// Encode F64 rounding as a SINGLE Thumb-2 VRINT (FPv5 / cortex-m7dp).
7460 /// `mode` keeps the legacy FPSCR-RMode numbering of the callers —
7461 /// 0b00=nearest(ties-to-even)→VRINTN, 0b01=+inf(ceil)→VRINTP,
7462 /// 0b10=-inf(floor)→VRINTM, 0b11=zero(trunc)→VRINTZ — but the rounding
7463 /// mode is now ENCODED in the instruction, not smuggled through FPSCR.
7464 /// (The previous pseudo-op round-tripped through a 32-bit integer in S0:
7465 /// wrong for |x| >= 2^31, NaN/±inf collapsed to 0, -0.0 lost, and it
7466 /// CLOBBERED S0/R12 behind the allocator's back — the #615 class.)
7467 /// VRINT quietens an sNaN and preserves the sign of ±0.0/NaN per IEEE 754
7468 /// roundToIntegral, which is exactly WASM Core §4.3.3 f64.ceil/floor/
7469 /// trunc/nearest. VRINTN/P/M live in the FE "always-execute" space (never
7470 /// IT-conditional; none of these sequences emits them inside an IT block).
7471 fn encode_thumb_f64_rounding(&self, dd: &VfpReg, dm: &VfpReg, mode: u8) -> Result<Vec<u8>> {
7472 let dd_num = vfp_dreg_to_num(dd)?;
7473 let dm_num = vfp_dreg_to_num(dm)?;
7474 let (vd, d) = encode_dreg(dd_num);
7475 let (vm, m) = encode_dreg(dm_num);
7476 // clang-verified bases (thumbv7em, fpv5-d16):
7477 // vrintn.f64 d1,d2 = feb9 1b42 ; vrintp = feba 1b42
7478 // vrintm.f64 d1,d2 = febb 1b42 ; vrintz = eeb6 1bc2
7479 let base: u32 = match mode {
7480 0b00 => 0xFEB90B40, // VRINTN.F64 (round to nearest, ties to even)
7481 0b01 => 0xFEBA0B40, // VRINTP.F64 (round toward +inf)
7482 0b10 => 0xFEBB0B40, // VRINTM.F64 (round toward -inf)
7483 _ => 0xEEB60BC0, // VRINTZ.F64 (round toward zero)
7484 };
7485 Ok(vfp_to_thumb_bytes(
7486 base | (d << 22) | (vd << 12) | (m << 5) | vm,
7487 ))
7488 }
7489
7490 /// Encode F64 min/max as Thumb-2 with WASM Core §4.3.3 semantics:
7491 ///
7492 /// VCMP.F64 Dn, Dm ; VMRS APSR_nzcv, FPSCR
7493 /// VMINNM.F64/VMAXNM.F64 Dd, Dn, Dm (FPv5; -0.0 < +0.0 ordered)
7494 /// IT VS ; VADD.F64(VS) Dd, Dn, Dm (unordered ⇒ NaN-propagating)
7495 ///
7496 /// VMINNM/VMAXNM alone are IEEE minNum/maxNum, which return the NUMBER
7497 /// when exactly one operand is NaN — WASM requires NaN. The VS-guarded
7498 /// VADD overwrites the result with a quiet NaN whenever the compare was
7499 /// unordered (either operand NaN); on the ordered path VMINNM/VMAXNM
7500 /// order -0.0 below +0.0, matching WASM's min(+0,-0) = -0 / max = +0.
7501 /// Clobbers ONLY Dd and the flags (the previous pseudo-op's ordered IT
7502 /// GT/MI select returned the WRONG operand for NaN and ±0 mixes).
7503 ///
7504 /// Ok-or-Err: `dd` must not alias `dn`/`dm` — the VS fix-up reads them
7505 /// AFTER VMINNM wrote `dd` (the selector always allocates a fresh
7506 /// destination while both sources are still marked live).
7507 fn encode_thumb_f64_minmax(
7508 &self,
7509 dd: &VfpReg,
7510 dn: &VfpReg,
7511 dm: &VfpReg,
7512 is_min: bool,
7513 ) -> Result<Vec<u8>> {
7514 if dd == dn || dd == dm {
7515 return Err(synth_core::Error::synthesis(format!(
7516 "F64{}: destination {dd:?} aliases a source ({dn:?},{dm:?}) — \
7517 the unordered NaN fix-up would read a clobbered operand \
7518 (compiler bug: the selector must allocate a fresh D-temp)",
7519 if is_min { "Min" } else { "Max" },
7520 )));
7521 }
7522 let mut bytes = Vec::new();
7523 let dd_num = vfp_dreg_to_num(dd)?;
7524 let dn_num = vfp_dreg_to_num(dn)?;
7525 let dm_num = vfp_dreg_to_num(dm)?;
7526 let (vd, d) = encode_dreg(dd_num);
7527 let (vn, n) = encode_dreg(dn_num);
7528 let (vm, m) = encode_dreg(dm_num);
7529
7530 // VCMP.F64 Dn, Dm (clang-verified: vcmp.f64 d2,d3 = eeb4 2b43)
7531 let vcmp = 0xEEB40B40 | (n << 22) | (vn << 12) | (m << 5) | vm;
7532 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcmp));
7533 // VMRS APSR_nzcv, FPSCR
7534 bytes.extend_from_slice(&vfp_to_thumb_bytes(0xEEF1FA10));
7535 // VMINNM.F64 / VMAXNM.F64 Dd, Dn, Dm (clang-verified:
7536 // vminnm.f64 d1,d2,d3 = fe82 1b43 ; vmaxnm = fe82 1b03)
7537 let base: u32 = if is_min { 0xFE800B40 } else { 0xFE800B00 };
7538 let vnm = base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7539 bytes.extend_from_slice(&vfp_to_thumb_bytes(vnm));
7540 // IT VS (unordered ⇒ at least one NaN operand)
7541 bytes.extend_from_slice(&0xBF68_u16.to_le_bytes());
7542 // VADD.F64(VS) Dd, Dn, Dm — NaN + x propagates a quiet NaN
7543 let vadd = 0xEE300B00 | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
7544 bytes.extend_from_slice(&vfp_to_thumb_bytes(vadd));
7545
7546 Ok(bytes)
7547 }
7548
7549 /// Encode F64 copysign as Thumb-2, clobbering ONLY R12 (the reserved
7550 /// encoder scratch, #212), the flags, and Dd:
7551 ///
7552 /// VMOV R12, S(2m+1) (high word of the SIGN source Dm)
7553 /// CMP R12, #0 (N flag = the sign bit)
7554 /// VABS.F64 Dd, Dn (magnitude, sign cleared)
7555 /// IT MI ; VNEG.F64(MI) Dd, Dd
7556 ///
7557 /// Bit-exact on ±0.0/NaN-sign/±inf (VABS/VNEG are sign-bit-only edits).
7558 /// The R12 capture happens BEFORE Dd is written, so Dd aliasing Dn or Dm
7559 /// is safe. (The previous pseudo-op clobbered R0/R1/R2 behind the
7560 /// allocator's back — the #615 class.)
7561 fn encode_thumb_f64_copysign(&self, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<Vec<u8>> {
7562 let dm_num = vfp_dreg_to_num(dm)?;
7563 if dm_num > 7 {
7564 return Err(synth_core::Error::synthesis(format!(
7565 "F64Copysign: sign source {dm:?} has no S-register alias \
7566 (D8..D15) — the selector allocates only D0..D7"
7567 )));
7568 }
7569 let mut bytes = Vec::new();
7570 // VMOV R12, S(2m+1) — the sign source's high word.
7571 let (vn_s, n_s) = encode_sreg(2 * dm_num + 1);
7572 let vmov = 0xEE100A10 | (vn_s << 16) | (12 << 12) | (n_s << 7);
7573 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7574 // CMP R12, #0 (T2: CMP.W R12, #0) — N = bit31 of the sign word.
7575 bytes.extend_from_slice(&0xF1BC_u16.to_le_bytes());
7576 bytes.extend_from_slice(&0x0F00_u16.to_le_bytes());
7577 // VABS.F64 Dd, Dn
7578 let dd_num = vfp_dreg_to_num(dd)?;
7579 let dn_num = vfp_dreg_to_num(dn)?;
7580 let (vd, d) = encode_dreg(dd_num);
7581 let (vn, n) = encode_dreg(dn_num);
7582 let vabs = 0xEEB00BC0 | (d << 22) | (vd << 12) | (n << 5) | vn;
7583 bytes.extend_from_slice(&vfp_to_thumb_bytes(vabs));
7584 // IT MI ; VNEG.F64(MI) Dd, Dd
7585 bytes.extend_from_slice(&0xBF48_u16.to_le_bytes());
7586 let vneg = 0xEEB10B40 | (d << 22) | (vd << 12) | (d << 5) | vd;
7587 bytes.extend_from_slice(&vfp_to_thumb_bytes(vneg));
7588
7589 Ok(bytes)
7590 }
7591
7592 /// Encode VCVT.S32/U32.F32 + VMOV as Thumb-2
7593 fn encode_thumb_i32_trunc_f32(&self, rd: &Reg, sm: &VfpReg, signed: bool) -> Result<Vec<u8>> {
7594 let mut bytes = Vec::new();
7595
7596 let sm_num = vfp_sreg_to_num(sm)?;
7597 let (vd, d) = encode_sreg(sm_num);
7598 let (vm, m) = encode_sreg(sm_num);
7599 let base = if signed { 0xEEBD0AC0 } else { 0xEEBC0AC0 };
7600 let vcvt = base | (d << 22) | (vd << 12) | (m << 5) | vm;
7601 bytes.extend_from_slice(&vfp_to_thumb_bytes(vcvt));
7602
7603 // VMOV Rd, Sm
7604 let vmov = encode_vmov_core_sreg(false, sm, rd)?;
7605 bytes.extend_from_slice(&vfp_to_thumb_bytes(vmov));
7606
7607 Ok(bytes)
7608 }
7609
7610 // === Thumb-2 32-bit encoding helpers ===
7611
7612 /// Encode Thumb-2 32-bit ADD with immediate
7613 fn encode_thumb32_add(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7614 let rd_bits = reg_to_bits(rd);
7615 let rn_bits = reg_to_bits(rn);
7616
7617 // The `i:imm3:imm8` field is split the same way for both forms.
7618 let i_bit = (imm >> 11) & 1;
7619 let imm3 = (imm >> 8) & 0x7;
7620 let imm8 = imm & 0xFF;
7621
7622 let hw1_base = if imm <= 0xFF {
7623 // ADD.W (T3): the field is a ThumbExpandImm modified immediate. For
7624 // imm <= 0xFF (i:imm3 = 0000) it is the zero-extended byte, which is
7625 // correct — keep this form so existing encodings stay bit-identical.
7626 0xF100
7627 } else if imm <= 0xFFF {
7628 // ADDW (T4): a PLAIN 12-bit immediate (0..4095) — no ThumbExpandImm.
7629 // This is what makes `add sp, sp, #frame` correct for frame sizes
7630 // >= 256, which ADD.W (T3) would silently mis-encode (e.g. #256 -> #0).
7631 0xF200
7632 } else {
7633 return Err(synth_core::Error::synthesis(
7634 "ADD immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7635 ));
7636 };
7637
7638 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7639 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7640
7641 let mut bytes = hw1.to_le_bytes().to_vec();
7642 bytes.extend_from_slice(&hw2.to_le_bytes());
7643 Ok(bytes)
7644 }
7645
7646 /// Encode Thumb-2 32-bit SUB with immediate
7647 fn encode_thumb32_sub(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7648 let rd_bits = reg_to_bits(rd);
7649 let rn_bits = reg_to_bits(rn);
7650
7651 let i_bit = (imm >> 11) & 1;
7652 let imm3 = (imm >> 8) & 0x7;
7653 let imm8 = imm & 0xFF;
7654
7655 let hw1_base = if imm <= 0xFF {
7656 // SUB.W (T3) modified immediate — correct for the zero-extended byte
7657 // (imm <= 0xFF). Kept bit-identical for existing encodings.
7658 0xF1A0
7659 } else if imm <= 0xFFF {
7660 // SUBW (T4): plain 12-bit immediate (0..4095). Makes
7661 // `sub sp, sp, #frame` correct for frame sizes >= 256.
7662 0xF2A0
7663 } else {
7664 return Err(synth_core::Error::synthesis(
7665 "SUB immediate > 0xFFF (4095) requires a multi-instruction sequence (not supported)",
7666 ));
7667 };
7668
7669 let hw1: u16 = (hw1_base | (i_bit << 10) | rn_bits) as u16;
7670 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7671
7672 let mut bytes = hw1.to_le_bytes().to_vec();
7673 bytes.extend_from_slice(&hw2.to_le_bytes());
7674 Ok(bytes)
7675 }
7676
7677 /// Encode Thumb-2 32-bit ADDS with immediate (sets flags)
7678 fn encode_thumb32_adds(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7679 let rd_bits = reg_to_bits(rd);
7680 let rn_bits = reg_to_bits(rn);
7681
7682 // ADDS.W (flag-setting) has only the modified-immediate form — error on
7683 // an un-encodable value rather than silently add the wrong constant.
7684 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7685 synth_core::Error::synthesis(
7686 "ADDS immediate is not a valid ThumbExpandImm — materialize into a register",
7687 )
7688 })?;
7689 let i_bit = (field >> 11) & 1;
7690 let imm3 = (field >> 8) & 0x7;
7691 let imm8 = field & 0xFF;
7692
7693 // ADDS.W Rd, Rn, #imm (with S=1)
7694 // First halfword: 1111 0 i 0 1000 1 Rn = F110 | i<<10 | Rn
7695 let hw1: u16 = (0xF110 | (i_bit << 10) | rn_bits) as u16;
7696 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7697
7698 let mut bytes = hw1.to_le_bytes().to_vec();
7699 bytes.extend_from_slice(&hw2.to_le_bytes());
7700 Ok(bytes)
7701 }
7702
7703 /// Encode Thumb-2 32-bit SUBS with immediate (sets flags)
7704 fn encode_thumb32_subs(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7705 let rd_bits = reg_to_bits(rd);
7706 let rn_bits = reg_to_bits(rn);
7707
7708 // SUBS.W (flag-setting) has only the modified-immediate form — error on
7709 // an un-encodable value rather than silently subtract the wrong constant.
7710 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7711 synth_core::Error::synthesis(
7712 "SUBS immediate is not a valid ThumbExpandImm — materialize into a register",
7713 )
7714 })?;
7715 let i_bit = (field >> 11) & 1;
7716 let imm3 = (field >> 8) & 0x7;
7717 let imm8 = field & 0xFF;
7718
7719 // SUBS.W Rd, Rn, #imm (with S=1)
7720 // First halfword: 1111 0 i 0 1101 1 Rn = F1B0 | i<<10 | Rn
7721 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7722 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7723
7724 let mut bytes = hw1.to_le_bytes().to_vec();
7725 bytes.extend_from_slice(&hw2.to_le_bytes());
7726 Ok(bytes)
7727 }
7728
7729 /// Encode Thumb-2 32-bit MOVW (16-bit immediate)
7730 ///
7731 /// # Contract (Verus-style)
7732 /// ```text
7733 /// requires rd <= R14
7734 /// ensures result.len() == 4
7735 /// ensures (imm & 0xFFFF) can be reconstructed from the encoding
7736 /// ```
7737 fn encode_thumb32_movw(&self, rd: &Reg, imm: u32) -> Result<Vec<u8>> {
7738 let rd_bits = reg_to_bits(rd);
7739 reg_bits_checked(rd_bits)?;
7740 let imm16 = imm & 0xFFFF;
7741
7742 // MOVW Rd, #imm16
7743 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
7744 let imm4 = (imm16 >> 12) & 0xF;
7745 let i_bit = (imm16 >> 11) & 1;
7746 let imm3 = (imm16 >> 8) & 0x7;
7747 let imm8 = imm16 & 0xFF;
7748
7749 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
7750 let hw2: u16 = ((imm3 << 12) | (rd_bits << 8) | imm8) as u16;
7751
7752 let mut bytes = hw1.to_le_bytes().to_vec();
7753 bytes.extend_from_slice(&hw2.to_le_bytes());
7754 encoding_contracts::verify_thumb32(&bytes);
7755 Ok(bytes)
7756 }
7757
7758 /// Encode Thumb-2 32-bit shift with immediate
7759 ///
7760 /// # Contract (Verus-style)
7761 /// ```text
7762 /// requires rd <= R14, rm <= R14
7763 /// ensures result.len() == 4
7764 /// ```
7765 fn encode_thumb32_shift(
7766 &self,
7767 rd: &Reg,
7768 rm: &Reg,
7769 shift: u32,
7770 shift_type: u8,
7771 ) -> Result<Vec<u8>> {
7772 let rd_bits = reg_to_bits(rd);
7773 let rm_bits = reg_to_bits(rm);
7774 reg_bits_checked(rd_bits)?;
7775 reg_bits_checked(rm_bits)?;
7776 let imm5 = shift & 0x1F;
7777 let imm2 = imm5 & 0x3;
7778 let imm3 = (imm5 >> 2) & 0x7;
7779
7780 // MOV.W Rd, Rm, <shift> #imm
7781 // EA4F 0 imm3 Rd imm2 type Rm
7782 let hw1: u16 = 0xEA4F;
7783 let hw2: u16 =
7784 ((imm3 << 12) | (rd_bits << 8) | (imm2 << 6) | ((shift_type as u32) << 4) | rm_bits)
7785 as u16;
7786
7787 let mut bytes = hw1.to_le_bytes().to_vec();
7788 bytes.extend_from_slice(&hw2.to_le_bytes());
7789 Ok(bytes)
7790 }
7791
7792 /// Encode Thumb-2 32-bit shift by register
7793 /// Encoding: 11111010 0xx0 Rn | 1111 Rd 0000 Rm
7794 /// shift_type: 00=LSL, 01=LSR, 10=ASR, 11=ROR
7795 fn encode_thumb32_shift_reg(
7796 &self,
7797 rd: &Reg,
7798 rn: &Reg,
7799 rm: &Reg,
7800 shift_type: u8,
7801 ) -> Result<Vec<u8>> {
7802 let rd_bits = reg_to_bits(rd);
7803 let rn_bits = reg_to_bits(rn);
7804 let rm_bits = reg_to_bits(rm);
7805
7806 // hw1: 1111 1010 0xx0 Rn
7807 let hw1: u16 = (0xFA00 | ((shift_type as u32) << 5) | rn_bits) as u16;
7808 // hw2: 1111 Rd 0000 Rm
7809 let hw2: u16 = (0xF000 | (rd_bits << 8) | rm_bits) as u16;
7810
7811 let mut bytes = hw1.to_le_bytes().to_vec();
7812 bytes.extend_from_slice(&hw2.to_le_bytes());
7813 Ok(bytes)
7814 }
7815
7816 /// Encode Thumb-2 32-bit CMP with immediate
7817 fn encode_thumb32_cmp_imm(&self, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
7818 let rn_bits = reg_to_bits(rn);
7819
7820 // CMP.W has only the modified-immediate form (no plain-imm12 like ADDW),
7821 // so an un-encodable immediate MUST be materialized into a register by
7822 // the selector. Error rather than silently compare the wrong constant.
7823 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
7824 synth_core::Error::synthesis(
7825 "CMP immediate is not a valid ThumbExpandImm — materialize into a register",
7826 )
7827 })?;
7828 let i_bit = (field >> 11) & 1;
7829 let imm3 = (field >> 8) & 0x7;
7830 let imm8 = field & 0xFF;
7831
7832 // CMP.W Rn, #imm
7833 let hw1: u16 = (0xF1B0 | (i_bit << 10) | rn_bits) as u16;
7834 let hw2: u16 = ((imm3 << 12) | 0x0F00 | imm8) as u16;
7835
7836 let mut bytes = hw1.to_le_bytes().to_vec();
7837 bytes.extend_from_slice(&hw2.to_le_bytes());
7838 Ok(bytes)
7839 }
7840
7841 /// #372/#382: resolve the base register AND residual immediate offset for an
7842 /// `I64Ldr`/`I64Str` whose address may carry an index register. Returns
7843 /// `(base, low_offset)`; the caller accesses the halves at `[base,
7844 /// #low_offset]` and `[base, #low_offset + 4]`.
7845 ///
7846 /// - Frame access (no `offset_reg`, e.g. a spilled local at `[SP, #off]`):
7847 /// returns `(addr.base, off)` and emits NOTHING — byte-identical.
7848 /// - Memory access (`reg_imm(R11, addr, offset)` = `R11 + addr + offset`)
7849 /// with `offset + 4 <= 0xFFF`: emits `ADD.W ip, base, index` and returns
7850 /// `(ip, offset)`, folding `offset`/`offset+4` into the halves' imm12.
7851 /// Byte-identical to the pre-#382 (#372) behavior.
7852 /// - Memory access with `offset + 4 > 0xFFF`: the imm12 form cannot hold the
7853 /// high half's offset, so `encode_thumb32_ldr`'s `check_ldst_imm12` (#259)
7854 /// rightly refused it and the WHOLE function was skipped (#382). Instead
7855 /// MATERIALIZE the offset into the base: `ADD ip, index, #offset` (against
7856 /// the read-only INDEX register, so `encode_thumb32_add_imm` never trips its
7857 /// `rd==rn==R12` alias trap), then `ADD.W ip, ip, base` (+ R11), and return
7858 /// `(ip, 0)` so the halves use `[ip, #0]` / `[ip, #4]`.
7859 ///
7860 /// The effective address is fully materialized into `ip` BEFORE the halves
7861 /// are accessed, so an `rdlo` aliasing the index register is safe.
7862 fn i64_effective_base(&self, bytes: &mut Vec<u8>, addr: &MemAddr) -> Result<(Reg, u32)> {
7863 let offset = if addr.offset < 0 {
7864 0u32
7865 } else {
7866 addr.offset as u32
7867 };
7868 match addr.offset_reg {
7869 Some(idx) => {
7870 let ip = Reg::R12;
7871 if offset.wrapping_add(4) > 0xFFF {
7872 // Large static offset (#382): fold it (and R11) into ip so the
7873 // imm12 halves stay in range instead of skipping the function.
7874 // ADD ip, index, #offset (index != ip → no add_imm alias trap)
7875 bytes.extend_from_slice(&self.encode_thumb32_add_imm(&ip, &idx, offset)?);
7876 // ADD.W ip, ip, base (+ R11)
7877 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(
7878 reg_to_bits(&ip),
7879 reg_to_bits(&ip),
7880 reg_to_bits(&addr.base),
7881 )?);
7882 Ok((ip, 0))
7883 } else {
7884 // ADD.W ip, addr.base, idx (Thumb-2, byte-verified vs as)
7885 let hw1: u16 = 0xEB00 | reg_to_bits(&addr.base) as u16;
7886 let hw2: u16 = 0x0C00 | reg_to_bits(&idx) as u16;
7887 bytes.extend_from_slice(&hw1.to_le_bytes());
7888 bytes.extend_from_slice(&hw2.to_le_bytes());
7889 Ok((ip, offset))
7890 }
7891 }
7892 None => Ok((addr.base, offset)),
7893 }
7894 }
7895
7896 /// Encode Thumb-2 32-bit LDR
7897 fn encode_thumb32_ldr(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7898 let rd_bits = reg_to_bits(rd);
7899 let base_bits = reg_to_bits(base);
7900
7901 // LDR.W Rd, [Rn, #imm12]
7902 check_ldst_imm12(offset)?;
7903 let hw1: u16 = (0xF8D0 | base_bits) as u16;
7904 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7905
7906 let mut bytes = hw1.to_le_bytes().to_vec();
7907 bytes.extend_from_slice(&hw2.to_le_bytes());
7908 Ok(bytes)
7909 }
7910
7911 /// Encode Thumb-2 32-bit STR
7912 fn encode_thumb32_str(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7913 let rd_bits = reg_to_bits(rd);
7914 let base_bits = reg_to_bits(base);
7915
7916 // STR.W Rd, [Rn, #imm12]
7917 check_ldst_imm12(offset)?;
7918 let hw1: u16 = (0xF8C0 | base_bits) as u16;
7919 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7920
7921 let mut bytes = hw1.to_le_bytes().to_vec();
7922 bytes.extend_from_slice(&hw2.to_le_bytes());
7923 Ok(bytes)
7924 }
7925
7926 /// Encode Thumb-2 32-bit LDR with register offset: LDR.W Rd, [Rn, Rm]
7927 fn encode_thumb32_ldr_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7928 let rd_bits = reg_to_bits(rd);
7929 let base_bits = reg_to_bits(base);
7930 let rm_bits = reg_to_bits(offset_reg);
7931
7932 // LDR.W Rd, [Rn, Rm, LSL #0]
7933 // Encoding: 1111 1000 0101 Rn | Rt 0000 00 imm2 Rm
7934 // imm2 = 00 for no shift (LSL #0)
7935 let hw1: u16 = (0xF850 | base_bits) as u16;
7936 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7937
7938 let mut bytes = hw1.to_le_bytes().to_vec();
7939 bytes.extend_from_slice(&hw2.to_le_bytes());
7940 Ok(bytes)
7941 }
7942
7943 /// Encode Thumb-2 32-bit STR with register offset: STR.W Rd, [Rn, Rm]
7944 fn encode_thumb32_str_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7945 let rd_bits = reg_to_bits(rd);
7946 let base_bits = reg_to_bits(base);
7947 let rm_bits = reg_to_bits(offset_reg);
7948
7949 // STR.W Rd, [Rn, Rm, LSL #0]
7950 // Encoding: 1111 1000 0100 Rn | Rt 0000 00 imm2 Rm
7951 // imm2 = 00 for no shift (LSL #0)
7952 let hw1: u16 = (0xF840 | base_bits) as u16;
7953 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7954
7955 let mut bytes = hw1.to_le_bytes().to_vec();
7956 bytes.extend_from_slice(&hw2.to_le_bytes());
7957 Ok(bytes)
7958 }
7959
7960 // === Sub-word load/store Thumb-2 encoding helpers ===
7961
7962 /// Encode Thumb-2 32-bit LDRB with immediate: LDRB.W Rd, [Rn, #imm12]
7963 fn encode_thumb32_ldrb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7964 let rd_bits = reg_to_bits(rd);
7965 let base_bits = reg_to_bits(base);
7966 // LDRB.W Rd, [Rn, #imm12]: 1111 1000 1001 Rn | Rt imm12
7967 check_ldst_imm12(offset)?;
7968 let hw1: u16 = (0xF890 | base_bits) as u16;
7969 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7970 let mut bytes = hw1.to_le_bytes().to_vec();
7971 bytes.extend_from_slice(&hw2.to_le_bytes());
7972 Ok(bytes)
7973 }
7974
7975 /// Encode Thumb-2 32-bit LDRB with register: LDRB.W Rd, [Rn, Rm]
7976 fn encode_thumb32_ldrb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
7977 let rd_bits = reg_to_bits(rd);
7978 let base_bits = reg_to_bits(base);
7979 let rm_bits = reg_to_bits(offset_reg);
7980 // LDRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0001 Rn | Rt 0000 00 imm2 Rm
7981 let hw1: u16 = (0xF810 | base_bits) as u16;
7982 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
7983 let mut bytes = hw1.to_le_bytes().to_vec();
7984 bytes.extend_from_slice(&hw2.to_le_bytes());
7985 Ok(bytes)
7986 }
7987
7988 /// Encode Thumb-2 32-bit LDRSB with immediate: LDRSB.W Rd, [Rn, #imm12]
7989 fn encode_thumb32_ldrsb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
7990 let rd_bits = reg_to_bits(rd);
7991 let base_bits = reg_to_bits(base);
7992 // LDRSB.W Rd, [Rn, #imm12]: 1111 1001 1001 Rn | Rt imm12
7993 check_ldst_imm12(offset)?;
7994 let hw1: u16 = (0xF990 | base_bits) as u16;
7995 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
7996 let mut bytes = hw1.to_le_bytes().to_vec();
7997 bytes.extend_from_slice(&hw2.to_le_bytes());
7998 Ok(bytes)
7999 }
8000
8001 /// Encode Thumb-2 32-bit LDRSB with register: LDRSB.W Rd, [Rn, Rm]
8002 fn encode_thumb32_ldrsb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8003 let rd_bits = reg_to_bits(rd);
8004 let base_bits = reg_to_bits(base);
8005 let rm_bits = reg_to_bits(offset_reg);
8006 // LDRSB.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0001 Rn | Rt 0000 00 imm2 Rm
8007 let hw1: u16 = (0xF910 | base_bits) as u16;
8008 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8009 let mut bytes = hw1.to_le_bytes().to_vec();
8010 bytes.extend_from_slice(&hw2.to_le_bytes());
8011 Ok(bytes)
8012 }
8013
8014 /// Encode Thumb-2 32-bit LDRH with immediate: LDRH.W Rd, [Rn, #imm12]
8015 fn encode_thumb32_ldrh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8016 let rd_bits = reg_to_bits(rd);
8017 let base_bits = reg_to_bits(base);
8018 // LDRH.W Rd, [Rn, #imm12]: 1111 1000 1011 Rn | Rt imm12
8019 check_ldst_imm12(offset)?;
8020 let hw1: u16 = (0xF8B0 | base_bits) as u16;
8021 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8022 let mut bytes = hw1.to_le_bytes().to_vec();
8023 bytes.extend_from_slice(&hw2.to_le_bytes());
8024 Ok(bytes)
8025 }
8026
8027 /// Encode Thumb-2 32-bit LDRH with register: LDRH.W Rd, [Rn, Rm]
8028 fn encode_thumb32_ldrh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8029 let rd_bits = reg_to_bits(rd);
8030 let base_bits = reg_to_bits(base);
8031 let rm_bits = reg_to_bits(offset_reg);
8032 // LDRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0011 Rn | Rt 0000 00 imm2 Rm
8033 let hw1: u16 = (0xF830 | base_bits) as u16;
8034 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8035 let mut bytes = hw1.to_le_bytes().to_vec();
8036 bytes.extend_from_slice(&hw2.to_le_bytes());
8037 Ok(bytes)
8038 }
8039
8040 /// Encode Thumb-2 32-bit LDRSH with immediate: LDRSH.W Rd, [Rn, #imm12]
8041 fn encode_thumb32_ldrsh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8042 let rd_bits = reg_to_bits(rd);
8043 let base_bits = reg_to_bits(base);
8044 // LDRSH.W Rd, [Rn, #imm12]: 1111 1001 1011 Rn | Rt imm12
8045 check_ldst_imm12(offset)?;
8046 let hw1: u16 = (0xF9B0 | base_bits) as u16;
8047 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8048 let mut bytes = hw1.to_le_bytes().to_vec();
8049 bytes.extend_from_slice(&hw2.to_le_bytes());
8050 Ok(bytes)
8051 }
8052
8053 /// Encode Thumb-2 32-bit LDRSH with register: LDRSH.W Rd, [Rn, Rm]
8054 fn encode_thumb32_ldrsh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8055 let rd_bits = reg_to_bits(rd);
8056 let base_bits = reg_to_bits(base);
8057 let rm_bits = reg_to_bits(offset_reg);
8058 // LDRSH.W Rd, [Rn, Rm, LSL #0]: 1111 1001 0011 Rn | Rt 0000 00 imm2 Rm
8059 let hw1: u16 = (0xF930 | base_bits) as u16;
8060 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8061 let mut bytes = hw1.to_le_bytes().to_vec();
8062 bytes.extend_from_slice(&hw2.to_le_bytes());
8063 Ok(bytes)
8064 }
8065
8066 /// Encode Thumb-2 32-bit STRB with immediate: STRB.W Rd, [Rn, #imm12]
8067 fn encode_thumb32_strb_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8068 let rd_bits = reg_to_bits(rd);
8069 let base_bits = reg_to_bits(base);
8070 // STRB.W Rd, [Rn, #imm12]: 1111 1000 1000 Rn | Rt imm12
8071 check_ldst_imm12(offset)?;
8072 let hw1: u16 = (0xF880 | base_bits) as u16;
8073 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8074 let mut bytes = hw1.to_le_bytes().to_vec();
8075 bytes.extend_from_slice(&hw2.to_le_bytes());
8076 Ok(bytes)
8077 }
8078
8079 /// Encode Thumb-2 32-bit STRB with register: STRB.W Rd, [Rn, Rm]
8080 fn encode_thumb32_strb_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8081 let rd_bits = reg_to_bits(rd);
8082 let base_bits = reg_to_bits(base);
8083 let rm_bits = reg_to_bits(offset_reg);
8084 // STRB.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0000 Rn | Rt 0000 00 imm2 Rm
8085 let hw1: u16 = (0xF800 | base_bits) as u16;
8086 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8087 let mut bytes = hw1.to_le_bytes().to_vec();
8088 bytes.extend_from_slice(&hw2.to_le_bytes());
8089 Ok(bytes)
8090 }
8091
8092 /// Encode Thumb-2 32-bit STRH with immediate: STRH.W Rd, [Rn, #imm12]
8093 fn encode_thumb32_strh_imm(&self, rd: &Reg, base: &Reg, offset: u32) -> Result<Vec<u8>> {
8094 let rd_bits = reg_to_bits(rd);
8095 let base_bits = reg_to_bits(base);
8096 // STRH.W Rd, [Rn, #imm12]: 1111 1000 1010 Rn | Rt imm12
8097 check_ldst_imm12(offset)?;
8098 let hw1: u16 = (0xF8A0 | base_bits) as u16;
8099 let hw2: u16 = ((rd_bits << 12) | (offset & 0xFFF)) as u16;
8100 let mut bytes = hw1.to_le_bytes().to_vec();
8101 bytes.extend_from_slice(&hw2.to_le_bytes());
8102 Ok(bytes)
8103 }
8104
8105 /// Encode Thumb-2 32-bit STRH with register: STRH.W Rd, [Rn, Rm]
8106 fn encode_thumb32_strh_reg(&self, rd: &Reg, base: &Reg, offset_reg: &Reg) -> Result<Vec<u8>> {
8107 let rd_bits = reg_to_bits(rd);
8108 let base_bits = reg_to_bits(base);
8109 let rm_bits = reg_to_bits(offset_reg);
8110 // STRH.W Rd, [Rn, Rm, LSL #0]: 1111 1000 0010 Rn | Rt 0000 00 imm2 Rm
8111 let hw1: u16 = (0xF820 | base_bits) as u16;
8112 let hw2: u16 = ((rd_bits << 12) | rm_bits) as u16;
8113 let mut bytes = hw1.to_le_bytes().to_vec();
8114 bytes.extend_from_slice(&hw2.to_le_bytes());
8115 Ok(bytes)
8116 }
8117
8118 /// Encode Thumb-2 32-bit ADD with immediate: ADD.W Rd, Rn, #imm
8119 fn encode_thumb32_add_imm(&self, rd: &Reg, rn: &Reg, imm: u32) -> Result<Vec<u8>> {
8120 let rd_bits = reg_to_bits(rd);
8121 let rn_bits = reg_to_bits(rn);
8122
8123 // In-range immediates (<= 0xFFF) delegate to `encode_thumb32_add`,
8124 // which picks the correct form per value:
8125 // - imm <= 0xFF -> ADD.W (T3). Its `i:imm3:imm8` field is a
8126 // ThumbExpandImm MODIFIED immediate — raw == expanded only here.
8127 // - 0x100..=0xFFF -> ADDW (T4, 0xF200): a PLAIN 12-bit immediate.
8128 //
8129 // #681: this function used to pack the raw value into the T3 field for
8130 // ALL imm <= 0xFFF. ThumbExpandImm(0x200) = 0 and ThumbExpandImm(0x400)
8131 // = 0x8000_0000, so every dynamic-address load/store with a static
8132 // offset in 0x100..=0xFFF silently computed a WRONG address — and in
8133 // --safety-bounds software the guard checked the intended address while
8134 // the access used the mis-encoded one (bounds bypass). Same
8135 // ThumbExpandImm raw-packing class as #253/#255, reached via #382.
8136 if imm <= 0xFFF {
8137 self.encode_thumb32_add(rd, rn, imm)
8138 } else {
8139 // Out-of-range immediate (> 0xFFF): materialize it into a scratch
8140 // register, then ADD.W Rd, Rn, scratch. This is the #180/#185
8141 // "encoder must produce a legal sequence, not assert" class — see #350.
8142 //
8143 // Scratch choice (must NEVER equal Rn, or Rn would be clobbered before
8144 // the ADD reads it):
8145 // - rd != rn => use rd itself (rn is untouched, since rd != rn).
8146 // - rd == rn => use R12/IP (the reserved encoder scratch). rd/rn are
8147 // never R12 (R12 is non-allocatable), so it can't alias.
8148 //
8149 // The materialized value is the same whether or not MOVT is emitted, so
8150 // the byte length depends only on `imm` (and rd==rn) — the size probe and
8151 // the final emit therefore agree (mandatory: the function is encoded twice).
8152 let scratch: u32 = if rd_bits == rn_bits {
8153 12 // R12/IP — in-place add, can't use rd because rd == rn
8154 } else {
8155 rd_bits // rn is preserved because rd != rn
8156 };
8157 // Invariant: the scratch must never alias Rn (would clobber it before
8158 // the ADD reads it). Unreachable in real codegen (rd/rn are never R12,
8159 // which is reserved encoder scratch), but the encoder is also driven by
8160 // the `encoder_no_panic` fuzz harness with ARBITRARY registers — incl.
8161 // rd==rn==R12, which makes scratch (R12) alias Rn. The encoder contract
8162 // (#180/#185) is Ok-or-Err, never a panic, so return a typed error
8163 // instead of asserting. #350 follow-up.
8164 if scratch == rn_bits {
8165 return Err(synth_core::Error::synthesis(format!(
8166 "ADD #imm: cannot lower #{imm:#x} for Rd==Rn==R12 — no free scratch \
8167 register (R12 is the reserved encoder scratch and aliases Rn here)"
8168 )));
8169 }
8170
8171 let lo16 = imm & 0xFFFF;
8172 let hi16 = (imm >> 16) & 0xFFFF;
8173
8174 let mut bytes = self.encode_thumb32_movw_raw(scratch, lo16)?;
8175 if hi16 != 0 {
8176 bytes.extend_from_slice(&self.encode_thumb32_movt_raw(scratch, hi16)?);
8177 }
8178 bytes.extend_from_slice(&self.encode_thumb32_add_reg_raw(rd_bits, rn_bits, scratch)?);
8179 Ok(bytes)
8180 }
8181 }
8182
8183 // === Raw encoding helpers for POPCNT (take register numbers directly) ===
8184
8185 /// Encode Thumb-2 32-bit MOVW (16-bit immediate) - raw version
8186 ///
8187 /// # Contract (Verus-style)
8188 /// ```text
8189 /// requires rd <= 14, imm16 <= 0xFFFF
8190 /// ensures result.len() == 4
8191 /// ```
8192 fn encode_thumb32_movw_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8193 reg_bits_checked(rd)?;
8194 encoding_contracts::verify_imm16(imm16);
8195 // MOVW Rd, #imm16
8196 // 1111 0 i 10 0 1 0 0 imm4 | 0 imm3 Rd imm8
8197 let imm16 = imm16 & 0xFFFF;
8198 let imm4 = (imm16 >> 12) & 0xF;
8199 let i_bit = (imm16 >> 11) & 1;
8200 let imm3 = (imm16 >> 8) & 0x7;
8201 let imm8 = imm16 & 0xFF;
8202
8203 let hw1: u16 = (0xF240 | (i_bit << 10) | imm4) as u16;
8204 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8205
8206 let mut bytes = hw1.to_le_bytes().to_vec();
8207 bytes.extend_from_slice(&hw2.to_le_bytes());
8208 encoding_contracts::verify_thumb32(&bytes);
8209 Ok(bytes)
8210 }
8211
8212 /// Encode Thumb-2 32-bit MOVT (move top 16 bits) - raw version
8213 ///
8214 /// # Contract (Verus-style)
8215 /// ```text
8216 /// requires rd <= 14, imm16 <= 0xFFFF
8217 /// ensures result.len() == 4
8218 /// ```
8219 fn encode_thumb32_movt_raw(&self, rd: u32, imm16: u32) -> Result<Vec<u8>> {
8220 reg_bits_checked(rd)?;
8221 encoding_contracts::verify_imm16(imm16);
8222 // MOVT Rd, #imm16
8223 // 1111 0 i 10 1 1 0 0 imm4 | 0 imm3 Rd imm8
8224 let imm16 = imm16 & 0xFFFF;
8225 let imm4 = (imm16 >> 12) & 0xF;
8226 let i_bit = (imm16 >> 11) & 1;
8227 let imm3 = (imm16 >> 8) & 0x7;
8228 let imm8 = imm16 & 0xFF;
8229
8230 let hw1: u16 = (0xF2C0 | (i_bit << 10) | imm4) as u16;
8231 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8232
8233 let mut bytes = hw1.to_le_bytes().to_vec();
8234 bytes.extend_from_slice(&hw2.to_le_bytes());
8235 encoding_contracts::verify_thumb32(&bytes);
8236 Ok(bytes)
8237 }
8238
8239 /// Encode Thumb-2 32-bit LSR (logical shift right) with immediate - raw version
8240 fn encode_thumb32_lsr_raw(&self, rd: u32, rm: u32, shift: u32) -> Result<Vec<u8>> {
8241 // MOV.W Rd, Rm, LSR #imm
8242 // EA4F 0 imm3 Rd imm2 01 Rm
8243 let imm5 = shift & 0x1F;
8244 let imm2 = imm5 & 0x3;
8245 let imm3 = (imm5 >> 2) & 0x7;
8246
8247 let hw1: u16 = 0xEA4F;
8248 let hw2: u16 = ((imm3 << 12) | (rd << 8) | (imm2 << 6) | (0b01 << 4) | rm) as u16;
8249
8250 let mut bytes = hw1.to_le_bytes().to_vec();
8251 bytes.extend_from_slice(&hw2.to_le_bytes());
8252 Ok(bytes)
8253 }
8254
8255 /// Encode Thumb-2 32-bit AND with immediate - raw version
8256 fn encode_thumb32_and_imm_raw(&self, rd: u32, rn: u32, imm: u32) -> Result<Vec<u8>> {
8257 // AND.W Rd, Rn, #<modified_immediate>
8258 // F0 00 Rn | 0 imm3 Rd imm8
8259 //
8260 // #681 class audit: the field is a ThumbExpandImm modified immediate,
8261 // not a raw value. The only current caller (POPCNT final mask) passes
8262 // 0x3F, which expands to itself — the gate is byte-identical today and
8263 // closes the raw-packing landmine for any future caller.
8264 let field = try_thumb_expand_imm(imm).ok_or_else(|| {
8265 synth_core::Error::synthesis(
8266 "AND immediate is not a valid ThumbExpandImm — materialize into a register",
8267 )
8268 })?;
8269 let i_bit = (field >> 11) & 1;
8270 let imm3 = (field >> 8) & 0x7;
8271 let imm8 = field & 0xFF;
8272
8273 let hw1: u16 = (0xF000 | (i_bit << 10) | rn) as u16;
8274 let hw2: u16 = ((imm3 << 12) | (rd << 8) | imm8) as u16;
8275
8276 let mut bytes = hw1.to_le_bytes().to_vec();
8277 bytes.extend_from_slice(&hw2.to_le_bytes());
8278 Ok(bytes)
8279 }
8280
8281 /// Encode Thumb-2 32-bit SUB (register) - raw version
8282 fn encode_thumb32_sub_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8283 // SUB.W Rd, Rn, Rm
8284 // EBA0 Rn | 0 Rd 00 00 Rm
8285 let hw1: u16 = (0xEBA0 | rn) as u16;
8286 let hw2: u16 = ((rd << 8) | rm) as u16;
8287
8288 let mut bytes = hw1.to_le_bytes().to_vec();
8289 bytes.extend_from_slice(&hw2.to_le_bytes());
8290 Ok(bytes)
8291 }
8292
8293 /// Encode Thumb-2 32-bit ADD (register) - raw version
8294 fn encode_thumb32_add_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8295 // ADD.W Rd, Rn, Rm
8296 // EB00 Rn | 0 Rd 00 00 Rm
8297 let hw1: u16 = (0xEB00 | rn) as u16;
8298 let hw2: u16 = ((rd << 8) | rm) as u16;
8299
8300 let mut bytes = hw1.to_le_bytes().to_vec();
8301 bytes.extend_from_slice(&hw2.to_le_bytes());
8302 Ok(bytes)
8303 }
8304
8305 /// Encode Thumb-2 32-bit ADDS (register, flag-setting) - raw version.
8306 /// Used as the high-register fallback for `ArmOp::Adds` (i64 low-word add)
8307 /// so R8-R11 pair operands don't overflow the 16-bit field — #178/#180.
8308 fn encode_thumb32_adds_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8309 // ADDS.W Rd, Rn, Rm (T3, S=1): EB10 Rn | 0 Rd 00 00 Rm
8310 let hw1: u16 = (0xEB10 | rn) as u16;
8311 let hw2: u16 = ((rd << 8) | rm) as u16;
8312 let mut bytes = hw1.to_le_bytes().to_vec();
8313 bytes.extend_from_slice(&hw2.to_le_bytes());
8314 Ok(bytes)
8315 }
8316
8317 /// Encode Thumb-2 32-bit SUBS (register, flag-setting) - raw version.
8318 /// High-register fallback for `ArmOp::Subs` (i64 low-word subtract) — #178/#180.
8319 fn encode_thumb32_subs_reg_raw(&self, rd: u32, rn: u32, rm: u32) -> Result<Vec<u8>> {
8320 // SUBS.W Rd, Rn, Rm (T3, S=1): EBB0 Rn | 0 Rd 00 00 Rm
8321 let hw1: u16 = (0xEBB0 | rn) as u16;
8322 let hw2: u16 = ((rd << 8) | rm) as u16;
8323 let mut bytes = hw1.to_le_bytes().to_vec();
8324 bytes.extend_from_slice(&hw2.to_le_bytes());
8325 Ok(bytes)
8326 }
8327
8328 /// Encode a sequence of ARM instructions
8329 pub fn encode_sequence(&self, ops: &[ArmOp]) -> Result<Vec<u8>> {
8330 let mut code = Vec::new();
8331
8332 for op in ops {
8333 let encoded = self.encode(op)?;
8334 code.extend_from_slice(&encoded);
8335 }
8336
8337 Ok(code)
8338 }
8339}
8340
8341/// Convert register to bit encoding (0-15)
8342/// Reverse of the ARMv7-M `ThumbExpandImm`: given a 32-bit immediate, return the
8343/// 12-bit `i:imm3:imm8` field if it is a representable modified immediate, else
8344/// `None` (the caller must materialize the value into a register). This is the
8345/// shared correct path for the data-processing immediate encoders — without it
8346/// they pack raw bits and silently mis-encode any value `> 0xFF` that isn't a
8347/// modified immediate (the silent-miscompile class behind #251/#253/#255).
8348fn try_thumb_expand_imm(value: u32) -> Option<u32> {
8349 // i:imm3 = 0000 → 8-bit value, zero-extended (00000000 00000000 00000000 XY).
8350 if value <= 0xFF {
8351 return Some(value);
8352 }
8353 let b0 = value & 0xFF; // byte 0
8354 let b1 = (value >> 8) & 0xFF; // byte 1
8355 // 0x00XY00XY (i:imm3 = 0001) — XY in bytes 0 and 2
8356 if value == (b0 << 16) | b0 {
8357 return Some(0x100 | b0);
8358 }
8359 // 0xXY00XY00 (i:imm3 = 0010) — XY in bytes 1 and 3
8360 if value == (b1 << 24) | (b1 << 8) {
8361 return Some(0x200 | b1);
8362 }
8363 // 0xXYXYXYXY (i:imm3 = 0011) — XY in all four bytes
8364 if value == (b0 << 24) | (b0 << 16) | (b0 << 8) | b0 {
8365 return Some(0x300 | b0);
8366 }
8367 // An 8-bit value with bit 7 set, rotated right by 8..=31. `rotate_left(rot)`
8368 // undoes the encoded right rotation; if the result is `1bbbbbbb` (0x80..=0xFF)
8369 // the value is representable. imm12[11:7] = rot, imm12[6:0] = low 7 bits.
8370 for rot in 8..=31u32 {
8371 let unrot = value.rotate_left(rot);
8372 if (0x80..=0xFF).contains(&unrot) {
8373 return Some((rot << 7) | (unrot & 0x7F));
8374 }
8375 }
8376 None
8377}
8378
8379/// Guard a Thumb-2 `LDR/STR Rd, [Rn, #imm12]` offset. The imm12 form supports
8380/// `0..=4095`; a larger offset must be materialized into a register by the
8381/// selector (register-offset addressing). Returning `Err` rather than silently
8382/// masking `offset & 0xFFF` closes the wrong-address miscompile class (#259,
8383/// the load/store sibling of #253/#255).
8384fn check_ldst_imm12(offset: u32) -> Result<()> {
8385 if offset > 0xFFF {
8386 Err(synth_core::Error::synthesis(
8387 "load/store immediate offset > 0xFFF (4095) — materialize the offset into a register",
8388 ))
8389 } else {
8390 Ok(())
8391 }
8392}
8393
8394/// #916 — emit `Rd = 0` in Thumb-2, correctly for EVERY destination register.
8395///
8396/// The 16-bit `MOVS Rd, #imm8` (T1) is `0010 0 Rd(3) imm8` — the Rd field is
8397/// **three bits**. For R8-R12 `reg_to_bits` yields 8..12, so `rd_bits << 8`
8398/// overflows into bit 11 and `0x2000 | 0x0800` is `0x2800` = `CMP r0, #0`:
8399/// not a move at all. The destination is never written (it keeps stale data)
8400/// and the flags are clobbered. Same class as #180 / H-CODE-9, and the same
8401/// defect #311 fixed for `I64SetCond`.
8402///
8403/// High registers therefore take the 32-bit `MOV.W Rd, #imm8` (T2,
8404/// `F04F 0000 | Rd<<8 | imm8`), whose Rd field is four bits. `MOV.W` with S=0
8405/// does not set flags, which is what these zero-fill sites want anyway.
8406///
8407/// **Callers with branches must consult [`thumb_zero_fill_halfwords`].** This
8408/// emits 1 halfword for R0-R7 and 2 for R8-R12; any branch whose target lies
8409/// PAST this instruction moves when it widens and its displacement has to be
8410/// derived rather than hard-coded. (A branch targeting this instruction's own
8411/// address is unaffected — an instruction cannot move itself.)
8412fn emit_thumb_zero_fill(bytes: &mut Vec<u8>, rd_bits: u32) {
8413 if rd_bits < 8 {
8414 let movs: u16 = 0x2000 | ((rd_bits as u16) << 8);
8415 bytes.extend_from_slice(&movs.to_le_bytes());
8416 } else {
8417 bytes.extend_from_slice(&0xF04Fu16.to_le_bytes());
8418 bytes.extend_from_slice(&((rd_bits as u16) << 8).to_le_bytes());
8419 }
8420}
8421
8422/// Halfword length of the encoding [`emit_thumb_zero_fill`] picks for
8423/// `rd_bits`. Branch displacements spanning the zero-fill derive from this so
8424/// the encoder cannot drift from itself (#916; the byte-size estimator mirrors
8425/// it in `synth_synthesis::estimate_arm_byte_size`, pinned by the #498
8426/// `estimator_encoder_agreement` oracle).
8427fn thumb_zero_fill_halfwords(rd_bits: u32) -> u16 {
8428 if rd_bits < 8 { 1 } else { 2 }
8429}
8430
8431fn reg_to_bits(reg: &Reg) -> u32 {
8432 match reg {
8433 Reg::R0 => 0,
8434 Reg::R1 => 1,
8435 Reg::R2 => 2,
8436 Reg::R3 => 3,
8437 Reg::R4 => 4,
8438 Reg::R5 => 5,
8439 Reg::R6 => 6,
8440 Reg::R7 => 7,
8441 Reg::R8 => 8,
8442 Reg::R9 => 9,
8443 Reg::R10 => 10,
8444 Reg::R11 => 11,
8445 Reg::R12 => 12,
8446 Reg::SP => 13,
8447 Reg::LR => 14,
8448 Reg::PC => 15,
8449 }
8450}
8451
8452// ======================================================================
8453// #610 — i64 fixed-ABI expansion wrappers.
8454//
8455// The hand-written multi-instruction i64 cores (rotl/rotr and the div/rem
8456// shift-subtract loops) compute in FIXED low registers. Before #610 the
8457// div/rem arms ignored their operand fields outright (hardcoded R0:R1 /
8458// R2:R3 in, result to R0:R1) and the rot arms used R3/R4 scratch that
8459// collided with selector-assigned registers — then restored the saved
8460// scratch OVER the result (`POP {R4}` with rd_lo == R4), so the op
8461// returned the caller's stale register: 0 for every input under qemu.
8462//
8463// These wrappers make each core honor its register parameters:
8464// 1. save R0-R3,
8465// 2. marshal the operand registers into the core's fixed input regs via
8466// the stack (permutation-safe: every source is read before any fixed
8467// register is written),
8468// 3. run the fixed-reg core (self-preserving for R4+; R12 is encoder
8469// scratch and never allocatable, #212),
8470// 4. MOV the result pair from R0:R1 into the selector's rd pair,
8471// 5. restore R0-R3, skipping any register the result now occupies.
8472//
8473// All emitted lengths are register-independent so the optimized path's
8474// byte-size estimator (`estimate_arm_byte_size`, pinned by the
8475// estimator↔encoder agreement oracle #498/#511) stays a constant per op.
8476// ======================================================================
8477
8478/// Steps 1+2: `PUSH {R0-R3}`, then marshal `srcs` (operand registers, any of
8479/// R0-R12) into `R0..R<n>` via individual stack pushes. Sources are all read
8480/// before any destination register is written, so arbitrary source/target
8481/// permutations (including operands living in R0-R3) are safe.
8482fn emit_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8483 debug_assert!(srcs.len() <= 4);
8484 // PUSH {R0-R3} — save the caller-visible low registers.
8485 bytes.extend_from_slice(&0xB40Fu16.to_le_bytes());
8486 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8487 for src in srcs.iter().rev() {
8488 let rt = reg_to_bits(src) as u16;
8489 bytes.extend_from_slice(&0xF84Du16.to_le_bytes());
8490 bytes.extend_from_slice(&((rt << 12) | 0x0D04).to_le_bytes());
8491 }
8492 // POP {Ri} — Ri := srcs[i].
8493 for i in 0..srcs.len() as u16 {
8494 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes());
8495 }
8496}
8497
8498/// Steps 4+5: move the core's R0:R1 result into the selector's rd pair, then
8499/// restore the R0-R3 saved by [`emit_i64_fixed_abi_entry`], skipping any
8500/// register the result now lives in (its saved caller word is discarded).
8501fn emit_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8502 let lo = reg_to_bits(rdlo);
8503 let hi = reg_to_bits(rdhi);
8504 if lo == 1 && hi == 0 {
8505 // A fully swapped pair would clobber one half in either MOV order.
8506 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8507 return Err(synth_core::Error::synthesis(
8508 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8509 ));
8510 }
8511 let mov16 = |bytes: &mut Vec<u8>, rd: u32, rm: u32| {
8512 let d = ((rd >> 3) & 1) as u16;
8513 bytes.extend_from_slice(
8514 &(0x4600u16 | (d << 7) | ((rm as u16) << 3) | ((rd & 7) as u16)).to_le_bytes(),
8515 );
8516 };
8517 if hi == 0 {
8518 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8519 mov16(bytes, lo, 0);
8520 mov16(bytes, hi, 1);
8521 } else {
8522 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8523 mov16(bytes, hi, 1);
8524 mov16(bytes, lo, 0);
8525 }
8526 for i in 0..4u32 {
8527 if i == lo || i == hi {
8528 // The result lives here — drop the saved caller word.
8529 bytes.extend_from_slice(&0xB001u16.to_le_bytes()); // ADD SP, #4
8530 } else {
8531 bytes.extend_from_slice(&(0xBC00u16 | (1u16 << i)).to_le_bytes()); // POP {Ri}
8532 }
8533 }
8534 Ok(())
8535}
8536
8537/// WASM `i64.div_*` / `i64.rem_*` by zero must trap, matching the i32 path's
8538/// cmp/bne/udf guard. Emitted after marshaling, when the divisor pair is in
8539/// R2:R3: `ORRS R12, R2, R3` — `BNE` over a `UDF #0` when nonzero.
8540fn emit_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8541 bytes.extend_from_slice(&0xEA52u16.to_le_bytes()); // ORRS.W R12, R2, R3
8542 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8543 bytes.extend_from_slice(&0xD100u16.to_le_bytes()); // BNE.N +0 (skip the UDF)
8544 bytes.extend_from_slice(&0xDE00u16.to_le_bytes()); // UDF #0 — divide by zero
8545}
8546
8547/// WASM `i64.div_s(INT64_MIN, -1)` must trap (Core §4.3.2 `idiv_s`: the
8548/// quotient +2^63 is unrepresentable), matching the i32 path's overflow
8549/// guard — #633: without it the core negated INT64_MIN onto itself and
8550/// silently returned INT64_MIN. Emitted after marshaling, when the dividend
8551/// pair is in R0:R1 and the divisor pair in R2:R3; R12 is encoder scratch.
8552///
8553/// div_s ONLY — `i64.rem_s(INT64_MIN, -1)` is defined as 0 and must NOT
8554/// trap (`irem_s`), so the I64RemS arm never calls this. 22 bytes,
8555/// register-independent (estimator contract, #498/#511).
8556fn emit_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8557 // AND.W R12, R2, R3 — R12 == 0xFFFFFFFF iff divisor == -1
8558 bytes.extend_from_slice(&0xEA02u16.to_le_bytes());
8559 bytes.extend_from_slice(&0x0C03u16.to_le_bytes());
8560 // CMN.W R12, #1 — EQ iff both divisor words are all-ones
8561 bytes.extend_from_slice(&0xF11Cu16.to_le_bytes());
8562 bytes.extend_from_slice(&0x0F01u16.to_le_bytes());
8563 // BNE .no_trap
8564 bytes.extend_from_slice(&0xD105u16.to_le_bytes());
8565 // CMP R0, #0 — dividend lo word of INT64_MIN
8566 bytes.extend_from_slice(&0x2800u16.to_le_bytes());
8567 // BNE .no_trap
8568 bytes.extend_from_slice(&0xD103u16.to_le_bytes());
8569 // CMP.W R1, #0x80000000 — dividend hi word of INT64_MIN
8570 bytes.extend_from_slice(&0xF1B1u16.to_le_bytes());
8571 bytes.extend_from_slice(&0x4F00u16.to_le_bytes());
8572 // BNE .no_trap
8573 bytes.extend_from_slice(&0xD100u16.to_le_bytes());
8574 // UDF #0 — signed-division overflow
8575 bytes.extend_from_slice(&0xDE00u16.to_le_bytes());
8576 // .no_trap:
8577}
8578
8579// ======================================================================
8580// #615 — A32 (ARM-mode) twins of the #610 i64 fixed-ABI wrappers above.
8581// Identical register contract, A32 encodings: the multi-instruction i64
8582// cores (rotl/rotr, div/rem) compute in fixed low registers (value/dividend
8583// R0:R1, amount R2 / divisor R2:R3, result to R0:R1); the wrappers marshal
8584// the selector-assigned operand registers in and the result out, saving and
8585// restoring the caller-visible R0-R3 around the core.
8586// ======================================================================
8587
8588/// A32 steps 1+2: `STMDB SP!, {R0-R3}`, then marshal `srcs` into `R0..R<n>`
8589/// via individual stack pushes (`STR src, [SP, #-4]!` in reverse order, then
8590/// `LDR Ri, [SP], #4`). Every source is read before any fixed register is
8591/// written, so arbitrary source/target permutations are safe.
8592fn emit_a32_i64_fixed_abi_entry(bytes: &mut Vec<u8>, srcs: &[&Reg]) {
8593 debug_assert!(srcs.len() <= 4);
8594 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8595 // PUSH {R0-R3} — save the caller-visible low registers.
8596 w(bytes, 0xE92D_000F);
8597 // STR src, [SP, #-4]! — push in reverse so srcs[0] ends up on top.
8598 for src in srcs.iter().rev() {
8599 w(bytes, 0xE52D_0004 | (reg_to_bits(src) << 12));
8600 }
8601 // LDR Ri, [SP], #4 — Ri := srcs[i].
8602 for i in 0..srcs.len() as u32 {
8603 w(bytes, 0xE49D_0004 | (i << 12));
8604 }
8605}
8606
8607/// A32 steps 4+5: move the core's R0:R1 result into the selector's rd pair,
8608/// then restore the R0-R3 saved by [`emit_a32_i64_fixed_abi_entry`], skipping
8609/// any register the result now lives in (its saved caller word is discarded).
8610fn emit_a32_i64_fixed_abi_exit(bytes: &mut Vec<u8>, rdlo: &Reg, rdhi: &Reg) -> Result<()> {
8611 let lo = reg_to_bits(rdlo);
8612 let hi = reg_to_bits(rdhi);
8613 if lo == 1 && hi == 0 {
8614 // A fully swapped pair would clobber one half in either MOV order.
8615 // Selector pairs are consecutive (lo, lo+1), so this cannot occur.
8616 return Err(synth_core::Error::synthesis(
8617 "i64 expansion: swapped result pair (rd_lo=R1, rd_hi=R0) is unsupported (#610)",
8618 ));
8619 }
8620 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8621 let mov = |bytes: &mut Vec<u8>, rd: u32, rm: u32| w(bytes, 0xE1A0_0000 | (rd << 12) | rm);
8622 if hi == 0 {
8623 // rd_hi is R0: read R0 into rd_lo BEFORE overwriting R0 with R1.
8624 mov(bytes, lo, 0);
8625 mov(bytes, hi, 1);
8626 } else {
8627 // rd_lo may be R1: read R1 into rd_hi BEFORE overwriting R1 with R0.
8628 mov(bytes, hi, 1);
8629 mov(bytes, lo, 0);
8630 }
8631 for i in 0..4u32 {
8632 if i == lo || i == hi {
8633 // The result lives here — drop the saved caller word.
8634 w(bytes, 0xE28D_D004); // ADD SP, SP, #4
8635 } else {
8636 w(bytes, 0xE49D_0004 | (i << 12)); // LDR Ri, [SP], #4
8637 }
8638 }
8639 Ok(())
8640}
8641
8642/// A32 zero-divisor trap, emitted after marshaling when the divisor pair is
8643/// in R2:R3: `ORRS R12, R2, R3` sets Z iff the divisor is zero; `BNE` skips a
8644/// `UDF #0` (WASM div/rem-by-zero must trap, matching the Thumb-2 twin).
8645fn emit_a32_i64_divisor_zero_trap(bytes: &mut Vec<u8>) {
8646 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8647 w(bytes, 0xE192_C003); // ORRS R12, R2, R3
8648 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8649 w(bytes, 0xE7F0_00F0); // UDF #0 — divide by zero
8650}
8651
8652/// A32 twin of [`emit_i64_divs_overflow_trap`] (#633): trap on
8653/// `i64.div_s(INT64_MIN, -1)`. Conditional execution replaces the Thumb
8654/// branches — the CMPEQ chain leaves EQ set only when divisor == -1 AND
8655/// dividend == INT64_MIN. div_s only; rem_s must keep returning 0.
8656fn emit_a32_i64_divs_overflow_trap(bytes: &mut Vec<u8>) {
8657 let w = |bytes: &mut Vec<u8>, word: u32| bytes.extend_from_slice(&word.to_le_bytes());
8658 w(bytes, 0xE002_C003); // AND R12, R2, R3 (== 0xFFFFFFFF iff divisor == -1)
8659 w(bytes, 0xE37C_0001); // CMN R12, #1 (EQ iff divisor == -1)
8660 w(bytes, 0x0350_0000); // CMPEQ R0, #0 (EQ iff also dividend lo == 0)
8661 w(bytes, 0x0351_0102); // CMPEQ R1, #0x80000000 (EQ iff dividend == INT64_MIN)
8662 w(bytes, 0x1A00_0000); // BNE +1 insn (skip the UDF)
8663 w(bytes, 0xE7F0_00F0); // UDF #0 — signed-division overflow
8664}
8665
8666/// Fallible form of the `verify_reg_bits` contract. PC (R15) is not a valid
8667/// data operand for the Thumb-2 encodings that use this guard (SDIV/UDIV/MLS/…
8668/// are UNPREDICTABLE with PC). Synth's own codegen never emits PC there, but
8669/// the encoder must stay *total* over arbitrary `ArmOp` inputs — the fuzz
8670/// harness (`encoder_no_panic`) requires Ok-or-Err, never a panic. Pre-fix, the
8671/// `debug_assert` in `verify_reg_bits` aborted under `-Cdebug-assertions`.
8672/// Returns a typed Err instead. See #185.
8673fn reg_bits_checked(bits: u32) -> Result<()> {
8674 if bits > 14 {
8675 return Err(synth_core::Error::synthesis(format!(
8676 "register bits {bits} (PC/R15) is not a valid operand for this Thumb-2 encoding"
8677 )));
8678 }
8679 Ok(())
8680}
8681
8682/// Try to encode a 32-bit value as an ARM rotated immediate (imm8 ROR 2*rot4).
8683/// Returns Some((encoded_bits, 1)) if representable, None otherwise.
8684fn try_encode_rotated_imm(val: u32) -> Option<(u32, u32)> {
8685 if val == 0 {
8686 return Some((0, 1));
8687 }
8688 for rot in 0..16u32 {
8689 let shift = rot * 2;
8690 // Rotate left by shift (undo the ROR) to see if result fits in 8 bits
8691 let unrotated = val.rotate_left(shift);
8692 if unrotated <= 0xFF {
8693 // Encoded as: rot4(4 bits) | imm8(8 bits) = rotate_imm << 8 | imm8
8694 return Some(((rot << 8) | unrotated, 1));
8695 }
8696 }
8697 None
8698}
8699
8700/// Encode operand2 field and return (bits, immediate_flag).
8701/// For ARM32 mode, immediates use the rotated-immediate encoding (imm8 ROR 2*rot4).
8702/// Panics if an immediate value cannot be represented. Callers that need large
8703/// immediates should use MOVW/MOVT instead of Operand2::Imm.
8704fn encode_operand2(op2: &Operand2) -> Result<(u32, u32)> {
8705 match op2 {
8706 Operand2::Imm(val) => {
8707 let uval = *val as u32;
8708 // Attempt rotated-immediate encoding (ARM32 Operand2)
8709 if let Some(encoded) = try_encode_rotated_imm(uval) {
8710 Ok(encoded)
8711 } else {
8712 // #378-class honesty: an immediate that can't be expressed as an
8713 // ARM32 rotated immediate is an INTERNAL selector bug — large
8714 // constants must be materialized via MOVW/MOVT, not passed here.
8715 // FAIL HONESTLY with an Err rather than silently masking to
8716 // `uval & 0xFF` and emitting a WRONG immediate. The encoder is
8717 // Ok-or-Err, never corrupt (#180/#185); a loud Err is also why
8718 // this is an Err and not a panic (the `encoder_no_panic` fuzz
8719 // contract — malformed/oversized input must degrade, not crash).
8720 Err(synth_core::Error::synthesis(format!(
8721 "encode_operand2: immediate {uval:#x} ({val}) is not an ARM32 \
8722 rotated immediate — the selector must materialize large \
8723 constants via MOVW/MOVT"
8724 )))
8725 }
8726 }
8727
8728 Operand2::Reg(reg) => {
8729 let reg_bits = reg_to_bits(reg);
8730 Ok((reg_bits, 0)) // I=0 for register
8731 }
8732
8733 Operand2::RegShift {
8734 rm,
8735 shift: _,
8736 amount,
8737 } => {
8738 // Simplified encoding with shift
8739 let rm_bits = reg_to_bits(rm);
8740 let shift_bits = (*amount & 0x1F) << 7;
8741 Ok((shift_bits | rm_bits, 0))
8742 }
8743 }
8744}
8745
8746/// Encode memory address to (base_reg, offset)
8747fn encode_mem_addr(addr: &MemAddr) -> (u32, u32) {
8748 let base_bits = reg_to_bits(&addr.base);
8749 let offset_bits = (addr.offset as u32) & 0xFFF; // 12-bit offset
8750 (base_bits, offset_bits)
8751}
8752
8753/// S-register number: S0=0, S1=1, ..., S31=31
8754fn vfp_sreg_to_num(reg: &VfpReg) -> Result<u32> {
8755 match reg {
8756 VfpReg::S0 => Ok(0),
8757 VfpReg::S1 => Ok(1),
8758 VfpReg::S2 => Ok(2),
8759 VfpReg::S3 => Ok(3),
8760 VfpReg::S4 => Ok(4),
8761 VfpReg::S5 => Ok(5),
8762 VfpReg::S6 => Ok(6),
8763 VfpReg::S7 => Ok(7),
8764 VfpReg::S8 => Ok(8),
8765 VfpReg::S9 => Ok(9),
8766 VfpReg::S10 => Ok(10),
8767 VfpReg::S11 => Ok(11),
8768 VfpReg::S12 => Ok(12),
8769 VfpReg::S13 => Ok(13),
8770 VfpReg::S14 => Ok(14),
8771 VfpReg::S15 => Ok(15),
8772 VfpReg::S16 => Ok(16),
8773 VfpReg::S17 => Ok(17),
8774 VfpReg::S18 => Ok(18),
8775 VfpReg::S19 => Ok(19),
8776 VfpReg::S20 => Ok(20),
8777 VfpReg::S21 => Ok(21),
8778 VfpReg::S22 => Ok(22),
8779 VfpReg::S23 => Ok(23),
8780 VfpReg::S24 => Ok(24),
8781 VfpReg::S25 => Ok(25),
8782 VfpReg::S26 => Ok(26),
8783 VfpReg::S27 => Ok(27),
8784 VfpReg::S28 => Ok(28),
8785 VfpReg::S29 => Ok(29),
8786 VfpReg::S30 => Ok(30),
8787 VfpReg::S31 => Ok(31),
8788 // D-registers are not used in F32 single-precision encodings
8789 _ => Err(synth_core::Error::SynthesisError(
8790 "D-register not supported in single-precision VFP encoding".to_string(),
8791 )),
8792 }
8793}
8794
8795/// D-register number: D0=0, D1=1, ..., D15=15
8796fn vfp_dreg_to_num(reg: &VfpReg) -> Result<u32> {
8797 match reg {
8798 VfpReg::D0 => Ok(0),
8799 VfpReg::D1 => Ok(1),
8800 VfpReg::D2 => Ok(2),
8801 VfpReg::D3 => Ok(3),
8802 VfpReg::D4 => Ok(4),
8803 VfpReg::D5 => Ok(5),
8804 VfpReg::D6 => Ok(6),
8805 VfpReg::D7 => Ok(7),
8806 VfpReg::D8 => Ok(8),
8807 VfpReg::D9 => Ok(9),
8808 VfpReg::D10 => Ok(10),
8809 VfpReg::D11 => Ok(11),
8810 VfpReg::D12 => Ok(12),
8811 VfpReg::D13 => Ok(13),
8812 VfpReg::D14 => Ok(14),
8813 VfpReg::D15 => Ok(15),
8814 // S-registers are not used in F64 double-precision encodings
8815 _ => Err(synth_core::Error::SynthesisError(
8816 "S-register not supported in double-precision VFP encoding".to_string(),
8817 )),
8818 }
8819}
8820
8821/// Split S-register into (Vx[3:0], qualifier_bit) for VFP encoding.
8822/// For an S-register number s: Vx = s >> 1, qualifier = s & 1.
8823/// The qualifier bit goes to D (bit 22), N (bit 7), or M (bit 5) depending on role.
8824fn encode_sreg(s: u32) -> (u32, u32) {
8825 (s >> 1, s & 1)
8826}
8827
8828/// Split D-register into (Vx[3:0], qualifier_bit) for VFP double-precision encoding.
8829/// For a D-register number d: Vx = d & 0xF, qualifier = (d >> 4) & 1.
8830/// For D0-D15, qualifier is always 0.
8831fn encode_dreg(d: u32) -> (u32, u32) {
8832 (d & 0xF, (d >> 4) & 1)
8833}
8834
8835/// Encode a VFP 3-register arithmetic instruction (VADD.F32, VSUB.F32, VMUL.F32, VDIV.F32).
8836/// Returns the full 32-bit instruction word.
8837///
8838/// VFP encoding: [cond 1110] [D opc1 Vn] [Vd 101 sz] [N opc2 M 0 Vm]
8839/// For single-precision (sz=0), coprocessor = 0xA (bits[11:8]).
8840fn encode_vfp_3reg(base: u32, sd: &VfpReg, sn: &VfpReg, sm: &VfpReg) -> Result<u32> {
8841 let sd_num = vfp_sreg_to_num(sd)?;
8842 let sn_num = vfp_sreg_to_num(sn)?;
8843 let sm_num = vfp_sreg_to_num(sm)?;
8844 let (vd, d) = encode_sreg(sd_num);
8845 let (vn, n) = encode_sreg(sn_num);
8846 let (vm, m) = encode_sreg(sm_num);
8847
8848 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8849}
8850
8851/// Encode a VFP 2-register instruction (VNEG.F32, VABS.F32, VSQRT.F32).
8852/// Returns the full 32-bit instruction word.
8853fn encode_vfp_2reg(base: u32, sd: &VfpReg, sm: &VfpReg) -> Result<u32> {
8854 let sd_num = vfp_sreg_to_num(sd)?;
8855 let sm_num = vfp_sreg_to_num(sm)?;
8856 let (vd, d) = encode_sreg(sd_num);
8857 let (vm, m) = encode_sreg(sm_num);
8858
8859 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8860}
8861
8862/// Encode a VFP load/store (VLDR.F32 / VSTR.F32).
8863/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8864/// U bit (bit 23) controls add/subtract offset.
8865fn encode_vfp_ldst(base: u32, sd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8866 let sd_num = vfp_sreg_to_num(sd)?;
8867 let (vd, d) = encode_sreg(sd_num);
8868 let rn = reg_to_bits(&addr.base);
8869
8870 let offset = addr.offset;
8871 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8872 let abs_offset = offset.unsigned_abs();
8873 let imm8 = (abs_offset / 4) & 0xFF;
8874
8875 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8876}
8877
8878/// Encode VMOV between core register and S-register.
8879/// VMOV Sn, Rt: 0xEE00_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8880/// VMOV Rt, Sn: 0xEE10_0A10 | (Vn << 16) | (N << 7) | (Rt << 12)
8881fn encode_vmov_core_sreg(to_sreg: bool, sreg: &VfpReg, core: &Reg) -> Result<u32> {
8882 let s_num = vfp_sreg_to_num(sreg)?;
8883 let (vn, n) = encode_sreg(s_num);
8884 let rt = reg_to_bits(core);
8885
8886 let base = if to_sreg { 0xEE000A10 } else { 0xEE100A10 };
8887 Ok(base | (vn << 16) | (rt << 12) | (n << 7))
8888}
8889
8890/// Encode a VFP 3-register double-precision instruction (VADD.F64, VSUB.F64, etc.).
8891/// For double-precision (sz=1), coprocessor = 0xB (bits[11:8]).
8892/// The base should have bit 8 = 1 for F64 (0xB suffix instead of 0xA).
8893fn encode_vfp_3reg_f64(base: u32, dd: &VfpReg, dn: &VfpReg, dm: &VfpReg) -> Result<u32> {
8894 let dd_num = vfp_dreg_to_num(dd)?;
8895 let dn_num = vfp_dreg_to_num(dn)?;
8896 let dm_num = vfp_dreg_to_num(dm)?;
8897 let (vd, d) = encode_dreg(dd_num);
8898 let (vn, n) = encode_dreg(dn_num);
8899 let (vm, m) = encode_dreg(dm_num);
8900
8901 Ok(base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm)
8902}
8903
8904/// Encode a VFP 2-register double-precision instruction (VNEG.F64, VABS.F64, VSQRT.F64).
8905fn encode_vfp_2reg_f64(base: u32, dd: &VfpReg, dm: &VfpReg) -> Result<u32> {
8906 let dd_num = vfp_dreg_to_num(dd)?;
8907 let dm_num = vfp_dreg_to_num(dm)?;
8908 let (vd, d) = encode_dreg(dd_num);
8909 let (vm, m) = encode_dreg(dm_num);
8910
8911 Ok(base | (d << 22) | (vd << 12) | (m << 5) | vm)
8912}
8913
8914/// Encode a VFP load/store for double-precision (VLDR.64 / VSTR.64).
8915/// offset is in bytes and must be word-aligned; encoded as imm8 = offset/4.
8916fn encode_vfp_ldst_f64(base: u32, dd: &VfpReg, addr: &MemAddr) -> Result<u32> {
8917 let dd_num = vfp_dreg_to_num(dd)?;
8918 let (vd, d) = encode_dreg(dd_num);
8919 let rn = reg_to_bits(&addr.base);
8920
8921 let offset = addr.offset;
8922 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
8923 let abs_offset = offset.unsigned_abs();
8924 let imm8 = (abs_offset / 4) & 0xFF;
8925
8926 Ok(base | (u_bit << 23) | (d << 22) | (rn << 16) | (vd << 12) | imm8)
8927}
8928
8929/// Encode VMOV between two core registers and a D-register.
8930/// VMOV Dm, Rt, Rt2: 0xEC40_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8931/// VMOV Rt, Rt2, Dm: 0xEC50_0B10 | (Rt2 << 16) | (Rt << 12) | (M << 5) | Vm
8932fn encode_vmov_core_dreg(
8933 to_dreg: bool,
8934 dreg: &VfpReg,
8935 core_lo: &Reg,
8936 core_hi: &Reg,
8937) -> Result<u32> {
8938 let d_num = vfp_dreg_to_num(dreg)?;
8939 let (vm, m) = encode_dreg(d_num);
8940 let rt = reg_to_bits(core_lo);
8941 let rt2 = reg_to_bits(core_hi);
8942
8943 let base = if to_dreg { 0xEC400B10 } else { 0xEC500B10 };
8944 Ok(base | (rt2 << 16) | (rt << 12) | (m << 5) | vm)
8945}
8946
8947/// Emit a VFP 32-bit instruction as Thumb-2 bytes (two LE halfwords).
8948fn vfp_to_thumb_bytes(instr: u32) -> Vec<u8> {
8949 let hw1 = ((instr >> 16) & 0xFFFF) as u16;
8950 let hw2 = (instr & 0xFFFF) as u16;
8951 let mut bytes = hw1.to_le_bytes().to_vec();
8952 bytes.extend_from_slice(&hw2.to_le_bytes());
8953 bytes
8954}
8955
8956// ============================================================================
8957// Helium MVE encoding helpers
8958// ============================================================================
8959
8960/// Q-register number: Q0=0, Q1=1, ..., Q7=7
8961fn qreg_to_num(reg: &QReg) -> u32 {
8962 match reg {
8963 QReg::Q0 => 0,
8964 QReg::Q1 => 1,
8965 QReg::Q2 => 2,
8966 QReg::Q3 => 3,
8967 QReg::Q4 => 4,
8968 QReg::Q5 => 5,
8969 QReg::Q6 => 6,
8970 QReg::Q7 => 7,
8971 }
8972}
8973
8974/// MVE element size to encoding bits: S8=0b00, S16=0b01, S32=0b10
8975fn mve_size_bits(size: &MveSize) -> u32 {
8976 match size {
8977 MveSize::S8 => 0b00,
8978 MveSize::S16 => 0b01,
8979 MveSize::S32 => 0b10,
8980 }
8981}
8982
8983/// Encode MVE 3-register instruction.
8984/// Q-registers are encoded as D-register pairs: Q0=D0:D1, Q1=D2:D3, etc.
8985/// In NEON/MVE encoding, the Q-register uses D-register number = Qn * 2.
8986fn encode_mve_3reg(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
8987 let d = qreg_to_num(qd) * 2;
8988 let n = qreg_to_num(qn) * 2;
8989 let m = qreg_to_num(qm) * 2;
8990
8991 // Standard NEON/MVE 3-register encoding:
8992 // D bit (bit 22) = Vd[4], Vd[3:0] = bits [15:12]
8993 // N bit (bit 7) = Vn[4], Vn[3:0] = bits [19:16]
8994 // M bit (bit 5) = Vm[4], Vm[3:0] = bits [3:0]
8995 let vd = d & 0xF;
8996 let d_bit = (d >> 4) & 1;
8997 let vn = n & 0xF;
8998 let n_bit = (n >> 4) & 1;
8999 let vm = m & 0xF;
9000 let m_bit = (m >> 4) & 1;
9001
9002 base | (d_bit << 22) | (vn << 16) | (vd << 12) | (n_bit << 7) | (m_bit << 5) | vm
9003}
9004
9005/// Encode MVE 3-register bitwise instruction (VAND, VORR, VEOR, VBIC).
9006fn encode_mve_3reg_bitwise(base: u32, qd: &QReg, qn: &QReg, qm: &QReg) -> u32 {
9007 encode_mve_3reg(base, qd, qn, qm)
9008}
9009
9010/// Encode MVE VLDRW.32 Qd, [Rn, #offset]
9011/// Format: EC9x xxxx - contiguous load, word-sized elements
9012fn encode_mve_vldrw(qd: &QReg, addr: &MemAddr) -> u32 {
9013 let qd_enc = qreg_to_num(qd) * 2;
9014 let rn = reg_to_bits(&addr.base);
9015 let offset = addr.offset;
9016 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9017 let abs_offset = offset.unsigned_abs();
9018 let imm7 = (abs_offset / 4) & 0x7F; // 7-bit word-aligned offset
9019
9020 // VLDRW.32 Qd, [Rn, #imm]: ED10 xx80 variant
9021 0xED100E80
9022 | (u_bit << 23)
9023 | ((qd_enc >> 4) << 22)
9024 | (rn << 16)
9025 | ((qd_enc & 0xF) << 12)
9026 | (imm7 & 0x7F)
9027}
9028
9029/// Encode MVE VSTRW.32 Qd, [Rn, #offset]
9030fn encode_mve_vstrw(qd: &QReg, addr: &MemAddr) -> u32 {
9031 let qd_enc = qreg_to_num(qd) * 2;
9032 let rn = reg_to_bits(&addr.base);
9033 let offset = addr.offset;
9034 let u_bit = if offset >= 0 { 1u32 } else { 0u32 };
9035 let abs_offset = offset.unsigned_abs();
9036 let imm7 = (abs_offset / 4) & 0x7F;
9037
9038 0xED000E80
9039 | (u_bit << 23)
9040 | ((qd_enc >> 4) << 22)
9041 | (rn << 16)
9042 | ((qd_enc & 0xF) << 12)
9043 | (imm7 & 0x7F)
9044}
9045
9046impl ArmEncoder {
9047 /// Encode MVE constant load: MOVW+MOVT+VMOV for each 32-bit word, then assemble Q-register
9048 fn encode_thumb_mve_const(&self, qd: &QReg, bytes: &[u8; 16]) -> Result<Vec<u8>> {
9049 let mut result = Vec::new();
9050 let qd_num = qreg_to_num(qd);
9051
9052 // Load each 32-bit word into R12 (temp) then VMOV into S-register
9053 for i in 0..4 {
9054 let word = u32::from_le_bytes([
9055 bytes[i * 4],
9056 bytes[i * 4 + 1],
9057 bytes[i * 4 + 2],
9058 bytes[i * 4 + 3],
9059 ]);
9060 let lo16 = word & 0xFFFF;
9061 let hi16 = (word >> 16) & 0xFFFF;
9062
9063 // MOVW R12, #lo16
9064 result.extend_from_slice(&self.encode_thumb32_movw_raw(12, lo16)?);
9065 // MOVT R12, #hi16
9066 if hi16 != 0 {
9067 result.extend_from_slice(&self.encode_thumb32_movt_raw(12, hi16)?);
9068 }
9069
9070 // VMOV Sn, R12 where Sn = Qd*4 + i
9071 let s_num = qd_num * 4 + i as u32;
9072 let (vn, n) = encode_sreg(s_num);
9073 let vmov: u32 = 0xEE000A10 | (vn << 16) | (12 << 12) | (n << 7);
9074 result.extend_from_slice(&vfp_to_thumb_bytes(vmov));
9075 }
9076
9077 Ok(result)
9078 }
9079
9080 /// Encode lane-wise f32 binary operation (VDIV, etc.) via S-register extraction
9081 fn encode_thumb_mve_lane_wise_f32_binop(
9082 &self,
9083 qd: &QReg,
9084 qn: &QReg,
9085 qm: &QReg,
9086 vfp_base: u32,
9087 ) -> Result<Vec<u8>> {
9088 let mut result = Vec::new();
9089 let qd_num = qreg_to_num(qd);
9090 let qn_num = qreg_to_num(qn);
9091 let qm_num = qreg_to_num(qm);
9092
9093 // For each lane 0..3: use S-registers directly (Q aliasing)
9094 for i in 0..4u32 {
9095 let sd = qd_num * 4 + i;
9096 let sn = qn_num * 4 + i;
9097 let sm = qm_num * 4 + i;
9098
9099 let (vd, d) = encode_sreg(sd);
9100 let (vn, n) = encode_sreg(sn);
9101 let (vm, m) = encode_sreg(sm);
9102
9103 let instr = vfp_base | (d << 22) | (vn << 16) | (vd << 12) | (n << 7) | (m << 5) | vm;
9104 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9105 }
9106
9107 Ok(result)
9108 }
9109
9110 /// Encode lane-wise f32 VSQRT via S-register extraction
9111 fn encode_thumb_mve_lane_wise_f32_sqrt(&self, qd: &QReg, qm: &QReg) -> Result<Vec<u8>> {
9112 let mut result = Vec::new();
9113 let qd_num = qreg_to_num(qd);
9114 let qm_num = qreg_to_num(qm);
9115
9116 // VSQRT.F32 base: 0xEEB10AC0
9117 for i in 0..4u32 {
9118 let sd = qd_num * 4 + i;
9119 let sm = qm_num * 4 + i;
9120
9121 let (vd, d) = encode_sreg(sd);
9122 let (vm, m) = encode_sreg(sm);
9123
9124 let instr: u32 = 0xEEB10AC0 | (d << 22) | (vd << 12) | (m << 5) | vm;
9125 result.extend_from_slice(&vfp_to_thumb_bytes(instr));
9126 }
9127
9128 Ok(result)
9129 }
9130}
9131
9132/// VCR-TIER-001 (#1021/#1048) — the SCRATCH CONTRACT of an `ArmOp`'s encoder
9133/// expansion: the registers, beyond the op's declared RESULT registers and the
9134/// globally sanctioned R12/IP encoder scratch, that the expansion may leave
9135/// modified when it completes. Transient-but-restored traffic (push/pop through
9136/// the expansion's own stack red-zone, SP restored on exit) is not "modified".
9137///
9138/// This is the SINGLE declaration site for that contract — the one place a
9139/// future expansion that must borrow a register says so (the repo rule: one
9140/// declaration site, no duplicate copy that can silently drift). It is deliberately NOT derived
9141/// from the expansion's observed behavior: a contract read off the bytes would
9142/// rubber-stamp any clobber. Intent is declared here; the canary gate
9143/// (`scripts/repro/expansion_canary_gate_1021.py`) executes the REAL emitted
9144/// bytes of every variant the shipped rule table emits, on both backends, with
9145/// every non-contract register holding a distinctive canary, and fails on any
9146/// undeclared write — and on any declared register the expansion never
9147/// actually writes, so an over-broad declaration cannot hollow the gate.
9148///
9149/// The default is the STRICTEST reading — result registers only — which is
9150/// exactly the silent claim the atomic `ArmSemantics` pseudo-op model already
9151/// makes (#1021: an atomic model of a multi-instruction expansion is a silent
9152/// claim that the expansion is scratch-free). Today the table is EMPTY: #1039
9153/// reworked `i32.popcnt` off R11 (the linear-memory base) and #1048 reworked
9154/// the i64 shifts and bit-counts off their own operand registers, so every
9155/// expansion of every rule-emitted variant is R12-only on both Thumb-2 and
9156/// A32. Backend-independent for the same reason; if a backend's expansion ever
9157/// diverges, this signature grows a backend parameter in the same PR.
9158pub fn expansion_scratch_contract(op: &ArmOp) -> &'static [Reg] {
9159 // No variant currently borrows any register beyond R12. A new declaration
9160 // is added as a `match op { .. }` arm here — nowhere else.
9161 let _ = op;
9162 &[]
9163}
9164
9165#[cfg(test)]
9166mod tests {
9167 use super::*;
9168
9169 #[test]
9170 fn test_encoder_creation() {
9171 let encoder_arm = ArmEncoder::new_arm32();
9172 assert!(!encoder_arm.thumb_mode);
9173
9174 let encoder_thumb = ArmEncoder::new_thumb2();
9175 assert!(encoder_thumb.thumb_mode);
9176 }
9177
9178 /// #204 WAKE-path regression: `SetCond` materialized 0/1 with the 16-bit
9179 /// `MOVS Rd,#imm` (T1), whose Rd field is 3 bits (R0–R7). For a high Rd
9180 /// (R8–R12) `rd_bits << 8` overflows bit 11, flipping the opcode MOVS→CMP
9181 /// (`0x2c00`), so the boolean was never written — gale's `has_waiter` kept a
9182 /// stale value and the binary-sem WAKE dispatch read garbage. High Rd must
9183 /// use the 32-bit `MOV.W` (T2). Verify the bytes, not the IR.
9184 /// #311: the SAME high-Rd MOVS→CMP transmutation as #204, but in the
9185 /// i64 comparison expansions (I64SetCond / I64SetCondZ) — missed by the
9186 /// #204 hardening. With rd=R8 the boolean died in the flags
9187 /// (`ite eq; cmpeq r0,#1; cmpne r0,#0`), so gale's packed-u64 select
9188 /// read a stale register on silicon. High Rd must take MOV.W / CMP.W.
9189 #[test]
9190 fn test_encode_i64setcond_high_reg_uses_mov_w_311() {
9191 use synth_synthesis::{ArmOp, Condition, Reg};
9192 let enc = ArmEncoder::new_thumb2();
9193 let bytes = enc
9194 .encode(&ArmOp::I64SetCond {
9195 rd: Reg::R8,
9196 rn_lo: Reg::R2,
9197 rn_hi: Reg::R3,
9198 rm_lo: Reg::R6,
9199 rm_hi: Reg::R7,
9200 cond: Condition::EQ,
9201 })
9202 .unwrap();
9203 // The 32-bit MOV.W immediate (T2) first halfword is 0xF04F; the
9204 // 16-bit transmuted forms would contain 0x2801/0x2800 (CMP r0,#1/#0).
9205 let halfwords: Vec<u16> = bytes
9206 .chunks(2)
9207 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9208 .collect();
9209 assert!(
9210 halfwords.iter().filter(|&&h| h == 0xF04F).count() == 2,
9211 "high rd must use two MOV.W (T2) encodings, got {halfwords:04x?}"
9212 );
9213 assert!(
9214 !halfwords.contains(&0x2801) && !halfwords.contains(&0x2800),
9215 "no transmuted 16-bit CMP imm: {halfwords:04x?}"
9216 );
9217
9218 let bytes_z = enc
9219 .encode(&ArmOp::I64SetCondZ {
9220 rd: Reg::R8,
9221 rn_lo: Reg::R2,
9222 rn_hi: Reg::R3,
9223 })
9224 .unwrap();
9225 let hw_z: Vec<u16> = bytes_z
9226 .chunks(2)
9227 .map(|c| u16::from_le_bytes([c[0], c[1]]))
9228 .collect();
9229 assert!(
9230 hw_z.iter().filter(|&&h| h == 0xF04F).count() == 2,
9231 "SetCondZ high rd MOV.W: {hw_z:04x?}"
9232 );
9233 // CMP.W rd,#0 (T2) first halfword: 0xF1B0 | rd
9234 assert!(
9235 hw_z.contains(&(0xF1B0 | 8)),
9236 "SetCondZ high rd must use CMP.W: {hw_z:04x?}"
9237 );
9238 }
9239
9240 #[test]
9241 fn test_encode_setcond_high_reg_uses_mov_w_204() {
9242 use synth_synthesis::{ArmOp, Condition, Reg};
9243 let enc = ArmEncoder::new_thumb2();
9244 // R12 (high): must be ITE + MOV.W #1 + MOV.W #0, never a 16-bit MOVS/CMP.
9245 let hi = enc
9246 .encode(&ArmOp::SetCond {
9247 rd: Reg::R12,
9248 cond: Condition::NE,
9249 })
9250 .unwrap();
9251 assert_eq!(hi.len(), 10, "ITE(2) + MOV.W(4) + MOV.W(4): {hi:02x?}");
9252 // both value halfwords are MOV.W (0xF04F) — NOT the corrupt CMP (0x2c..).
9253 assert_eq!(&hi[2..4], &[0x4F, 0xF0], "then = MOV.W: {hi:02x?}");
9254 assert_eq!(&hi[6..8], &[0x4F, 0xF0], "else = MOV.W: {hi:02x?}");
9255 assert_eq!(hi[4] & 0x0F, 0x01, "then imm = #1");
9256 assert_eq!(hi[8] & 0x0F, 0x00, "else imm = #0");
9257 // Low Rd keeps the compact 16-bit MOVS form.
9258 let lo = enc
9259 .encode(&ArmOp::SetCond {
9260 rd: Reg::R0,
9261 cond: Condition::NE,
9262 })
9263 .unwrap();
9264 assert_eq!(lo.len(), 6, "ITE(2) + MOVS(2) + MOVS(2): {lo:02x?}");
9265 assert_eq!(lo[2..4], [0x01, 0x20], "then = MOVS R0,#1");
9266 assert_eq!(lo[4..6], [0x00, 0x20], "else = MOVS R0,#0");
9267 }
9268
9269 /// #209 Opt 1b: UMULL RdLo, RdHi, Rn, Rm encodes correctly on both ISAs.
9270 /// Thumb-2 T1: 1111 1011 1010 Rn | RdLo RdHi 0000 Rm.
9271 /// A32: cond 0000 1000 RdHi RdLo Rm 1001 Rn.
9272 #[test]
9273 fn test_encode_umull_209b() {
9274 use synth_synthesis::{ArmOp, Reg};
9275 let op = ArmOp::Umull {
9276 rdlo: Reg::R4,
9277 rdhi: Reg::R5,
9278 rn: Reg::R0,
9279 rm: Reg::R3,
9280 };
9281 // Thumb-2: hw1 = 0xFBA0 | 0 = 0xFBA0; hw2 = (4<<12)|(5<<8)|3 = 0x4503.
9282 let t = ArmEncoder::new_thumb2().encode(&op).unwrap();
9283 assert_eq!(
9284 t,
9285 vec![0xA0, 0xFB, 0x03, 0x45],
9286 "umull r4,r5,r0,r3 (T2): {t:02x?}"
9287 );
9288 // A32: 0xE0800090 | (5<<16) | (4<<12) | (3<<8) | 0 = 0xE0854390.
9289 let a = ArmEncoder::new_arm32().encode(&op).unwrap();
9290 assert_eq!(
9291 a,
9292 0xE085_4390u32.to_le_bytes().to_vec(),
9293 "umull (A32): {a:02x?}"
9294 );
9295 }
9296
9297 /// #206 regression: the ARM32 (A32) `Ldr`/`Str` encoders fed `addr` through
9298 /// `encode_mem_addr`, which returns only the 12-bit immediate — so a register
9299 /// offset (`[rn, rm, #off]`) was silently dropped to `[rn, #off]`, sending
9300 /// the access to the wrong runtime address (silent miscompile on the default
9301 /// `--target arm`). A register offset must materialize `ip = rn + rm` and
9302 /// load from `[ip, #off]`. Verify the bytes.
9303 #[test]
9304 fn test_encode_arm32_indexed_load_keeps_index_206() {
9305 use synth_synthesis::{ArmOp, MemAddr, Reg};
9306 let enc = ArmEncoder::new_arm32();
9307 // ldr r0, [r11, r1, #8] must NOT collapse to a single immediate ldr.
9308 let bytes = enc
9309 .encode(&ArmOp::Ldr {
9310 rd: Reg::R0,
9311 addr: MemAddr::reg_imm(Reg::R11, Reg::R1, 8),
9312 })
9313 .unwrap();
9314 assert_eq!(
9315 bytes.len(),
9316 8,
9317 "expected ADD ip + LDR (2 words): {bytes:02x?}"
9318 );
9319 let add = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9320 let ldr = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9321 // ADD ip, r11, r1 = 0xE08BC001
9322 assert_eq!(add, 0xE08B_C001, "ADD ip,r11,r1: {add:#010x}");
9323 // LDR r0, [ip, #8] = 0xE59C0008
9324 assert_eq!(ldr, 0xE59C_0008, "LDR r0,[ip,#8]: {ldr:#010x}");
9325 // A bare immediate ldr (the bug) would be 0xE59B0008 (base=r11) — reject.
9326 assert_ne!(ldr, 0xE59B_0008, "index must not be dropped");
9327 }
9328
9329 /// #594 regression: `call_indirect` on the A32 path (`--target cortex-r5`)
9330 /// was encoded as a literal NOP (0xE1A00000) — the call never happened and
9331 /// the function silently returned the leftover table-index value. The A32
9332 /// encoder must emit a real dispatch expansion, since #642 guarded by an
9333 /// inline bounds check:
9334 /// `MOVW r12, #size; CMP idx, r12; BLO +1; UDF;
9335 /// MOV r12, idx, LSL #2; LDR r12, [r11, r12]; BLX r12`.
9336 #[test]
9337 fn test_encode_arm32_call_indirect_is_real_call_594() {
9338 use synth_synthesis::{ArmOp, Reg};
9339 let enc = ArmEncoder::new_arm32();
9340 let bytes = enc
9341 .encode(&ArmOp::CallIndirect {
9342 rd: Reg::R0,
9343 type_idx: 0,
9344 table_index_reg: Reg::R0,
9345 table_size: 4,
9346 table_byte_offset: 0,
9347 null_check: false,
9348 type_check: None,
9349 })
9350 .unwrap();
9351 assert_eq!(
9352 bytes.len(),
9353 28,
9354 "expected MOVW + CMP + BLO + UDF + MOV + LDR + BLX (7 words): {bytes:02x?}"
9355 );
9356 let words: Vec<u32> = bytes
9357 .as_chunks::<4>()
9358 .0
9359 .iter()
9360 .map(|&w| u32::from_le_bytes(w))
9361 .collect();
9362 // #642 bounds guard: MOVW r12, #4; CMP r0, r12; BLO +1; UDF
9363 assert_eq!(words[0], 0xE300_C004, "MOVW r12,#4: {:#010x}", words[0]);
9364 assert_eq!(words[1], 0xE150_000C, "CMP r0,r12: {:#010x}", words[1]);
9365 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9366 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9367 // MOV r12, r0, LSL #2 = 0xE1A0C100
9368 assert_eq!(
9369 words[4], 0xE1A0_C100,
9370 "MOV r12,r0,LSL#2: {:#010x}",
9371 words[4]
9372 );
9373 // LDR r12, [r11, r12] = 0xE79BC00C
9374 assert_eq!(
9375 words[5], 0xE79B_C00C,
9376 "LDR r12,[r11,r12]: {:#010x}",
9377 words[5]
9378 );
9379 // BLX r12 = 0xE12FFF3C
9380 assert_eq!(words[6], 0xE12F_FF3C, "BLX r12: {:#010x}", words[6]);
9381 // The bug: a single NOP word. Must never come back.
9382 assert!(
9383 !bytes
9384 .as_chunks::<4>()
9385 .0
9386 .iter()
9387 .any(|&w| w == 0xE1A0_0000u32.to_le_bytes()),
9388 "call_indirect must not contain a NOP (#594): {bytes:02x?}"
9389 );
9390
9391 // A non-R0 index register lands in the MOV's Rm and CMP's Rn fields.
9392 let bytes = enc
9393 .encode(&ArmOp::CallIndirect {
9394 rd: Reg::R0,
9395 type_idx: 0,
9396 table_index_reg: Reg::R4,
9397 table_size: 4,
9398 table_byte_offset: 0,
9399 null_check: false,
9400 type_check: None,
9401 })
9402 .unwrap();
9403 let cmp = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9404 assert_eq!(cmp, 0xE154_000C, "CMP r4,r12: {cmp:#010x}");
9405 let mov = u32::from_le_bytes(bytes[16..20].try_into().unwrap());
9406 assert_eq!(mov, 0xE1A0_C104, "MOV r12,r4,LSL#2: {mov:#010x}");
9407 }
9408
9409 /// #642: a table size above 16 bits must not be silently truncated by the
9410 /// MOVW — the A32 guard adds a MOVT for the high half.
9411 #[test]
9412 fn test_encode_arm32_call_indirect_wide_table_size_642() {
9413 use synth_synthesis::{ArmOp, Reg};
9414 let enc = ArmEncoder::new_arm32();
9415 let bytes = enc
9416 .encode(&ArmOp::CallIndirect {
9417 rd: Reg::R0,
9418 type_idx: 0,
9419 table_index_reg: Reg::R0,
9420 table_size: 0x0002_0003,
9421 table_byte_offset: 0,
9422 null_check: false,
9423 type_check: None,
9424 })
9425 .unwrap();
9426 assert_eq!(bytes.len(), 32, "MOVT arm adds one word: {bytes:02x?}");
9427 let movw = u32::from_le_bytes(bytes[0..4].try_into().unwrap());
9428 let movt = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
9429 assert_eq!(movw, 0xE300_C003, "MOVW r12,#3: {movw:#010x}");
9430 assert_eq!(movt, 0xE340_C002, "MOVT r12,#2: {movt:#010x}");
9431 }
9432
9433 /// #597 anchor (justified correctness RE-PIN of the #594-era freeze): the
9434 /// Thumb-2 `CallIndirect` expansion is `mov.w ip, rm, LSL #2; ldr.w ip,
9435 /// [r11, ip]; blx ip`.
9436 ///
9437 /// The #594 PR froze the then-current bytes `4F EA 20 0C ...` whose first
9438 /// word decodes as `mov.w ip, rm, ASR #32` — the intended `LSL #2` had
9439 /// its shift amount in the TYPE field (bits 5:4) instead of imm2 (bits
9440 /// 7:6), so the index was destroyed and every call_indirect dispatched
9441 /// table entry 0 (shipped miscompile, masked by index-0 probes). #597
9442 /// corrects the encoding; new bytes `4F EA 80 0C ...` were
9443 /// execution-validated under unicorn against the wasmtime oracle on a
9444 /// multi-entry table (indexes 0, 1, 3 —
9445 /// scripts/repro/call_indirect_597_differential.py) before this pin was
9446 /// replaced. Old pin: [4F EA 20 0C, 5B F8 0C C0, E0 47] (ASR #32 — must
9447 /// never come back).
9448 #[test]
9449 fn test_encode_thumb_call_indirect_lsl2_597() {
9450 use synth_synthesis::{ArmOp, Reg};
9451 let enc = ArmEncoder::new_thumb2();
9452 let bytes = enc
9453 .encode(&ArmOp::CallIndirect {
9454 rd: Reg::R0,
9455 type_idx: 0,
9456 table_index_reg: Reg::R0,
9457 table_size: 4,
9458 table_byte_offset: 0,
9459 null_check: false,
9460 type_check: None,
9461 })
9462 .unwrap();
9463 assert_eq!(
9464 bytes,
9465 vec![
9466 // #642 bounds guard: movw ip,#4; cmp r0,ip; blo +1; udf #0
9467 0x40, 0xF2, 0x04, 0x0C, // movw ip, #4
9468 0x60, 0x45, // cmp r0, ip
9469 0x00, 0xD3, // blo .+4 (skip the udf)
9470 0x00, 0xDE, // udf #0 — OOB index trap (WASM §4.4.8)
9471 // #597-pinned dispatch
9472 0x4F, 0xEA, 0x80, 0x0C, // mov.w ip, r0, lsl #2
9473 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9474 0xE0, 0x47, // blx ip
9475 ],
9476 "Thumb-2 CallIndirect: bounds guard + mov.w/ldr.w/blx dispatch: {bytes:02x?}"
9477 );
9478 // The #597 bug bytes (ASR #32 dispatch first word) must never come back.
9479 assert!(
9480 !bytes.windows(4).any(|w| w == [0x4F, 0xEA, 0x20, 0x0C]),
9481 "mov.w ip, rm, ASR #32 — the #597 type-field bug"
9482 );
9483
9484 // A non-R0 index register lands in the mov.w's Rm field (hw2 bits 3:0)
9485 // and the cmp's Rn field.
9486 let bytes = enc
9487 .encode(&ArmOp::CallIndirect {
9488 rd: Reg::R0,
9489 type_idx: 0,
9490 table_index_reg: Reg::R4,
9491 table_size: 4,
9492 table_byte_offset: 0,
9493 null_check: false,
9494 type_check: None,
9495 })
9496 .unwrap();
9497 assert_eq!(&bytes[4..6], &[0x64, 0x45], "cmp r4, ip: {bytes:02x?}");
9498 assert_eq!(
9499 &bytes[10..14],
9500 &[0x4F, 0xEA, 0x84, 0x0C],
9501 "mov.w ip, r4, LSL #2: {bytes:02x?}"
9502 );
9503 }
9504
9505 /// #642: the Thumb-2 bounds guard for a high-register index (R8 — the top
9506 /// of the allocatable pool) uses the high-reg-capable 16-bit CMP (T2) with
9507 /// the N bit set; a table size above 16 bits adds a MOVT.
9508 #[test]
9509 fn test_encode_thumb_call_indirect_guard_shapes_642() {
9510 use synth_synthesis::{ArmOp, Reg};
9511 let enc = ArmEncoder::new_thumb2();
9512 let bytes = enc
9513 .encode(&ArmOp::CallIndirect {
9514 rd: Reg::R0,
9515 type_idx: 0,
9516 table_index_reg: Reg::R8,
9517 table_size: 3,
9518 table_byte_offset: 0,
9519 null_check: false,
9520 type_check: None,
9521 })
9522 .unwrap();
9523 // cmp r8, ip — T2: 0x4500 | N(1)<<7 | Rm(12)<<3 | Rn(0) = 0x45E0
9524 assert_eq!(&bytes[4..6], &[0xE0, 0x45], "cmp r8, ip: {bytes:02x?}");
9525
9526 let bytes = enc
9527 .encode(&ArmOp::CallIndirect {
9528 rd: Reg::R0,
9529 type_idx: 0,
9530 table_index_reg: Reg::R0,
9531 table_size: 0x0002_0003,
9532 table_byte_offset: 0,
9533 null_check: false,
9534 type_check: None,
9535 })
9536 .unwrap();
9537 // movw ip,#3 then movt ip,#2 — the size must not be truncated.
9538 assert_eq!(
9539 &bytes[0..8],
9540 &[0x40, 0xF2, 0x03, 0x0C, 0xC0, 0xF2, 0x02, 0x0C],
9541 "movw ip,#3; movt ip,#2: {bytes:02x?}"
9542 );
9543 }
9544
9545 /// #650: a non-zero table base offset (table N of the contiguous R11
9546 /// region) routes the Thumb-2 pointer load through
9547 /// `add.w ip, r11, ip; ldr.w ip, [ip, #offset]` — and offset 0 keeps the
9548 /// pre-#650 single-load bytes IDENTICAL (the by-construction pin).
9549 #[test]
9550 fn test_encode_thumb_call_indirect_table_offset_650() {
9551 use synth_synthesis::{ArmOp, Reg};
9552 let enc = ArmEncoder::new_thumb2();
9553 // falcon's fused-component shape: table 0 has 7 entries, so table 1
9554 // sits at byte offset 28.
9555 let bytes = enc
9556 .encode(&ArmOp::CallIndirect {
9557 rd: Reg::R0,
9558 type_idx: 0,
9559 table_index_reg: Reg::R1,
9560 table_size: 41,
9561 table_byte_offset: 28,
9562 null_check: false,
9563 type_check: None,
9564 })
9565 .unwrap();
9566 assert_eq!(
9567 bytes,
9568 vec![
9569 // #642 bounds guard against TABLE 1's OWN size (41)
9570 0x40, 0xF2, 0x29, 0x0C, // movw ip, #41
9571 0x61, 0x45, // cmp r1, ip
9572 0x00, 0xD3, // blo .+4 (skip the udf)
9573 0x00, 0xDE, // udf #0 — OOB trap (WASM §4.4.8)
9574 // dispatch through table 1's base (R11 + 28)
9575 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9576 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9577 0xDC, 0xF8, 0x1C, 0xC0, // ldr.w ip, [ip, #28]
9578 0xE0, 0x47, // blx ip
9579 ],
9580 "Thumb-2 table-1 dispatch (#650): {bytes:02x?}"
9581 );
9582
9583 // Offset 0 must stay the #597-pinned single-load form (no add.w, no
9584 // imm-form ldr) — single-table byte identity by construction.
9585 let zero = enc
9586 .encode(&ArmOp::CallIndirect {
9587 rd: Reg::R0,
9588 type_idx: 0,
9589 table_index_reg: Reg::R1,
9590 table_size: 41,
9591 table_byte_offset: 0,
9592 null_check: false,
9593 type_check: None,
9594 })
9595 .unwrap();
9596 assert_eq!(
9597 &zero[10..],
9598 &[
9599 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9600 0x5B, 0xF8, 0x0C, 0xC0, // ldr.w ip, [r11, ip]
9601 0xE0, 0x47, // blx ip
9602 ],
9603 "offset 0 keeps the pre-#650 dispatch bytes: {zero:02x?}"
9604 );
9605 }
9606
9607 /// #650: the A32 twin — `add r12, r11, r12; ldr r12, [r12, #offset]` for
9608 /// a non-zero table base offset; offset 0 keeps the #594/#642 form.
9609 #[test]
9610 fn test_encode_arm32_call_indirect_table_offset_650() {
9611 use synth_synthesis::{ArmOp, Reg};
9612 let enc = ArmEncoder::new_arm32();
9613 let bytes = enc
9614 .encode(&ArmOp::CallIndirect {
9615 rd: Reg::R0,
9616 type_idx: 0,
9617 table_index_reg: Reg::R1,
9618 table_size: 41,
9619 table_byte_offset: 28,
9620 null_check: false,
9621 type_check: None,
9622 })
9623 .unwrap();
9624 let words: Vec<u32> = bytes
9625 .as_chunks::<4>()
9626 .0
9627 .iter()
9628 .map(|&w| u32::from_le_bytes(w))
9629 .collect();
9630 assert_eq!(words[0], 0xE300_C029, "MOVW r12,#41: {:#010x}", words[0]);
9631 assert_eq!(words[1], 0xE151_000C, "CMP r1,r12: {:#010x}", words[1]);
9632 assert_eq!(words[2], 0x3A00_0000, "BLO +1 insn: {:#010x}", words[2]);
9633 assert_eq!(words[3], 0xE7F0_00F0, "UDF: {:#010x}", words[3]);
9634 assert_eq!(
9635 words[4], 0xE1A0_C101,
9636 "MOV r12,r1,LSL#2: {:#010x}",
9637 words[4]
9638 );
9639 assert_eq!(
9640 words[5], 0xE08B_C00C,
9641 "ADD r12,r11,r12 (#650): {:#010x}",
9642 words[5]
9643 );
9644 assert_eq!(
9645 words[6], 0xE59C_C01C,
9646 "LDR r12,[r12,#28] (#650): {:#010x}",
9647 words[6]
9648 );
9649 assert_eq!(words[7], 0xE12F_FF3C, "BLX r12: {:#010x}", words[7]);
9650 }
9651
9652 /// #664: `null_check` inserts a null-funcref trap between the Thumb-2
9653 /// pointer load and the `BLX` (`cmp.w ip, #0; bne .+4; udf #0`) — a
9654 /// zero-linked (uninitialized) slot must TRAP (WASM §4.4.8), never
9655 /// branch to address 0. `null_check: false` keeps the expansion
9656 /// byte-identical to the pre-#664 form (by-construction pin).
9657 #[test]
9658 fn test_encode_thumb_call_indirect_null_check_664() {
9659 use synth_synthesis::{ArmOp, Reg};
9660 let enc = ArmEncoder::new_thumb2();
9661 let op = |null_check| ArmOp::CallIndirect {
9662 rd: Reg::R0,
9663 type_idx: 0,
9664 table_index_reg: Reg::R1,
9665 table_size: 4,
9666 table_byte_offset: 0,
9667 null_check,
9668 type_check: None,
9669 };
9670 let with = enc.encode(&op(true)).unwrap();
9671 let without = enc.encode(&op(false)).unwrap();
9672 // The checked form = the unchecked form with EXACTLY the three-insn
9673 // null check spliced in before the final BLX (byte identity of the
9674 // shared prefix/suffix — nothing else may move).
9675 assert_eq!(
9676 with.len(),
9677 without.len() + 8,
9678 "cmp.w (4) + bne (2) + udf (2): {with:02x?}"
9679 );
9680 let blx_at = without.len() - 2;
9681 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9682 assert_eq!(
9683 &with[blx_at..],
9684 &[
9685 0xBC, 0xF1, 0x00, 0x0F, // cmp.w ip, #0
9686 0x00, 0xD1, // bne .+4 (skip the udf)
9687 0x00, 0xDE, // udf #0 — null-funcref trap (#664)
9688 0xE0, 0x47, // blx ip
9689 ],
9690 "null check precedes the BLX: {with:02x?}"
9691 );
9692 assert_eq!(&with[with.len() - 2..], &without[blx_at..], "same BLX");
9693 }
9694
9695 /// #664: the A32 twin — `cmp r12, #0; bne .+8; udf` before the `BLX`;
9696 /// `null_check: false` keeps the #594/#642/#650 bytes identical.
9697 #[test]
9698 fn test_encode_arm32_call_indirect_null_check_664() {
9699 use synth_synthesis::{ArmOp, Reg};
9700 let enc = ArmEncoder::new_arm32();
9701 let op = |null_check| ArmOp::CallIndirect {
9702 rd: Reg::R0,
9703 type_idx: 0,
9704 table_index_reg: Reg::R1,
9705 table_size: 4,
9706 table_byte_offset: 0,
9707 null_check,
9708 type_check: None,
9709 };
9710 let with = enc.encode(&op(true)).unwrap();
9711 let without = enc.encode(&op(false)).unwrap();
9712 assert_eq!(with.len(), without.len() + 12, "3 A32 words: {with:02x?}");
9713 let blx_at = without.len() - 4;
9714 assert_eq!(&with[..blx_at], &without[..blx_at], "shared prefix");
9715 let words: Vec<u32> = with[blx_at..]
9716 .as_chunks::<4>()
9717 .0
9718 .iter()
9719 .map(|&w| u32::from_le_bytes(w))
9720 .collect();
9721 assert_eq!(words[0], 0xE35C_0000, "CMP r12,#0: {:#010x}", words[0]);
9722 assert_eq!(words[1], 0x1A00_0000, "BNE +1 insn: {:#010x}", words[1]);
9723 assert_eq!(words[2], 0xE7F0_00F0, "UDF (null trap): {:#010x}", words[2]);
9724 assert_eq!(words[3], 0xE12F_FF3C, "BLX r12: {:#010x}", words[3]);
9725 }
9726
9727 /// #676: `type_check` splices the runtime type check — scale the index,
9728 /// load the slot's structural class id from the type-id sidecar
9729 /// (`ldr.w ip, [ip, #type_off]`), compare against the expected class id
9730 /// and trap on mismatch (WASM §4.4.8) — between the bounds guard and
9731 /// the dispatch tail. `type_check: None` keeps the expansion
9732 /// byte-identical to the pre-#676 form (by-construction pin, the same
9733 /// trick as #650 offset-0 / #664 `null_check: false`).
9734 #[test]
9735 fn test_encode_thumb_call_indirect_type_check_676() {
9736 use synth_synthesis::{ArmOp, Reg};
9737 let enc = ArmEncoder::new_thumb2();
9738 let op = |type_check| ArmOp::CallIndirect {
9739 rd: Reg::R0,
9740 type_idx: 1,
9741 table_index_reg: Reg::R1,
9742 table_size: 5,
9743 table_byte_offset: 0,
9744 null_check: false,
9745 type_check,
9746 };
9747 let with = enc.encode(&op(Some((2, 20)))).unwrap();
9748 let without = enc.encode(&op(None)).unwrap();
9749 // The checked form = the unchecked form with EXACTLY the six-insn
9750 // type check spliced in after the bounds guard (byte identity of
9751 // the shared prefix/suffix — nothing else may move).
9752 assert_eq!(
9753 with.len(),
9754 without.len() + 20,
9755 "lsl.w(4)+add.w(4)+ldr.w(4)+cmp.w(4)+beq(2)+udf(2): {with:02x?}"
9756 );
9757 // Bounds guard: movw(4) + cmp(2) + blo(2) + udf(2) = 10 bytes.
9758 let guard_end = 10;
9759 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9760 assert_eq!(
9761 &with[guard_end..guard_end + 20],
9762 &[
9763 0x4F, 0xEA, 0x81, 0x0C, // mov.w ip, r1, lsl #2
9764 0x0B, 0xEB, 0x0C, 0x0C, // add.w ip, r11, ip
9765 0xDC, 0xF8, 0x14, 0xC0, // ldr.w ip, [ip, #20] — sidecar slot id
9766 0xBC, 0xF1, 0x02, 0x0F, // cmp.w ip, #2 — expected class id
9767 0x00, 0xD0, // beq .+4 (skip the udf on a match)
9768 0x00, 0xDE, // udf #0 — §4.4.8 type-mismatch trap (#676)
9769 ],
9770 "type check follows the bounds guard: {with:02x?}"
9771 );
9772 assert_eq!(
9773 &with[guard_end + 20..],
9774 &without[guard_end..],
9775 "dispatch tail unchanged (idx*4 recomputed)"
9776 );
9777 }
9778
9779 /// #676: the A32 twin — `mov r12, idx, lsl #2; add r12, r11, r12;
9780 /// ldr r12, [r12, #type_off]; cmp r12, #id; beq .+8; udf` after the
9781 /// bounds guard; `type_check: None` keeps the #594/#642/#650/#664
9782 /// bytes identical.
9783 #[test]
9784 fn test_encode_arm32_call_indirect_type_check_676() {
9785 use synth_synthesis::{ArmOp, Reg};
9786 let enc = ArmEncoder::new_arm32();
9787 let op = |type_check| ArmOp::CallIndirect {
9788 rd: Reg::R0,
9789 type_idx: 1,
9790 table_index_reg: Reg::R1,
9791 table_size: 5,
9792 table_byte_offset: 0,
9793 null_check: false,
9794 type_check,
9795 };
9796 let with = enc.encode(&op(Some((2, 20)))).unwrap();
9797 let without = enc.encode(&op(None)).unwrap();
9798 assert_eq!(with.len(), without.len() + 24, "6 A32 words: {with:02x?}");
9799 // Bounds guard: movw + cmp + blo + udf = 4 words = 16 bytes.
9800 let guard_end = 16;
9801 assert_eq!(&with[..guard_end], &without[..guard_end], "shared guard");
9802 let words: Vec<u32> = with[guard_end..guard_end + 24]
9803 .as_chunks::<4>()
9804 .0
9805 .iter()
9806 .map(|&w| u32::from_le_bytes(w))
9807 .collect();
9808 assert_eq!(
9809 words[0], 0xE1A0_C101,
9810 "MOV r12,r1,LSL#2: {:#010x}",
9811 words[0]
9812 );
9813 assert_eq!(words[1], 0xE08B_C00C, "ADD r12,r11,r12: {:#010x}", words[1]);
9814 assert_eq!(
9815 words[2], 0xE59C_C014,
9816 "LDR r12,[r12,#20] (sidecar): {:#010x}",
9817 words[2]
9818 );
9819 assert_eq!(
9820 words[3], 0xE35C_0002,
9821 "CMP r12,#2 (expected class id): {:#010x}",
9822 words[3]
9823 );
9824 assert_eq!(words[4], 0x0A00_0000, "BEQ +1 insn: {:#010x}", words[4]);
9825 assert_eq!(
9826 words[5], 0xE7F0_00F0,
9827 "UDF (type-mismatch trap): {:#010x}",
9828 words[5]
9829 );
9830 assert_eq!(
9831 &with[guard_end + 24..],
9832 &without[guard_end..],
9833 "dispatch tail unchanged"
9834 );
9835 }
9836
9837 /// #178/#180 regression: the Thumb `Add`/`Adds`/`Subs` reg-forms used the
9838 /// 16-bit encoding unconditionally. For high registers (R12 base scratch,
9839 /// R8-R11 i64 pairs) the 3-bit register fields overflow and corrupt the
9840 /// operands — `add ip,ip,r0` came out as `adds r4,r5,r1` (0x186C), silently
9841 /// dropping the address operand and miscompiling every optimized memory
9842 /// access. High registers must use the 32-bit `.W` forms.
9843 #[test]
9844 fn test_encode_thumb_add_high_reg_uses_add_w_178_180() {
9845 let encoder = ArmEncoder::new_thumb2();
9846
9847 // add ip, ip, r0 — the exact MemLoad/MemStore base+addr op.
9848 let code = encoder
9849 .encode(&ArmOp::Add {
9850 rd: Reg::R12,
9851 rn: Reg::R12,
9852 op2: Operand2::Reg(Reg::R0),
9853 })
9854 .unwrap();
9855 // ADD.W ip, ip, r0 = EB0C 0C00 (little-endian halfwords).
9856 assert_eq!(
9857 code,
9858 vec![0x0C, 0xEB, 0x00, 0x0C],
9859 "high-reg Thumb ADD must be 32-bit ADD.W (EB0C 0C00), not corrupt 16-bit; got {code:02X?}"
9860 );
9861 // Must NOT be the buggy 16-bit 0x186C (`adds r4,r5,r1`).
9862 assert_ne!(code, vec![0x6C, 0x18], "regressed to corrupt 16-bit ADDS");
9863
9864 // Low-register add stays 16-bit (no regression for the common case).
9865 let lo = encoder
9866 .encode(&ArmOp::Add {
9867 rd: Reg::R1,
9868 rn: Reg::R2,
9869 op2: Operand2::Reg(Reg::R3),
9870 })
9871 .unwrap();
9872 assert_eq!(
9873 lo.len(),
9874 2,
9875 "low-reg ADD should remain 16-bit, got {lo:02X?}"
9876 );
9877 }
9878
9879 /// #178/#180 sibling: i64 low-word `Adds`/`Subs` can land in R8-R11 pairs;
9880 /// those must fall back to 32-bit ADDS.W/SUBS.W (flag-setting preserved).
9881 #[test]
9882 fn test_encode_thumb_adds_subs_high_reg_use_32bit_178_180() {
9883 let encoder = ArmEncoder::new_thumb2();
9884
9885 // adds r10, r10, r8 → ADDS.W = EB1A 0A08
9886 let adds = encoder
9887 .encode(&ArmOp::Adds {
9888 rd: Reg::R10,
9889 rn: Reg::R10,
9890 op2: Operand2::Reg(Reg::R8),
9891 })
9892 .unwrap();
9893 assert_eq!(
9894 adds,
9895 vec![0x1A, 0xEB, 0x08, 0x0A],
9896 "high-reg ADDS must be 32-bit ADDS.W (EB1A 0A08); got {adds:02X?}"
9897 );
9898
9899 // subs r10, r10, r8 → SUBS.W = EBBA 0A08
9900 let subs = encoder
9901 .encode(&ArmOp::Subs {
9902 rd: Reg::R10,
9903 rn: Reg::R10,
9904 op2: Operand2::Reg(Reg::R8),
9905 })
9906 .unwrap();
9907 assert_eq!(
9908 subs,
9909 vec![0xBA, 0xEB, 0x08, 0x0A],
9910 "high-reg SUBS must be 32-bit SUBS.W (EBBA 0A08); got {subs:02X?}"
9911 );
9912 }
9913
9914 /// #184 (sibling of #180): 16-bit CMN (T1) only encodes R0-R7. High registers
9915 /// must use 32-bit CMN.W, not the corrupt truncated 16-bit form.
9916 #[test]
9917 fn test_encode_thumb_cmn_high_reg_uses_cmn_w_184() {
9918 let encoder = ArmEncoder::new_thumb2();
9919
9920 // cmn r10, r8 → CMN.W = EB1A 0F08 (ADD.W S=1, Rd=PC discarded).
9921 let cmn = encoder
9922 .encode(&ArmOp::Cmn {
9923 rn: Reg::R10,
9924 op2: Operand2::Reg(Reg::R8),
9925 })
9926 .unwrap();
9927 assert_eq!(
9928 cmn,
9929 vec![0x1A, 0xEB, 0x08, 0x0F],
9930 "high-reg CMN must be 32-bit CMN.W (EB1A 0F08); got {cmn:02X?}"
9931 );
9932
9933 // Low registers stay 16-bit: cmn r1, r2 = 0x42D1.
9934 let lo = encoder
9935 .encode(&ArmOp::Cmn {
9936 rn: Reg::R1,
9937 op2: Operand2::Reg(Reg::R2),
9938 })
9939 .unwrap();
9940 assert_eq!(
9941 lo.len(),
9942 2,
9943 "low-reg CMN should remain 16-bit, got {lo:02X?}"
9944 );
9945 assert_eq!(lo, vec![0xD1, 0x42], "low-reg CMN bytes wrong: {lo:02X?}");
9946 }
9947
9948 /// #185 regression: feeding PC (R15) as a data operand to a Thumb-2 op that
9949 /// guards its registers must return Err, not panic under debug-assertions.
9950 /// (Synth never emits PC here; the fuzz harness requires encode() be total.)
9951 #[test]
9952 fn test_encode_pc_operand_returns_err_not_panic_185() {
9953 let encoder = ArmEncoder::new_thumb2();
9954 for op in [
9955 ArmOp::Sdiv {
9956 rd: Reg::PC,
9957 rn: Reg::R0,
9958 rm: Reg::R1,
9959 },
9960 ArmOp::Udiv {
9961 rd: Reg::R0,
9962 rn: Reg::PC,
9963 rm: Reg::R1,
9964 },
9965 ArmOp::Sdiv {
9966 rd: Reg::R0,
9967 rn: Reg::R1,
9968 rm: Reg::PC,
9969 },
9970 ] {
9971 let r = encoder.encode(&op);
9972 assert!(
9973 r.is_err(),
9974 "encode({op:?}) must return Err for a PC operand, got {r:?}"
9975 );
9976 }
9977 // Valid registers still encode fine (no false rejection).
9978 assert!(
9979 encoder
9980 .encode(&ArmOp::Sdiv {
9981 rd: Reg::R0,
9982 rn: Reg::R1,
9983 rm: Reg::R2
9984 })
9985 .is_ok()
9986 );
9987 }
9988
9989 #[test]
9990 fn test_encode_nop_arm32() {
9991 let encoder = ArmEncoder::new_arm32();
9992 let code = encoder.encode(&ArmOp::Nop).unwrap();
9993
9994 assert_eq!(code.len(), 4); // ARM32 instructions are 4 bytes
9995 assert_eq!(code, vec![0x00, 0x00, 0xA0, 0xE1]); // MOV R0, R0
9996 }
9997
9998 #[test]
9999 fn test_encode_nop_thumb() {
10000 let encoder = ArmEncoder::new_thumb2();
10001 let code = encoder.encode(&ArmOp::Nop).unwrap();
10002
10003 assert_eq!(code.len(), 2); // Thumb instructions are 2 bytes
10004 assert_eq!(code, vec![0x00, 0xBF]); // NOP
10005 }
10006
10007 #[test]
10008 fn test_encode_mov_immediate_arm32() {
10009 let encoder = ArmEncoder::new_arm32();
10010 let op = ArmOp::Mov {
10011 rd: Reg::R0,
10012 op2: Operand2::Imm(42),
10013 };
10014
10015 let code = encoder.encode(&op).unwrap();
10016 assert_eq!(code.len(), 4);
10017
10018 // Verify it's a MOV instruction (bits should have immediate flag set)
10019 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10020 assert_eq!(instr & 0x0E000000, 0x02000000); // Check I bit is set
10021 }
10022
10023 #[test]
10024 fn test_encode_add_registers_arm32() {
10025 let encoder = ArmEncoder::new_arm32();
10026 let op = ArmOp::Add {
10027 rd: Reg::R0,
10028 rn: Reg::R1,
10029 op2: Operand2::Reg(Reg::R2),
10030 };
10031
10032 let code = encoder.encode(&op).unwrap();
10033 assert_eq!(code.len(), 4);
10034
10035 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10036 // Verify it's an ADD instruction with correct opcode
10037 assert_eq!(instr & 0x0FE00000, 0x00800000);
10038 }
10039
10040 /// #350 — `encode_thumb32_add_imm` must lower an out-of-range immediate
10041 /// (> 0xFFF) to a legal MOVW(/MOVT) + ADD.W-register sequence instead of
10042 /// erroring. The small-imm fast path (imm <= 0xFFF) stays byte-identical.
10043 #[test]
10044 fn test_encode_add_imm_large_350() {
10045 let enc = ArmEncoder::new_thumb2();
10046
10047 // --- Fast path: imm <= 0xFFF is a single 4-byte instruction, and the
10048 // VALUE must be right (#681: this test used to assert only the length,
10049 // letting the raw-packed T3 mis-encoding of 0x123 pass CI). 0x123 is
10050 // not ThumbExpandImm-representable, so it must be ADDW (T4, plain
10051 // imm12): clang `addw r0, r1, #0x123` = f201 0023.
10052 let small = enc
10053 .encode_thumb32_add_imm(&Reg::R0, &Reg::R1, 0x123)
10054 .unwrap();
10055 assert_eq!(small, vec![0x01, 0xF2, 0x23, 0x10], "ADDW r0, r1, #0x123");
10056
10057 // helper: decode a Thumb-2 MOVW/MOVT halfword pair back to its imm16
10058 fn movx_imm16(b: &[u8]) -> u32 {
10059 let hw1 = u16::from_le_bytes([b[0], b[1]]) as u32;
10060 let hw2 = u16::from_le_bytes([b[2], b[3]]) as u32;
10061 let imm4 = hw1 & 0xF;
10062 let i = (hw1 >> 10) & 1;
10063 let imm3 = (hw2 >> 12) & 0x7;
10064 let imm8 = hw2 & 0xFF;
10065 (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8
10066 }
10067 fn movx_rd(b: &[u8]) -> u32 {
10068 (u16::from_le_bytes([b[2], b[3]]) as u32 >> 8) & 0xF
10069 }
10070
10071 // --- rd != rn: scratch is rd. imm = 70000 = 0x11170 needs MOVW+MOVT. ---
10072 // 0x11170: lo16 = 0x1170, hi16 = 0x0001
10073 let seq = enc
10074 .encode_thumb32_add_imm(&Reg::R12, &Reg::R0, 70000)
10075 .unwrap();
10076 assert_eq!(seq.len(), 12, "MOVW + MOVT + ADD = 12 bytes");
10077 // MOVW r12, #0x1170
10078 assert_eq!(u16::from_le_bytes([seq[0], seq[1]]) & 0xFBF0, 0xF240);
10079 assert_eq!(movx_rd(&seq[0..4]), 12);
10080 assert_eq!(movx_imm16(&seq[0..4]), 0x1170);
10081 // MOVT r12, #0x0001
10082 assert_eq!(u16::from_le_bytes([seq[4], seq[5]]) & 0xFBF0, 0xF2C0);
10083 assert_eq!(movx_rd(&seq[4..8]), 12);
10084 assert_eq!(movx_imm16(&seq[4..8]), 0x0001);
10085 // ADD.W r12, r0, r12 (EB00 | rn=0 ; rd=12, rm=12)
10086 let add1 = u16::from_le_bytes([seq[8], seq[9]]) as u32;
10087 let add2 = u16::from_le_bytes([seq[10], seq[11]]) as u32;
10088 assert_eq!(add1 & 0xFFF0, 0xEB00);
10089 assert_eq!(add1 & 0xF, 0); // rn = r0
10090 assert_eq!((add2 >> 8) & 0xF, 12); // rd = r12
10091 assert_eq!(add2 & 0xF, 12); // rm = scratch = r12
10092 // The materialized scratch must reconstruct exactly 70000.
10093 assert_eq!(
10094 (movx_imm16(&seq[4..8]) << 16) | movx_imm16(&seq[0..4]),
10095 70000
10096 );
10097
10098 // --- imm <= 0xFFFF: MOVT is skipped (MOVW + ADD = 8 bytes). ---
10099 let seq16 = enc
10100 .encode_thumb32_add_imm(&Reg::R3, &Reg::R0, 0xABCD)
10101 .unwrap();
10102 assert_eq!(seq16.len(), 8, "imm <= 0xFFFF skips MOVT");
10103 assert_eq!(movx_imm16(&seq16[0..4]), 0xABCD);
10104 assert_eq!(movx_rd(&seq16[0..4]), 3); // scratch = rd = r3
10105
10106 // --- rd == rn (in-place add): scratch must be R12, not rd. ---
10107 // imm = 0x12345: lo16 = 0x2345, hi16 = 0x0001
10108 let inplace = enc
10109 .encode_thumb32_add_imm(&Reg::R5, &Reg::R5, 0x12345)
10110 .unwrap();
10111 assert_eq!(inplace.len(), 12);
10112 assert_eq!(movx_rd(&inplace[0..4]), 12, "rd==rn must use R12 scratch");
10113 assert_eq!(
10114 (movx_imm16(&inplace[4..8]) << 16) | movx_imm16(&inplace[0..4]),
10115 0x12345
10116 );
10117 // ADD.W r5, r5, r12 — rm must be the scratch (12), never rn.
10118 let ip_add2 = u16::from_le_bytes([inplace[10], inplace[11]]) as u32;
10119 assert_eq!(ip_add2 & 0xF, 12);
10120 assert_eq!((ip_add2 >> 8) & 0xF, 5);
10121 }
10122
10123 /// #681 — `encode_thumb32_add_imm` packed a RAW immediate into the T3
10124 /// ADD.W `i:imm3:imm8` field, which is a ThumbExpandImm MODIFIED immediate:
10125 /// ThumbExpandImm(0x200) = 0, ThumbExpandImm(0x400) = 0x8000_0000. Every
10126 /// dynamic-address load/store with a static offset in 0x100..=0xFFF
10127 /// computed a wrong address (and bypassed --safety-bounds software: the
10128 /// guard checked the intended address, the access used the mis-encoded
10129 /// one). Fix: imm <= 0xFF keeps T3 (raw == expanded there, bit-identical);
10130 /// 0x100..=0xFFF uses ADDW (T4, plain imm12) — same lowering
10131 /// `encode_thumb32_add` already uses per #253.
10132 ///
10133 /// Every expected byte sequence below is pinned against clang
10134 /// (`-target thumbv7m-none-eabi`) output, bit-for-bit (#544 pattern).
10135 #[test]
10136 fn test_encode_add_imm_thumb_expand_681() {
10137 let enc = ArmEncoder::new_thumb2();
10138 let add = |rd: &Reg, rn: &Reg, imm: u32| enc.encode_thumb32_add_imm(rd, rn, imm).unwrap();
10139
10140 // imm <= 0xFF stays T3 ADD.W (raw == ThumbExpandImm-expanded):
10141 // clang: add.w r12, r0, #0xff = f100 0cff
10142 assert_eq!(add(&Reg::R12, &Reg::R0, 0xFF), vec![0x00, 0xF1, 0xFF, 0x0C]);
10143
10144 // 0x100..=0xFFF must be ADDW (T4, plain imm12). The old T3 raw packing
10145 // decoded as +0 (0x100/0x200), +0x80000000 (0x400), etc.
10146 // clang: addw r12, r0, #0x100 = f200 1c00
10147 assert_eq!(
10148 add(&Reg::R12, &Reg::R0, 0x100),
10149 vec![0x00, 0xF2, 0x00, 0x1C]
10150 );
10151 // clang: addw r12, r0, #0x104 = f200 1c04
10152 assert_eq!(
10153 add(&Reg::R12, &Reg::R0, 0x104),
10154 vec![0x00, 0xF2, 0x04, 0x1C]
10155 );
10156 // clang: addw r12, r0, #0x200 = f200 2c00
10157 assert_eq!(
10158 add(&Reg::R12, &Reg::R0, 0x200),
10159 vec![0x00, 0xF2, 0x00, 0x2C]
10160 );
10161 // clang: addw r12, r0, #0x3fc = f200 3cfc
10162 assert_eq!(
10163 add(&Reg::R12, &Reg::R0, 0x3FC),
10164 vec![0x00, 0xF2, 0xFC, 0x3C]
10165 );
10166 // clang: addw r12, r0, #0x400 = f200 4c00
10167 assert_eq!(
10168 add(&Reg::R12, &Reg::R0, 0x400),
10169 vec![0x00, 0xF2, 0x00, 0x4C]
10170 );
10171 // clang: addw r12, r0, #0xfff = f600 7cff
10172 assert_eq!(
10173 add(&Reg::R12, &Reg::R0, 0xFFF),
10174 vec![0x00, 0xF6, 0xFF, 0x7C]
10175 );
10176 // Non-scratch rd/rn — clang: addw r1, r2, #0x104 = f202 1104
10177 assert_eq!(add(&Reg::R1, &Reg::R2, 0x104), vec![0x02, 0xF2, 0x04, 0x11]);
10178 }
10179
10180 /// #681 class audit — the T2 RSB and AND.W immediate fields are also
10181 /// ThumbExpandImm-coded and were raw-packed. Neither has a plain-imm12
10182 /// (T4-style) form, so a non-representable immediate must Err loudly
10183 /// (#253/#255/#378 class: never silently encode a different constant).
10184 /// Existing emitters only use representable values (RSB #32, AND #0x3F),
10185 /// pinned here bit-for-bit against clang.
10186 #[test]
10187 fn test_rsb_and_imm_thumb_expand_gate_681() {
10188 let enc = ArmEncoder::new_thumb2();
10189
10190 // clang: rsb.w r3, r2, #0x20 = f1c2 0320 — byte-identical to before.
10191 let rsb = enc
10192 .encode(&ArmOp::Rsb {
10193 rd: Reg::R3,
10194 rn: Reg::R2,
10195 imm: 32,
10196 })
10197 .unwrap();
10198 assert_eq!(rsb, vec![0xC2, 0xF1, 0x20, 0x03]);
10199
10200 // 0x101 is not ThumbExpandImm-representable -> must Err, not mis-encode.
10201 assert!(
10202 enc.encode(&ArmOp::Rsb {
10203 rd: Reg::R3,
10204 rn: Reg::R2,
10205 imm: 0x101,
10206 })
10207 .is_err(),
10208 "non-ThumbExpandImm RSB immediate must Err"
10209 );
10210
10211 // clang: and r4, r4, #0x3f = f004 043f — byte-identical to before.
10212 let and = enc.encode_thumb32_and_imm_raw(4, 4, 0x3F).unwrap();
10213 assert_eq!(and, vec![0x04, 0xF0, 0x3F, 0x04]);
10214 assert!(
10215 enc.encode_thumb32_and_imm_raw(4, 4, 0x101).is_err(),
10216 "non-ThumbExpandImm AND immediate must Err"
10217 );
10218
10219 // A32 RSB: imm12 is a rotate:imm8 modified immediate; > 0xFF used to be
10220 // silently masked to `imm & 0xFF` (#378 masking class) -> must Err.
10221 let a32 = ArmEncoder::new_arm32();
10222 assert!(
10223 a32.encode(&ArmOp::Rsb {
10224 rd: Reg::R3,
10225 rn: Reg::R2,
10226 imm: 0x120,
10227 })
10228 .is_err(),
10229 "A32 RSB immediate > 0xFF must Err, not mask"
10230 );
10231 // imm 32 (the only value real codegen emits) still encodes.
10232 assert!(
10233 a32.encode(&ArmOp::Rsb {
10234 rd: Reg::R3,
10235 rn: Reg::R2,
10236 imm: 32,
10237 })
10238 .is_ok()
10239 );
10240 }
10241
10242 /// #350 follow-up — the `encoder_no_panic` fuzz harness drives the encoder
10243 /// with ARBITRARY registers, including the one case the in-place lowering
10244 /// cannot serve: rd==rn==R12. There the scratch (R12, the reserved encoder
10245 /// register) would alias Rn and clobber it before the ADD reads it. The
10246 /// encoder contract (#180/#185) is Ok-or-Err, never a panic — so this must
10247 /// return Err, not assert. (Real codegen never emits rd==rn==R12 because R12
10248 /// is non-allocatable; this guards only the fuzz/adversarial path.)
10249 #[test]
10250 fn test_encode_add_imm_large_rd_rn_r12_errs_not_panics_350() {
10251 let enc = ArmEncoder::new_thumb2();
10252 // Out-of-range imm with rd==rn==R12: no free scratch -> Err.
10253 let r = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 70000);
10254 assert!(
10255 r.is_err(),
10256 "rd==rn==R12 with out-of-range imm must Err (no free scratch), got {r:?}"
10257 );
10258 // Small imm with rd==rn==R12 still takes the single-instruction fast path
10259 // (no scratch needed) and must succeed — the guard is scoped to the
10260 // out-of-range lowering only.
10261 let small = enc.encode_thumb32_add_imm(&Reg::R12, &Reg::R12, 0x10);
10262 assert!(small.is_ok(), "small imm needs no scratch, must stay Ok");
10263 }
10264
10265 /// #378 — `encode_operand2` (ARM32 data-processing operand) must FAIL
10266 /// HONESTLY on an immediate that is not a valid rotated immediate, rather
10267 /// than silently masking it to `imm & 0xFF` and emitting a WRONG
10268 /// instruction. `0x1FF` has 9 set bits, so it cannot come from rotating an
10269 /// 8-bit imm8 — non-encodable. Real codegen materializes large constants via
10270 /// MOVW/MOVT; this guards the encoder's Ok-or-Err contract (#180/#185)
10271 /// directly. It is an Err (not a panic) so the `encoder_no_panic` fuzz
10272 /// harness — which drives arbitrary operands — still passes.
10273 #[test]
10274 fn test_encode_operand2_non_rotatable_imm_errs_not_masks_378() {
10275 let enc = ArmEncoder::new_arm32();
10276 let bad = enc.encode(&ArmOp::Add {
10277 rd: Reg::R0,
10278 rn: Reg::R1,
10279 op2: Operand2::Imm(0x1FF),
10280 });
10281 assert!(
10282 bad.is_err(),
10283 "non-rotatable ARM32 immediate 0x1FF must Err (was silently masked \
10284 to 0xFF), got {bad:?}"
10285 );
10286 // A representable rotated immediate still encodes fine (regression guard).
10287 let ok = enc.encode(&ArmOp::Add {
10288 rd: Reg::R0,
10289 rn: Reg::R1,
10290 op2: Operand2::Imm(0xFF),
10291 });
10292 assert!(
10293 ok.is_ok(),
10294 "0xFF is a valid rotated immediate, must stay Ok"
10295 );
10296 }
10297
10298 #[test]
10299 fn test_encode_ldr_arm32() {
10300 let encoder = ArmEncoder::new_arm32();
10301 let op = ArmOp::Ldr {
10302 rd: Reg::R0,
10303 addr: MemAddr::imm(Reg::R1, 4),
10304 };
10305
10306 let code = encoder.encode(&op).unwrap();
10307 assert_eq!(code.len(), 4);
10308
10309 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10310 // Verify load bit is set
10311 assert_eq!(instr & 0x00100000, 0x00100000);
10312 }
10313
10314 #[test]
10315 fn test_encode_str_arm32() {
10316 let encoder = ArmEncoder::new_arm32();
10317 let op = ArmOp::Str {
10318 rd: Reg::R0,
10319 addr: MemAddr::imm(Reg::SP, 0),
10320 };
10321
10322 let code = encoder.encode(&op).unwrap();
10323 assert_eq!(code.len(), 4);
10324 }
10325
10326 #[test]
10327 fn test_encode_branch_arm32() {
10328 let encoder = ArmEncoder::new_arm32();
10329 let op = ArmOp::Bl {
10330 label: "main".to_string(),
10331 };
10332
10333 let code = encoder.encode(&op).unwrap();
10334 assert_eq!(code.len(), 4);
10335
10336 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10337 // Verify BL opcode
10338 assert_eq!(instr & 0x0F000000, 0x0B000000);
10339 }
10340
10341 /// Regression test for #167 + #174: the Thumb-2 BL relocatable placeholder
10342 /// must carry a -4 addend so an R_ARM_THM_CALL nets to exactly the symbol S.
10343 /// The correct encoding is what `gas` emits for `bl <extern>`: f7ff fffe
10344 /// (hw1=0xF7FF, hw2=0xFFFE), little-endian bytes FF F7 FE FF.
10345 /// - 0xD000 (J1=J2=0) → ~+0x600000 garbage addend: `bl c0000c` / truncated
10346 /// to fit (#167).
10347 /// - 0xF800 (addend 0) → lands at S+4, one instruction past the callee
10348 /// entry (#174).
10349 /// - 0xFFFE (addend -4) → lands at S. Correct.
10350 #[test]
10351 fn test_encode_thumb_bl_placeholder_addend_167_174() {
10352 let encoder = ArmEncoder::new_thumb2();
10353 let op = ArmOp::Bl {
10354 label: "callee".to_string(),
10355 };
10356
10357 let code = encoder.encode(&op).unwrap();
10358 assert_eq!(code.len(), 4, "Thumb-2 BL is 32-bit");
10359
10360 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10361 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10362 assert_eq!(hw1, 0xF7FF, "BL first halfword (matches gas `bl <extern>`)");
10363 assert_eq!(
10364 hw2, 0xFFFE,
10365 "BL second halfword must be 0xFFFE (-4 addend → nets to S), not 0xF800 (→ S+4, #174) or 0xD000 (#167)"
10366 );
10367 assert_ne!(hw2, 0xF800, "0xF800 (addend 0) lands at S+4 (#174)");
10368 assert_ne!(hw2, 0xD000, "0xD000 bakes in a ~+0x600000 addend (#167)");
10369 }
10370
10371 /// #740: the Thumb-2 32-bit B<cond>.W (encoding T3) must pack the
10372 /// HALFWORD offset directly into S:J2:J1:imm6:imm11 — the byte offset is
10373 /// SignExtend(S:J2:J1:imm6:imm11:'0'). The old arm packed
10374 /// `halfword_offset >> 1`, HALVING every wide conditional branch's
10375 /// displacement: gust_poll's loop-head `br_if` to an outer block end
10376 /// landed mid-shape (a spurious state write + spurious calls on the
10377 /// empty-budget path). Narrow (16-bit) B<cond> was unaffected — only
10378 /// spans > 254 bytes hit the bug. Bytes cross-checked against the llvm
10379 /// disassembler (`bne.w #0x224` = f040 8112).
10380 #[test]
10381 fn test_encode_thumb_bcond_wide_t3_halfword_offset_740() {
10382 use synth_synthesis::Condition;
10383 let encoder = ArmEncoder::new_thumb2();
10384
10385 // gust_poll's loop-head edge: NE, +0x112 halfwords (+0x224 bytes).
10386 let code = encoder
10387 .encode(&ArmOp::BCondOffset {
10388 cond: Condition::NE,
10389 offset: 0x112,
10390 })
10391 .unwrap();
10392 assert_eq!(code.len(), 4, "offset beyond ±127 halfwords must be wide");
10393 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10394 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10395 assert_eq!(hw1, 0xF040, "T3 hw1: 1111 0 S=0 cond=NE imm6=0");
10396 assert_eq!(
10397 hw2, 0x8112,
10398 "T3 hw2 imm11 must carry halfword offset bits [10:0] directly — \
10399 0x8089 (offset>>1) is the halved #740 miscompile"
10400 );
10401
10402 // Backward wide branch: EQ, -0x100 halfwords. S=1, J2=J1=1,
10403 // imm6=0b111111, imm11=0x700 → f43f af00.
10404 let code = encoder
10405 .encode(&ArmOp::BCondOffset {
10406 cond: Condition::EQ,
10407 offset: -0x100,
10408 })
10409 .unwrap();
10410 assert_eq!(code.len(), 4);
10411 let hw1 = u16::from_le_bytes([code[0], code[1]]);
10412 let hw2 = u16::from_le_bytes([code[2], code[3]]);
10413 assert_eq!(hw1, 0xF43F, "T3 hw1: S=1, cond=EQ, imm6=0x3F");
10414 assert_eq!(hw2, 0xAF00, "T3 hw2: J1=1 J2=1 imm11=0x700");
10415
10416 // Narrow encoding stays byte-identical (in-range offsets untouched).
10417 let code = encoder
10418 .encode(&ArmOp::BCondOffset {
10419 cond: Condition::EQ,
10420 offset: 5,
10421 })
10422 .unwrap();
10423 assert_eq!(code, vec![0x05, 0xD0], "narrow B<cond> unchanged");
10424
10425 // Out of the signed 20-bit T3 range: loud Err, never a truncated jump.
10426 assert!(
10427 encoder
10428 .encode(&ArmOp::BCondOffset {
10429 cond: Condition::NE,
10430 offset: 1 << 19,
10431 })
10432 .is_err(),
10433 "out-of-range T3 offset must be a loud decline"
10434 );
10435 }
10436
10437 #[test]
10438 fn test_encode_sequence() {
10439 let encoder = ArmEncoder::new_arm32();
10440 let ops = vec![
10441 ArmOp::Mov {
10442 rd: Reg::R0,
10443 op2: Operand2::Imm(42),
10444 },
10445 ArmOp::Mov {
10446 rd: Reg::R1,
10447 op2: Operand2::Imm(10),
10448 },
10449 ArmOp::Add {
10450 rd: Reg::R2,
10451 rn: Reg::R0,
10452 op2: Operand2::Reg(Reg::R1),
10453 },
10454 ];
10455
10456 let code = encoder.encode_sequence(&ops).unwrap();
10457 assert_eq!(code.len(), 12); // 3 instructions * 4 bytes
10458 }
10459
10460 #[test]
10461 fn test_reg_to_bits() {
10462 assert_eq!(reg_to_bits(&Reg::R0), 0);
10463 assert_eq!(reg_to_bits(&Reg::R7), 7);
10464 assert_eq!(reg_to_bits(&Reg::SP), 13);
10465 assert_eq!(reg_to_bits(&Reg::LR), 14);
10466 assert_eq!(reg_to_bits(&Reg::PC), 15);
10467 }
10468
10469 #[test]
10470 fn test_encode_bitwise_operations() {
10471 let encoder = ArmEncoder::new_arm32();
10472
10473 let and_op = ArmOp::And {
10474 rd: Reg::R0,
10475 rn: Reg::R1,
10476 op2: Operand2::Reg(Reg::R2),
10477 };
10478 let and_code = encoder.encode(&and_op).unwrap();
10479 assert_eq!(and_code.len(), 4);
10480
10481 let orr_op = ArmOp::Orr {
10482 rd: Reg::R0,
10483 rn: Reg::R1,
10484 op2: Operand2::Reg(Reg::R2),
10485 };
10486 let orr_code = encoder.encode(&orr_op).unwrap();
10487 assert_eq!(orr_code.len(), 4);
10488
10489 let eor_op = ArmOp::Eor {
10490 rd: Reg::R0,
10491 rn: Reg::R1,
10492 op2: Operand2::Reg(Reg::R2),
10493 };
10494 let eor_code = encoder.encode(&eor_op).unwrap();
10495 assert_eq!(eor_code.len(), 4);
10496 }
10497
10498 // === Thumb-2 32-bit encoding tests ===
10499
10500 #[test]
10501 fn test_encode_sdiv_thumb2() {
10502 let encoder = ArmEncoder::new_thumb2();
10503 let op = ArmOp::Sdiv {
10504 rd: Reg::R0,
10505 rn: Reg::R1,
10506 rm: Reg::R2,
10507 };
10508
10509 let code = encoder.encode(&op).unwrap();
10510 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10511
10512 // SDIV R0, R1, R2: 0xFB91 0xF0F2
10513 // First halfword: 0xFB90 | Rn(1) = 0xFB91
10514 // Second halfword: 0xF0F0 | Rd(0)<<8 | Rm(2) = 0xF0F2
10515 // Little-endian: [0x91, 0xFB, 0xF2, 0xF0]
10516 assert_eq!(code[0], 0x91);
10517 assert_eq!(code[1], 0xFB);
10518 assert_eq!(code[2], 0xF2);
10519 assert_eq!(code[3], 0xF0);
10520 }
10521
10522 #[test]
10523 fn test_encode_udiv_thumb2() {
10524 let encoder = ArmEncoder::new_thumb2();
10525 let op = ArmOp::Udiv {
10526 rd: Reg::R0,
10527 rn: Reg::R1,
10528 rm: Reg::R2,
10529 };
10530
10531 let code = encoder.encode(&op).unwrap();
10532 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10533
10534 // UDIV R0, R1, R2: 0xFBB1 0xF0F2
10535 // Little-endian: [0xB1, 0xFB, 0xF2, 0xF0]
10536 assert_eq!(code[0], 0xB1);
10537 assert_eq!(code[1], 0xFB);
10538 assert_eq!(code[2], 0xF2);
10539 assert_eq!(code[3], 0xF0);
10540 }
10541
10542 #[test]
10543 fn test_encode_mul_thumb2() {
10544 let encoder = ArmEncoder::new_thumb2();
10545 let op = ArmOp::Mul {
10546 rd: Reg::R0,
10547 rn: Reg::R1,
10548 rm: Reg::R2,
10549 };
10550
10551 let code = encoder.encode(&op).unwrap();
10552 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10553 }
10554
10555 #[test]
10556 fn test_encode_and_thumb2() {
10557 let encoder = ArmEncoder::new_thumb2();
10558 let op = ArmOp::And {
10559 rd: Reg::R0,
10560 rn: Reg::R1,
10561 op2: Operand2::Reg(Reg::R2),
10562 };
10563
10564 let code = encoder.encode(&op).unwrap();
10565 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10566 }
10567
10568 #[test]
10569 fn test_encode_lsl_thumb2_low_regs() {
10570 let encoder = ArmEncoder::new_thumb2();
10571 let op = ArmOp::Lsl {
10572 rd: Reg::R0,
10573 rn: Reg::R1,
10574 shift: 5,
10575 };
10576
10577 let code = encoder.encode(&op).unwrap();
10578 assert_eq!(code.len(), 2); // 16-bit for low registers
10579 }
10580
10581 #[test]
10582 fn test_encode_clz_thumb2() {
10583 let encoder = ArmEncoder::new_thumb2();
10584 let op = ArmOp::Clz {
10585 rd: Reg::R0,
10586 rm: Reg::R1,
10587 };
10588
10589 let code = encoder.encode(&op).unwrap();
10590 assert_eq!(code.len(), 4); // 32-bit Thumb-2 instruction
10591 }
10592
10593 #[test]
10594 fn test_encode_bx_thumb2() {
10595 let encoder = ArmEncoder::new_thumb2();
10596 let op = ArmOp::Bx { rm: Reg::LR };
10597
10598 let code = encoder.encode(&op).unwrap();
10599 assert_eq!(code.len(), 2); // 16-bit instruction
10600
10601 // BX LR: 0x4770
10602 assert_eq!(code, vec![0x70, 0x47]);
10603 }
10604
10605 // ========================================================================
10606 // f32 pseudo-op encoding tests
10607 // ========================================================================
10608
10609 #[test]
10610 fn test_encode_f32_abs_arm32() {
10611 let encoder = ArmEncoder::new_arm32();
10612 let op = ArmOp::F32Abs {
10613 sd: VfpReg::S0,
10614 sm: VfpReg::S2,
10615 };
10616 let code = encoder.encode(&op).unwrap();
10617 assert_eq!(code.len(), 4); // Single VFP instruction
10618 }
10619
10620 #[test]
10621 fn test_encode_f32_neg_arm32() {
10622 let encoder = ArmEncoder::new_arm32();
10623 let op = ArmOp::F32Neg {
10624 sd: VfpReg::S0,
10625 sm: VfpReg::S2,
10626 };
10627 let code = encoder.encode(&op).unwrap();
10628 assert_eq!(code.len(), 4);
10629 }
10630
10631 #[test]
10632 fn test_encode_f32_sqrt_arm32() {
10633 let encoder = ArmEncoder::new_arm32();
10634 let op = ArmOp::F32Sqrt {
10635 sd: VfpReg::S0,
10636 sm: VfpReg::S2,
10637 };
10638 let code = encoder.encode(&op).unwrap();
10639 assert_eq!(code.len(), 4);
10640 }
10641
10642 #[test]
10643 fn test_encode_f32_ceil_arm32() {
10644 let encoder = ArmEncoder::new_arm32();
10645 let op = ArmOp::F32Ceil {
10646 sd: VfpReg::S0,
10647 sm: VfpReg::S2,
10648 };
10649 let code = encoder.encode(&op).unwrap();
10650 // VMRS + BIC + ORR + VMSR + VCVT.S32.F32 + VMRS + BIC + VMSR + VCVT.F32.S32
10651 assert_eq!(code.len(), 36);
10652 }
10653
10654 #[test]
10655 fn test_encode_f32_floor_thumb2() {
10656 let encoder = ArmEncoder::new_thumb2();
10657 let op = ArmOp::F32Floor {
10658 sd: VfpReg::S0,
10659 sm: VfpReg::S2,
10660 };
10661 let code = encoder.encode(&op).unwrap();
10662 // VMRS + BIC.W + ORR.W + VMSR + VCVT + VMRS + BIC.W + VMSR + VCVT.F32.S32
10663 assert_eq!(code.len(), 36);
10664 }
10665
10666 #[test]
10667 fn test_encode_f32_min_arm32() {
10668 let encoder = ArmEncoder::new_arm32();
10669 let op = ArmOp::F32Min {
10670 sd: VfpReg::S0,
10671 sn: VfpReg::S2,
10672 sm: VfpReg::S4,
10673 };
10674 let code = encoder.encode(&op).unwrap();
10675 assert_eq!(code.len(), 16); // VMOV + VCMP + VMRS + conditional VMOV
10676 }
10677
10678 #[test]
10679 fn test_encode_f32_max_thumb2() {
10680 let encoder = ArmEncoder::new_thumb2();
10681 let op = ArmOp::F32Max {
10682 sd: VfpReg::S0,
10683 sn: VfpReg::S2,
10684 sm: VfpReg::S4,
10685 };
10686 let code = encoder.encode(&op).unwrap();
10687 // VMOV(4) + VCMP(4) + VMRS(4) + IT(2) + VMOV(4) = 18
10688 assert_eq!(code.len(), 18);
10689 }
10690
10691 #[test]
10692 fn test_encode_f32_copysign_arm32() {
10693 let encoder = ArmEncoder::new_arm32();
10694 let op = ArmOp::F32Copysign {
10695 sd: VfpReg::S0,
10696 sn: VfpReg::S2,
10697 sm: VfpReg::S4,
10698 };
10699 let code = encoder.encode(&op).unwrap();
10700 // VMOV + VMOV + AND + BIC + ORR + VMOV = 6 * 4 = 24
10701 assert_eq!(code.len(), 24);
10702 }
10703
10704 // ========================================================================
10705 // f64 encoding tests
10706 // ========================================================================
10707
10708 #[test]
10709 fn test_encode_f64_add_arm32() {
10710 let encoder = ArmEncoder::new_arm32();
10711 let op = ArmOp::F64Add {
10712 dd: VfpReg::D0,
10713 dn: VfpReg::D1,
10714 dm: VfpReg::D2,
10715 };
10716 let code = encoder.encode(&op).unwrap();
10717 assert_eq!(code.len(), 4);
10718 // VADD.F64 D0, D1, D2: check coprocessor is cp11 (0xB)
10719 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10720 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
10721 }
10722
10723 #[test]
10724 fn test_encode_f64_sub_thumb2() {
10725 let encoder = ArmEncoder::new_thumb2();
10726 let op = ArmOp::F64Sub {
10727 dd: VfpReg::D0,
10728 dn: VfpReg::D1,
10729 dm: VfpReg::D2,
10730 };
10731 let code = encoder.encode(&op).unwrap();
10732 assert_eq!(code.len(), 4); // 32-bit VFP as two Thumb halfwords
10733 }
10734
10735 #[test]
10736 fn test_encode_f64_mul_arm32() {
10737 let encoder = ArmEncoder::new_arm32();
10738 let op = ArmOp::F64Mul {
10739 dd: VfpReg::D0,
10740 dn: VfpReg::D1,
10741 dm: VfpReg::D2,
10742 };
10743 let code = encoder.encode(&op).unwrap();
10744 assert_eq!(code.len(), 4);
10745 }
10746
10747 #[test]
10748 fn test_encode_f64_div_arm32() {
10749 let encoder = ArmEncoder::new_arm32();
10750 let op = ArmOp::F64Div {
10751 dd: VfpReg::D0,
10752 dn: VfpReg::D1,
10753 dm: VfpReg::D2,
10754 };
10755 let code = encoder.encode(&op).unwrap();
10756 assert_eq!(code.len(), 4);
10757 }
10758
10759 #[test]
10760 fn test_encode_f64_abs_arm32() {
10761 let encoder = ArmEncoder::new_arm32();
10762 let op = ArmOp::F64Abs {
10763 dd: VfpReg::D0,
10764 dm: VfpReg::D2,
10765 };
10766 let code = encoder.encode(&op).unwrap();
10767 assert_eq!(code.len(), 4);
10768 }
10769
10770 #[test]
10771 fn test_encode_f64_neg_arm32() {
10772 let encoder = ArmEncoder::new_arm32();
10773 let op = ArmOp::F64Neg {
10774 dd: VfpReg::D0,
10775 dm: VfpReg::D2,
10776 };
10777 let code = encoder.encode(&op).unwrap();
10778 assert_eq!(code.len(), 4);
10779 }
10780
10781 #[test]
10782 fn test_encode_f64_sqrt_arm32() {
10783 let encoder = ArmEncoder::new_arm32();
10784 let op = ArmOp::F64Sqrt {
10785 dd: VfpReg::D0,
10786 dm: VfpReg::D2,
10787 };
10788 let code = encoder.encode(&op).unwrap();
10789 assert_eq!(code.len(), 4);
10790 }
10791
10792 #[test]
10793 fn test_encode_f64_load_arm32() {
10794 let encoder = ArmEncoder::new_arm32();
10795 let op = ArmOp::F64Load {
10796 dd: VfpReg::D0,
10797 addr: MemAddr::imm(Reg::R0, 8),
10798 };
10799 let code = encoder.encode(&op).unwrap();
10800 assert_eq!(code.len(), 4);
10801 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
10802 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11 for F64
10803 assert_eq!(instr & 0xFF, 2); // offset 8 / 4 = 2
10804 }
10805
10806 #[test]
10807 fn test_encode_f64_store_thumb2() {
10808 let encoder = ArmEncoder::new_thumb2();
10809 let op = ArmOp::F64Store {
10810 dd: VfpReg::D0,
10811 addr: MemAddr::imm(Reg::SP, 0),
10812 };
10813 let code = encoder.encode(&op).unwrap();
10814 assert_eq!(code.len(), 4);
10815 }
10816
10817 #[test]
10818 fn test_encode_f64_compare_arm32() {
10819 let encoder = ArmEncoder::new_arm32();
10820 let op = ArmOp::F64Eq {
10821 rd: Reg::R0,
10822 dn: VfpReg::D0,
10823 dm: VfpReg::D1,
10824 };
10825 let code = encoder.encode(&op).unwrap();
10826 assert_eq!(code.len(), 16); // VCMP + VMRS + MOV #0 + MOVcond #1
10827 }
10828
10829 #[test]
10830 fn test_encode_f64_compare_thumb2() {
10831 let encoder = ArmEncoder::new_thumb2();
10832 let op = ArmOp::F64Lt {
10833 rd: Reg::R0,
10834 dn: VfpReg::D0,
10835 dm: VfpReg::D1,
10836 };
10837 let code = encoder.encode(&op).unwrap();
10838 // VCMP(4) + VMRS(4) + MOVS(2) + IT(2) + MOV(2) = 14
10839 assert_eq!(code.len(), 14);
10840 }
10841
10842 #[test]
10843 fn test_encode_f64_const_arm32() {
10844 let encoder = ArmEncoder::new_arm32();
10845 let op = ArmOp::F64Const {
10846 dd: VfpReg::D0,
10847 value: 3.125,
10848 };
10849 let code = encoder.encode(&op).unwrap();
10850 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10851 assert_eq!(code.len(), 20);
10852 }
10853
10854 #[test]
10855 fn test_encode_f64_const_thumb2() {
10856 let encoder = ArmEncoder::new_thumb2();
10857 let op = ArmOp::F64Const {
10858 dd: VfpReg::D0,
10859 value: 2.5,
10860 };
10861 let code = encoder.encode(&op).unwrap();
10862 // MOVW(4) + MOVT(4) + MOVW(4) + MOVT(4) + VMOV(4) = 20
10863 assert_eq!(code.len(), 20);
10864 }
10865
10866 #[test]
10867 fn test_encode_f64_convert_i32s_arm32() {
10868 let encoder = ArmEncoder::new_arm32();
10869 let op = ArmOp::F64ConvertI32S {
10870 dd: VfpReg::D0,
10871 rm: Reg::R0,
10872 };
10873 let code = encoder.encode(&op).unwrap();
10874 // VMOV(4) + VCVT(4) = 8
10875 assert_eq!(code.len(), 8);
10876 }
10877
10878 #[test]
10879 fn test_encode_f64_promote_f32_arm32() {
10880 let encoder = ArmEncoder::new_arm32();
10881 let op = ArmOp::F64PromoteF32 {
10882 dd: VfpReg::D0,
10883 sm: VfpReg::S0,
10884 };
10885 let code = encoder.encode(&op).unwrap();
10886 assert_eq!(code.len(), 4); // Single VCVT.F64.F32 instruction
10887 }
10888
10889 #[test]
10890 fn test_encode_f64_promote_f32_thumb2() {
10891 let encoder = ArmEncoder::new_thumb2();
10892 let op = ArmOp::F64PromoteF32 {
10893 dd: VfpReg::D0,
10894 sm: VfpReg::S0,
10895 };
10896 let code = encoder.encode(&op).unwrap();
10897 assert_eq!(code.len(), 4);
10898 }
10899
10900 #[test]
10901 fn test_encode_i32_trunc_f64s_arm32() {
10902 let encoder = ArmEncoder::new_arm32();
10903 let op = ArmOp::I32TruncF64S {
10904 rd: Reg::R0,
10905 dm: VfpReg::D0,
10906 };
10907 let code = encoder.encode(&op).unwrap();
10908 // VCVT(4) + VMOV(4) = 8
10909 assert_eq!(code.len(), 8);
10910 }
10911
10912 #[test]
10913 fn test_encode_f64_reinterpret_i64_arm32() {
10914 let encoder = ArmEncoder::new_arm32();
10915 let op = ArmOp::F64ReinterpretI64 {
10916 dd: VfpReg::D0,
10917 rmlo: Reg::R0,
10918 rmhi: Reg::R1,
10919 };
10920 let code = encoder.encode(&op).unwrap();
10921 assert_eq!(code.len(), 4); // Single VMOV instruction
10922 }
10923
10924 #[test]
10925 fn test_encode_i64_reinterpret_f64_thumb2() {
10926 let encoder = ArmEncoder::new_thumb2();
10927 let op = ArmOp::I64ReinterpretF64 {
10928 rdlo: Reg::R0,
10929 rdhi: Reg::R1,
10930 dm: VfpReg::D0,
10931 };
10932 let code = encoder.encode(&op).unwrap();
10933 assert_eq!(code.len(), 4);
10934 }
10935
10936 #[test]
10937 fn test_encode_f64_trunc_thumb2() {
10938 let encoder = ArmEncoder::new_thumb2();
10939 let op = ArmOp::F64Trunc {
10940 dd: VfpReg::D0,
10941 dm: VfpReg::D1,
10942 };
10943 let code = encoder.encode(&op).unwrap();
10944 // GI-FPU-002 phase 3 (#369): a single VRINTZ.F64 (clang-verified
10945 // vrintz.f64 d0,d1 base) — no more FPSCR dance / S0 clobber.
10946 assert_eq!(code.len(), 4);
10947 assert_eq!(code, vec![0xb6, 0xee, 0xc1, 0x0b]);
10948 }
10949
10950 /// GI-FPU-002 phase 3 (#369): the rewritten f64 tail sequences, byte-exact
10951 /// against clang (`-target thumbv7em-none-eabi -mfpu=fpv5-d16`). Each
10952 /// clobbers ONLY its destination (+R12/flags where noted) — the previous
10953 /// pseudo-ops staged through live S0/R0-R2 (the #615 class) and the
10954 /// min/max/rounding semantics were wrong (ordered IT select returned the
10955 /// wrong operand on NaN/±0; rounding round-tripped through a 32-bit int).
10956 #[test]
10957 fn test_369_f64_tail_thumb2_encodings_match_clang() {
10958 let enc = ArmEncoder::new_thumb2();
10959 // vrintn/vrintp/vrintm.f64 d1, d2 (FE space, never IT'd).
10960 for (op, want) in [
10961 (
10962 ArmOp::F64Nearest {
10963 dd: VfpReg::D1,
10964 dm: VfpReg::D2,
10965 },
10966 vec![0xb9, 0xfe, 0x42, 0x1b],
10967 ),
10968 (
10969 ArmOp::F64Ceil {
10970 dd: VfpReg::D1,
10971 dm: VfpReg::D2,
10972 },
10973 vec![0xba, 0xfe, 0x42, 0x1b],
10974 ),
10975 (
10976 ArmOp::F64Floor {
10977 dd: VfpReg::D1,
10978 dm: VfpReg::D2,
10979 },
10980 vec![0xbb, 0xfe, 0x42, 0x1b],
10981 ),
10982 ] {
10983 assert_eq!(enc.encode(&op).unwrap(), want, "{op:?}");
10984 }
10985 // vcmp.f64 d1,d2 ; vmrs ; vminnm.f64 d0,d1,d2 ; it vs ; vaddvs.f64
10986 let min = enc
10987 .encode(&ArmOp::F64Min {
10988 dd: VfpReg::D0,
10989 dn: VfpReg::D1,
10990 dm: VfpReg::D2,
10991 })
10992 .unwrap();
10993 assert_eq!(
10994 min,
10995 vec![
10996 0xb4, 0xee, 0x42, 0x1b, // vcmp.f64 d1, d2
10997 0xf1, 0xee, 0x10, 0xfa, // vmrs APSR_nzcv, fpscr
10998 0x81, 0xfe, 0x42, 0x0b, // vminnm.f64 d0, d1, d2
10999 0x68, 0xbf, // it vs
11000 0x31, 0xee, 0x02, 0x0b, // vaddvs.f64 d0, d1, d2
11001 ]
11002 );
11003 // vmaxnm variant flips only bit6 of the VMINNM word.
11004 let max = enc
11005 .encode(&ArmOp::F64Max {
11006 dd: VfpReg::D0,
11007 dn: VfpReg::D1,
11008 dm: VfpReg::D2,
11009 })
11010 .unwrap();
11011 assert_eq!(&max[8..12], &[0x81, 0xfe, 0x02, 0x0b]);
11012 // Destination aliasing a source must ERR (the NaN fix-up would read
11013 // a clobbered operand), never encode.
11014 assert!(
11015 enc.encode(&ArmOp::F64Min {
11016 dd: VfpReg::D1,
11017 dn: VfpReg::D1,
11018 dm: VfpReg::D2,
11019 })
11020 .is_err()
11021 );
11022 // copysign d0,(mag)d1,(sign)d2:
11023 // vmov r12,s5 ; cmp.w r12,#0 ; vabs.f64 d0,d1 ; it mi ; vnegmi.f64 d0,d0
11024 let cs = enc
11025 .encode(&ArmOp::F64Copysign {
11026 dd: VfpReg::D0,
11027 dn: VfpReg::D1,
11028 dm: VfpReg::D2,
11029 })
11030 .unwrap();
11031 assert_eq!(
11032 cs,
11033 vec![
11034 0x12, 0xee, 0x90, 0xca, // vmov r12, s5
11035 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11036 0xb0, 0xee, 0xc1, 0x0b, // vabs.f64 d0, d1
11037 0x48, 0xbf, // it mi
11038 0xb1, 0xee, 0x40, 0x0b, // vnegmi.f64 d0, d0
11039 ]
11040 );
11041 // f32 copysign s0,(mag)s1,(sign)s2 — the R0-clobber-free rewrite:
11042 // vmov r12,s2 ; cmp.w r12,#0 ; vabs.f32 s0,s1 ; it mi ; vnegmi.f32
11043 let cs32 = enc
11044 .encode(&ArmOp::F32Copysign {
11045 sd: VfpReg::S0,
11046 sn: VfpReg::S1,
11047 sm: VfpReg::S2,
11048 })
11049 .unwrap();
11050 assert_eq!(
11051 cs32,
11052 vec![
11053 0x11, 0xee, 0x10, 0xca, // vmov r12, s2
11054 0xbc, 0xf1, 0x00, 0x0f, // cmp.w r12, #0
11055 0xb0, 0xee, 0xe0, 0x0a, // vabs.f32 s0, s1
11056 0x48, 0xbf, // it mi
11057 0xb1, 0xee, 0x40, 0x0a, // vnegmi.f32 s0, s0
11058 ]
11059 );
11060 // i32 -> f64 stages through the DESTINATION's S-alias (never S0) and
11061 // uses the CORRECT signed/unsigned VCVT bases (previously swapped):
11062 // vmov s0,r3 ; vcvt.f64.s32 d0,s0
11063 let conv_s = enc
11064 .encode(&ArmOp::F64ConvertI32S {
11065 dd: VfpReg::D0,
11066 rm: Reg::R3,
11067 })
11068 .unwrap();
11069 assert_eq!(
11070 conv_s,
11071 vec![
11072 0x00, 0xee, 0x10, 0x3a, // vmov s0, r3
11073 0xb8, 0xee, 0xc0, 0x0b, // vcvt.f64.s32 d0, s0
11074 ]
11075 );
11076 let conv_u = enc
11077 .encode(&ArmOp::F64ConvertI32U {
11078 dd: VfpReg::D0,
11079 rm: Reg::R3,
11080 })
11081 .unwrap();
11082 assert_eq!(&conv_u[4..8], &[0xb8, 0xee, 0x40, 0x0b]); // vcvt.f64.u32
11083 // f64 -> i32 stages through the SOURCE's S-alias (never S0):
11084 // vcvt.s32.f64 s2,d1 ; vmov r3,s2
11085 let trunc_s = enc
11086 .encode(&ArmOp::I32TruncF64S {
11087 rd: Reg::R3,
11088 dm: VfpReg::D1,
11089 })
11090 .unwrap();
11091 assert_eq!(
11092 trunc_s,
11093 vec![
11094 0xbd, 0xee, 0xc1, 0x1b, // vcvt.s32.f64 s2, d1
11095 0x11, 0xee, 0x10, 0x3a, // vmov r3, s2
11096 ]
11097 );
11098 let trunc_u = enc
11099 .encode(&ArmOp::I32TruncF64U {
11100 rd: Reg::R3,
11101 dm: VfpReg::D1,
11102 })
11103 .unwrap();
11104 assert_eq!(&trunc_u[0..4], &[0xbc, 0xee, 0xc1, 0x1b]); // vcvt.u32.f64
11105 // f32.demote_f64: vcvt.f32.f64 s1, d2
11106 let demote = enc
11107 .encode(&ArmOp::F32DemoteF64 {
11108 sd: VfpReg::S1,
11109 dm: VfpReg::D2,
11110 })
11111 .unwrap();
11112 assert_eq!(demote, vec![0xf7, 0xee, 0xc2, 0x0b]);
11113 }
11114
11115 #[test]
11116 fn test_encode_f64_min_arm32() {
11117 let encoder = ArmEncoder::new_arm32();
11118 let op = ArmOp::F64Min {
11119 dd: VfpReg::D0,
11120 dn: VfpReg::D1,
11121 dm: VfpReg::D2,
11122 };
11123 let code = encoder.encode(&op).unwrap();
11124 // VMOV + VCMP + VMRS + conditional VMOV = 16
11125 assert_eq!(code.len(), 16);
11126 }
11127
11128 #[test]
11129 fn test_f64_cp11_encoding() {
11130 // Verify that F64 instructions use coprocessor 11 (0xB), not 10 (0xA)
11131 let encoder = ArmEncoder::new_arm32();
11132
11133 // F64Add
11134 let code = encoder
11135 .encode(&ArmOp::F64Add {
11136 dd: VfpReg::D0,
11137 dn: VfpReg::D0,
11138 dm: VfpReg::D0,
11139 })
11140 .unwrap();
11141 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11142 assert_eq!((instr >> 8) & 0xF, 0xB, "F64 should use cp11");
11143
11144 // F32Add for comparison
11145 let code = encoder
11146 .encode(&ArmOp::F32Add {
11147 sd: VfpReg::S0,
11148 sn: VfpReg::S0,
11149 sm: VfpReg::S0,
11150 })
11151 .unwrap();
11152 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11153 assert_eq!((instr >> 8) & 0xF, 0xA, "F32 should use cp10");
11154 }
11155
11156 #[test]
11157 fn test_dreg_encoding_higher_registers() {
11158 let encoder = ArmEncoder::new_arm32();
11159
11160 // Test with D15 (highest register)
11161 let op = ArmOp::F64Add {
11162 dd: VfpReg::D15,
11163 dn: VfpReg::D14,
11164 dm: VfpReg::D13,
11165 };
11166 let code = encoder.encode(&op).unwrap();
11167 assert_eq!(code.len(), 4);
11168
11169 // Verify the register encoding worked (instruction is valid)
11170 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11171 assert_eq!((instr >> 8) & 0xF, 0xB); // cp11
11172 }
11173
11174 // ========================================================================
11175 // Control flow encoding tests
11176 // ========================================================================
11177
11178 #[test]
11179 fn test_encode_label_emits_no_bytes() {
11180 let encoder = ArmEncoder::new_thumb2();
11181 let op = ArmOp::Label {
11182 name: ".Lblock_end_0".to_string(),
11183 };
11184 let code = encoder.encode(&op).unwrap();
11185 assert!(code.is_empty(), "Label should emit zero bytes");
11186
11187 let encoder32 = ArmEncoder::new_arm32();
11188 let code32 = encoder32.encode(&op).unwrap();
11189 assert!(
11190 code32.is_empty(),
11191 "Label should emit zero bytes in ARM32 too"
11192 );
11193 }
11194
11195 #[test]
11196 fn test_encode_bcc_eq_thumb2() {
11197 use synth_synthesis::Condition;
11198 let encoder = ArmEncoder::new_thumb2();
11199 let op = ArmOp::Bcc {
11200 cond: Condition::EQ,
11201 label: "target".to_string(),
11202 };
11203 let code = encoder.encode(&op).unwrap();
11204 assert_eq!(code.len(), 2); // 16-bit conditional branch
11205
11206 // BEQ with offset 0: 0xD000 in little-endian
11207 assert_eq!(code, vec![0x00, 0xD0]);
11208 }
11209
11210 #[test]
11211 fn test_encode_bcc_ne_thumb2() {
11212 use synth_synthesis::Condition;
11213 let encoder = ArmEncoder::new_thumb2();
11214 let op = ArmOp::Bcc {
11215 cond: Condition::NE,
11216 label: "target".to_string(),
11217 };
11218 let code = encoder.encode(&op).unwrap();
11219 assert_eq!(code.len(), 2);
11220
11221 // BNE with offset 0: 0xD100 in little-endian
11222 assert_eq!(code, vec![0x00, 0xD1]);
11223 }
11224
11225 #[test]
11226 fn test_encode_bcc_arm32() {
11227 use synth_synthesis::Condition;
11228 let encoder = ArmEncoder::new_arm32();
11229 let op = ArmOp::Bcc {
11230 cond: Condition::EQ,
11231 label: "target".to_string(),
11232 };
11233 let code = encoder.encode(&op).unwrap();
11234 assert_eq!(code.len(), 4); // 32-bit ARM instruction
11235
11236 let instr = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
11237 // BEQ: cond=0x0, opcode=0xA, offset=0
11238 assert_eq!(instr & 0xF0000000, 0x00000000); // EQ condition
11239 assert_eq!(instr & 0x0F000000, 0x0A000000); // Branch opcode
11240 }
11241
11242 #[test]
11243 fn test_encode_udf_thumb2() {
11244 let encoder = ArmEncoder::new_thumb2();
11245 let op = ArmOp::Udf { imm: 0 };
11246 let code = encoder.encode(&op).unwrap();
11247 assert_eq!(code.len(), 2); // 16-bit
11248
11249 // UDF #0: 0xDE00 in little-endian
11250 assert_eq!(code, vec![0x00, 0xDE]);
11251 }
11252
11253 /// #610: the i64 rot/div/rem expansions must land the result in the
11254 /// selector-assigned rd pair and leave R0-R3 preserved (restored from the
11255 /// fixed-ABI wrapper's save area) — pre-#610 the rot expansion's own
11256 /// `POP {R4}` restored stale scratch OVER the result (rd_lo == R4) and
11257 /// the div/rem expansions ignored their register fields outright.
11258 #[test]
11259 fn test_610_i64_rot_expansion_ends_with_rd_movs_and_restore() {
11260 let encoder = ArmEncoder::new_thumb2();
11261 for op in [
11262 ArmOp::I64Rotl {
11263 rdlo: Reg::R4,
11264 rdhi: Reg::R5,
11265 rnlo: Reg::R0,
11266 rnhi: Reg::R1,
11267 shift: Reg::R2,
11268 },
11269 ArmOp::I64Rotr {
11270 rdlo: Reg::R4,
11271 rdhi: Reg::R5,
11272 rnlo: Reg::R0,
11273 rnhi: Reg::R1,
11274 shift: Reg::R2,
11275 },
11276 ] {
11277 let code = encoder.encode(&op).unwrap();
11278 assert_eq!(code.len(), 102, "register-independent size (estimator pin)");
11279 // Tail: MOV r5, r1 (0x460D); MOV r4, r0 (0x4604); POP {r0..r3}
11280 // (rd pair r4:r5 does not overlap the save area — all 4 restored).
11281 let tail: Vec<u16> = code[code.len() - 12..]
11282 .chunks(2)
11283 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11284 .collect();
11285 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11286 }
11287 }
11288
11289 /// #610: div/rem expansions honor rd and carry the divide-by-zero trap
11290 /// guard (`ORRS R12, R2, R3; BNE +0; UDF #0`) after operand marshaling.
11291 #[test]
11292 fn test_610_i64_div_rem_expansion_guard_and_rd() {
11293 let encoder = ArmEncoder::new_thumb2();
11294 let mk = |which: u8| {
11295 let (rdlo, rdhi, rnlo, rnhi, rmlo, rmhi) =
11296 (Reg::R4, Reg::R5, Reg::R0, Reg::R1, Reg::R2, Reg::R3);
11297 match which {
11298 0 => ArmOp::I64DivU {
11299 rdlo,
11300 rdhi,
11301 rnlo,
11302 rnhi,
11303 rmlo,
11304 rmhi,
11305 elide_zero_guard: false,
11306 },
11307 1 => ArmOp::I64RemU {
11308 rdlo,
11309 rdhi,
11310 rnlo,
11311 rnhi,
11312 rmlo,
11313 rmhi,
11314 elide_zero_guard: false,
11315 },
11316 2 => ArmOp::I64DivS {
11317 rdlo,
11318 rdhi,
11319 rnlo,
11320 rnhi,
11321 rmlo,
11322 rmhi,
11323 elide_zero_guard: false,
11324 elide_overflow_guard: false,
11325 },
11326 _ => ArmOp::I64RemS {
11327 rdlo,
11328 rdhi,
11329 rnlo,
11330 rnhi,
11331 rmlo,
11332 rmhi,
11333 elide_zero_guard: false,
11334 },
11335 }
11336 };
11337 for which in 0..4u8 {
11338 let code = encoder.encode(&mk(which)).unwrap();
11339 // Zero-divisor trap guard right after the 26-byte marshal prologue.
11340 let guard: Vec<u16> = code[26..34]
11341 .chunks(2)
11342 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11343 .collect();
11344 assert_eq!(
11345 guard,
11346 vec![0xEA52, 0x0C03, 0xD100, 0xDE00],
11347 "ORRS R12,R2,R3; BNE +0; UDF #0"
11348 );
11349 // Tail: result into rd pair (r5:r4), then restore all of R0-R3.
11350 let tail: Vec<u16> = code[code.len() - 12..]
11351 .chunks(2)
11352 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11353 .collect();
11354 assert_eq!(tail, vec![0x460D, 0x4604, 0xBC01, 0xBC02, 0xBC04, 0xBC08]);
11355 }
11356 }
11357
11358 /// #610: when rd overlaps R0-R3 the restore must SKIP the result
11359 /// registers (drop the saved caller word) instead of popping over them.
11360 #[test]
11361 fn test_610_i64_divu_rd_in_r0_r1_skips_restore() {
11362 let encoder = ArmEncoder::new_thumb2();
11363 let code = encoder
11364 .encode(&ArmOp::I64DivU {
11365 rdlo: Reg::R0,
11366 rdhi: Reg::R1,
11367 rnlo: Reg::R0,
11368 rnhi: Reg::R1,
11369 rmlo: Reg::R2,
11370 rmhi: Reg::R3,
11371 elide_zero_guard: false,
11372 })
11373 .unwrap();
11374 let tail: Vec<u16> = code[code.len() - 12..]
11375 .chunks(2)
11376 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11377 .collect();
11378 // MOV r1,r1 / MOV r0,r0 (no-ops, size-stable), ADD SP,#4 twice
11379 // (discard saved r0/r1 — the result lives there), POP {r2}, POP {r3}.
11380 assert_eq!(tail, vec![0x4609, 0x4600, 0xB001, 0xB001, 0xBC04, 0xBC08]);
11381 }
11382
11383 /// #610: a fully swapped rd pair (rd_lo=R1, rd_hi=R0) cannot be
11384 /// materialized by two MOVs in either order — must be a loud Err, never
11385 /// silent corruption. (Selector pairs are consecutive, so unreachable.)
11386 #[test]
11387 fn test_610_i64_swapped_rd_pair_rejected() {
11388 let encoder = ArmEncoder::new_thumb2();
11389 let result = encoder.encode(&ArmOp::I64RemU {
11390 rdlo: Reg::R1,
11391 rdhi: Reg::R0,
11392 rnlo: Reg::R2,
11393 rnhi: Reg::R3,
11394 rmlo: Reg::R4,
11395 rmhi: Reg::R5,
11396 elide_zero_guard: false,
11397 });
11398 assert!(result.is_err(), "swapped rd pair must be rejected loudly");
11399 }
11400
11401 /// #632: the I64Popcnt expansion's own scratch restore (`POP {R3,R4,R5}`)
11402 /// must not clobber the result. Pre-fix the total was materialized with
11403 /// `ADDS rd, R4, R5` BEFORE the pop, so any allocator-assigned
11404 /// rd ∈ {R3,R4,R5} received stale stack garbage. Post-fix the count is
11405 /// carried across the restore in R12 (never allocatable, never restored)
11406 /// and moved into rd only after the pop — structurally rd-independent.
11407 #[test]
11408 fn test_632_i64_popcnt_result_survives_scratch_restore() {
11409 let encoder = ArmEncoder::new_thumb2();
11410 // Every allocatable rd, including the restore set {R3,R4,R5} and R8.
11411 for rd in [
11412 Reg::R0,
11413 Reg::R2,
11414 Reg::R3,
11415 Reg::R4,
11416 Reg::R5,
11417 Reg::R6,
11418 Reg::R8,
11419 ] {
11420 let code = encoder
11421 .encode(&ArmOp::I64Popcnt {
11422 rd,
11423 rnlo: Reg::R6,
11424 rnhi: Reg::R7,
11425 })
11426 .unwrap();
11427 assert_eq!(code.len(), 176, "register-independent size (estimator pin)");
11428 let hw: Vec<u16> = code
11429 .chunks(2)
11430 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11431 .collect();
11432 let pop = hw
11433 .iter()
11434 .position(|&h| h == 0xBC38)
11435 .expect("POP {R3,R4,R5} present");
11436 // Immediately before the POP: ADD.W R12, R4, R5 (the total lives
11437 // in R12, which the POP cannot touch).
11438 assert_eq!(
11439 &hw[pop - 2..pop],
11440 &[0xEB04, 0x0C05],
11441 "total must be carried in R12 across the restore"
11442 );
11443 // Immediately after the POP: MOV rd, R12.
11444 let rd_bits = match rd {
11445 Reg::R8 => 8u16,
11446 Reg::R6 => 6,
11447 Reg::R5 => 5,
11448 Reg::R4 => 4,
11449 Reg::R3 => 3,
11450 Reg::R2 => 2,
11451 _ => 0,
11452 };
11453 let expect_mov = 0x4600 | (((rd_bits >> 3) & 1) << 7) | (12 << 3) | (rd_bits & 7);
11454 assert_eq!(hw[pop + 1], expect_mov, "MOV rd, R12 after the restore");
11455 // No write into rd between the PUSH and the POP (the old
11456 // pre-restore ADDS is gone).
11457 assert!(
11458 !hw[..pop].contains(&(0x1800 | (5 << 6) | (4 << 3) | rd_bits)),
11459 "no ADDS rd, R4, R5 before the restore pop"
11460 );
11461 }
11462 }
11463
11464 /// #632 audit: the entry marshal must be permutation-safe. Pre-fix
11465 /// `MOV R4, rnlo; MOV R5, rnhi` read a clobbered R4 when the operand
11466 /// pair lived at (R3, R4). Post-fix rnlo routes through R12.
11467 #[test]
11468 fn test_632_i64_popcnt_marshal_pair_at_r3_r4() {
11469 let encoder = ArmEncoder::new_thumb2();
11470 let code = encoder
11471 .encode(&ArmOp::I64Popcnt {
11472 rd: Reg::R0,
11473 rnlo: Reg::R3,
11474 rnhi: Reg::R4,
11475 })
11476 .unwrap();
11477 let hw: Vec<u16> = code
11478 .chunks(2)
11479 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11480 .collect();
11481 // PUSH {R3,R4,R5}; MOV R12, R3; MOV R5, R4 (rnhi read BEFORE any
11482 // write to R4); MOV R4, R12.
11483 assert_eq!(hw[0], 0xB438);
11484 assert_eq!(hw[1], 0x4600 | (1 << 7) | (3 << 3) | 4, "MOV R12, rnlo");
11485 assert_eq!(hw[2], 0x4600 | (4 << 3) | 5, "MOV R5, rnhi");
11486 assert_eq!(hw[3], 0x4664, "MOV R4, R12");
11487 }
11488
11489 /// #632: A32 twin — same structural fix on the ARM-mode path
11490 /// (`--target cortex-r5`): total carried in R12 across the restore.
11491 #[test]
11492 fn test_632_a32_i64_popcnt_result_survives_scratch_restore() {
11493 let encoder = ArmEncoder::new_arm32();
11494 for rd in [Reg::R0, Reg::R3, Reg::R4, Reg::R5, Reg::R8] {
11495 let code = encoder
11496 .encode(&ArmOp::I64Popcnt {
11497 rd,
11498 rnlo: Reg::R6,
11499 rnhi: Reg::R7,
11500 })
11501 .unwrap();
11502 let words: Vec<u32> = code
11503 .chunks(4)
11504 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11505 .collect();
11506 let pop = words
11507 .iter()
11508 .position(|&w| w == 0xE8BD_0038)
11509 .expect("POP {R3,R4,R5} present");
11510 assert_eq!(words[pop - 1], 0xE084_C005, "ADD R12, R4, R5 before POP");
11511 let rd_bits = match rd {
11512 Reg::R8 => 8u32,
11513 Reg::R5 => 5,
11514 Reg::R4 => 4,
11515 Reg::R3 => 3,
11516 _ => 0,
11517 };
11518 assert_eq!(
11519 words[pop + 1],
11520 0xE1A0_0000 | (rd_bits << 12) | 12,
11521 "MOV rd, R12 after the restore"
11522 );
11523 }
11524 }
11525
11526 /// #633: I64DivS must carry the INT64_MIN/-1 overflow guard (mirroring
11527 /// the i32 path) right after the zero-divisor guard — dividend in R0:R1,
11528 /// divisor in R2:R3 on the #610/#613 fixed-ABI wrapper path.
11529 #[test]
11530 fn test_633_i64_divs_overflow_guard_emitted() {
11531 let encoder = ArmEncoder::new_thumb2();
11532 let code = encoder
11533 .encode(&ArmOp::I64DivS {
11534 rdlo: Reg::R4,
11535 rdhi: Reg::R5,
11536 rnlo: Reg::R0,
11537 rnhi: Reg::R1,
11538 rmlo: Reg::R2,
11539 rmhi: Reg::R3,
11540 elide_zero_guard: false,
11541 elide_overflow_guard: false,
11542 })
11543 .unwrap();
11544 // 26-byte marshal + 8-byte zero-trap, then the 22-byte overflow guard.
11545 let guard: Vec<u16> = code[34..56]
11546 .chunks(2)
11547 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11548 .collect();
11549 assert_eq!(
11550 guard,
11551 vec![
11552 0xEA02, 0x0C03, // AND.W R12, R2, R3
11553 0xF11C, 0x0F01, // CMN.W R12, #1
11554 0xD105, // BNE .no_trap
11555 0x2800, // CMP R0, #0
11556 0xD103, // BNE .no_trap
11557 0xF1B1, 0x4F00, // CMP.W R1, #0x80000000
11558 0xD100, // BNE .no_trap
11559 0xDE00, // UDF #0 — signed-division overflow
11560 ],
11561 "INT64_MIN/-1 overflow guard after the zero-divisor guard"
11562 );
11563 }
11564
11565 /// #633 fix-guard twin: I64RemS must NOT carry the overflow guard —
11566 /// rem_s(INT64_MIN, -1) is defined as 0 and must not trap. Exactly one
11567 /// UDF (the zero-divisor trap) in the whole expansion.
11568 #[test]
11569 fn test_633_i64_rems_has_no_overflow_guard() {
11570 let encoder = ArmEncoder::new_thumb2();
11571 for (is_rem_s, op) in [
11572 (
11573 true,
11574 ArmOp::I64RemS {
11575 rdlo: Reg::R4,
11576 rdhi: Reg::R5,
11577 rnlo: Reg::R0,
11578 rnhi: Reg::R1,
11579 rmlo: Reg::R2,
11580 rmhi: Reg::R3,
11581 elide_zero_guard: false,
11582 },
11583 ),
11584 (
11585 false,
11586 ArmOp::I64DivS {
11587 rdlo: Reg::R4,
11588 rdhi: Reg::R5,
11589 rnlo: Reg::R0,
11590 rnhi: Reg::R1,
11591 rmlo: Reg::R2,
11592 rmhi: Reg::R3,
11593 elide_zero_guard: false,
11594 elide_overflow_guard: false,
11595 },
11596 ),
11597 ] {
11598 let code = encoder.encode(&op).unwrap();
11599 let udfs = code
11600 .chunks(2)
11601 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11602 .count();
11603 let want = if is_rem_s { 1 } else { 2 };
11604 assert_eq!(
11605 udfs, want,
11606 "rem_s: zero-trap only; div_s: zero-trap + overflow trap"
11607 );
11608 }
11609 }
11610
11611 /// #494 phase 2b: `elide_zero_guard` drops EXACTLY the 8-byte fused
11612 /// zero-trap (`ORRS.W R12,R2,R3; BNE; UDF #0`) and nothing else — the
11613 /// rest of the expansion is byte-identical (splice check).
11614 #[test]
11615 fn test_494_i64_zero_guard_elision_is_exact_splice() {
11616 let encoder = ArmEncoder::new_thumb2();
11617 let mk = |elide_zero_guard: bool| {
11618 encoder
11619 .encode(&ArmOp::I64DivU {
11620 rdlo: Reg::R4,
11621 rdhi: Reg::R5,
11622 rnlo: Reg::R0,
11623 rnhi: Reg::R1,
11624 rmlo: Reg::R2,
11625 rmhi: Reg::R3,
11626 elide_zero_guard,
11627 })
11628 .unwrap()
11629 };
11630 let full = mk(false);
11631 let elided = mk(true);
11632 assert_eq!(full.len(), elided.len() + 8, "zero guard is 8 bytes");
11633 // Marshal prologue (26 B) unchanged, guard (8 B) gone, tail identical.
11634 assert_eq!(&full[..26], &elided[..26]);
11635 assert_eq!(
11636 &full[26..34],
11637 &[0x52, 0xEA, 0x03, 0x0C, 0x00, 0xD1, 0x00, 0xDE],
11638 "the spliced-out bytes are exactly ORRS.W; BNE; UDF #0"
11639 );
11640 assert_eq!(&full[34..], &elided[26..]);
11641 }
11642
11643 /// #494 phase 2b two-guard distinction (the #633/#634 synergy): a
11644 /// divisor-nonzero fact elides ONLY the zero guard — the INT64_MIN/-1
11645 /// OVERFLOW guard is a separate obligation and must survive
11646 /// `elide_zero_guard: true`. Pinned on div_s in all flag states.
11647 #[test]
11648 fn test_494_i64_divs_overflow_guard_retained_when_only_zero_elided() {
11649 let encoder = ArmEncoder::new_thumb2();
11650 let mk = |zero: bool, ovf: bool| {
11651 encoder
11652 .encode(&ArmOp::I64DivS {
11653 rdlo: Reg::R4,
11654 rdhi: Reg::R5,
11655 rnlo: Reg::R0,
11656 rnhi: Reg::R1,
11657 rmlo: Reg::R2,
11658 rmhi: Reg::R3,
11659 elide_zero_guard: zero,
11660 elide_overflow_guard: ovf,
11661 })
11662 .unwrap()
11663 };
11664 let udf_count = |code: &[u8]| {
11665 code.chunks(2)
11666 .filter(|c| u16::from_le_bytes([c[0], c[1]]) == 0xDE00)
11667 .count()
11668 };
11669 let full = mk(false, false);
11670 let zero_only = mk(true, false);
11671 let both = mk(true, true);
11672 assert_eq!(udf_count(&full), 2, "baseline: zero trap + overflow trap");
11673 assert_eq!(
11674 udf_count(&zero_only),
11675 1,
11676 "divisor-nonzero elides the zero trap ONLY — the #633 overflow \
11677 guard must be retained"
11678 );
11679 // The retained guard is the 22-byte overflow sequence, now right
11680 // after the 26-byte marshal prologue.
11681 let guard: Vec<u16> = zero_only[26..48]
11682 .chunks(2)
11683 .map(|c| u16::from_le_bytes([c[0], c[1]]))
11684 .collect();
11685 assert_eq!(
11686 guard,
11687 vec![
11688 0xEA02, 0x0C03, 0xF11C, 0x0F01, 0xD105, 0x2800, 0xD103, 0xF1B1, 0x4F00, 0xD100,
11689 0xDE00,
11690 ],
11691 "the surviving guard is the INT64_MIN/-1 overflow trap"
11692 );
11693 assert_eq!(full.len(), zero_only.len() + 8);
11694 assert_eq!(zero_only.len(), both.len() + 22);
11695 assert_eq!(udf_count(&both), 0, "both obligations discharged ⇒ no UDF");
11696 }
11697
11698 /// #494 phase 2b A32 twin: zero-guard elision is an exact 12-byte splice
11699 /// and the A32 overflow guard survives a zero-only elision.
11700 #[test]
11701 fn test_494_a32_i64_guard_elision() {
11702 let encoder = ArmEncoder::new_arm32();
11703 let mk = |zero: bool, ovf: bool| {
11704 encoder
11705 .encode(&ArmOp::I64DivS {
11706 rdlo: Reg::R4,
11707 rdhi: Reg::R5,
11708 rnlo: Reg::R0,
11709 rnhi: Reg::R1,
11710 rmlo: Reg::R2,
11711 rmhi: Reg::R3,
11712 elide_zero_guard: zero,
11713 elide_overflow_guard: ovf,
11714 })
11715 .unwrap()
11716 };
11717 let full = mk(false, false);
11718 let zero_only = mk(true, false);
11719 let both = mk(true, true);
11720 // A32 zero guard = 3 words (ORRS/BNE/UDF), overflow guard = 6 words.
11721 assert_eq!(full.len(), zero_only.len() + 12);
11722 assert_eq!(zero_only.len(), both.len() + 24);
11723 let udf_count = |code: &[u8]| {
11724 code.chunks(4)
11725 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11726 .count()
11727 };
11728 assert_eq!(udf_count(&full), 2);
11729 assert_eq!(
11730 udf_count(&zero_only),
11731 1,
11732 "A32: overflow guard retained under zero-only elision"
11733 );
11734 assert_eq!(udf_count(&both), 0);
11735 }
11736
11737 /// #633: A32 twin — the conditional-execution overflow guard on the
11738 /// ARM-mode I64DivS, and its absence from I64RemS.
11739 #[test]
11740 fn test_633_a32_i64_divs_overflow_guard() {
11741 let encoder = ArmEncoder::new_arm32();
11742 let mk_divs = ArmOp::I64DivS {
11743 rdlo: Reg::R4,
11744 rdhi: Reg::R5,
11745 rnlo: Reg::R0,
11746 rnhi: Reg::R1,
11747 rmlo: Reg::R2,
11748 rmhi: Reg::R3,
11749 elide_zero_guard: false,
11750 elide_overflow_guard: false,
11751 };
11752 let code = encoder.encode(&mk_divs).unwrap();
11753 let words: Vec<u32> = code
11754 .chunks(4)
11755 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
11756 .collect();
11757 let guard = [
11758 0xE002_C003u32, // AND R12, R2, R3
11759 0xE37C_0001, // CMN R12, #1
11760 0x0350_0000, // CMPEQ R0, #0
11761 0x0351_0102, // CMPEQ R1, #0x80000000
11762 0x1A00_0000, // BNE +1 insn
11763 0xE7F0_00F0, // UDF #0
11764 ];
11765 assert!(
11766 words.windows(6).any(|w| w == guard),
11767 "A32 I64DivS carries the INT64_MIN/-1 overflow guard"
11768 );
11769 let rems = encoder
11770 .encode(&ArmOp::I64RemS {
11771 rdlo: Reg::R4,
11772 rdhi: Reg::R5,
11773 rnlo: Reg::R0,
11774 rnhi: Reg::R1,
11775 rmlo: Reg::R2,
11776 rmhi: Reg::R3,
11777 elide_zero_guard: false,
11778 })
11779 .unwrap();
11780 let rems_udfs = rems
11781 .chunks(4)
11782 .filter(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]) == 0xE7F0_00F0)
11783 .count();
11784 assert_eq!(rems_udfs, 1, "A32 I64RemS keeps only the zero-divisor trap");
11785 }
11786
11787 #[test]
11788 fn test_encode_nop_thumb2() {
11789 let encoder = ArmEncoder::new_thumb2();
11790 let op = ArmOp::Nop;
11791 let code = encoder.encode(&op).unwrap();
11792 assert_eq!(code.len(), 2); // 16-bit
11793
11794 // NOP: 0xBF00 in little-endian
11795 assert_eq!(code, vec![0x00, 0xBF]);
11796 }
11797
11798 // =========================================================================
11799 // i64 Thumb-2 encoding tests
11800 // =========================================================================
11801
11802 #[test]
11803 fn test_encode_i64_add_thumb2() {
11804 let encoder = ArmEncoder::new_thumb2();
11805 let op = ArmOp::I64Add {
11806 rdlo: Reg::R0,
11807 rdhi: Reg::R1,
11808 rnlo: Reg::R0,
11809 rnhi: Reg::R1,
11810 rmlo: Reg::R2,
11811 rmhi: Reg::R3,
11812 };
11813 let code = encoder.encode(&op).unwrap();
11814 // Should emit ADDS (2 bytes) + ADC.W (4 bytes) = 6 bytes
11815 assert_eq!(code.len(), 6, "I64Add should be 6 bytes (ADDS + ADC.W)");
11816 }
11817
11818 #[test]
11819 fn test_encode_i64_sub_thumb2() {
11820 let encoder = ArmEncoder::new_thumb2();
11821 let op = ArmOp::I64Sub {
11822 rdlo: Reg::R0,
11823 rdhi: Reg::R1,
11824 rnlo: Reg::R0,
11825 rnhi: Reg::R1,
11826 rmlo: Reg::R2,
11827 rmhi: Reg::R3,
11828 };
11829 let code = encoder.encode(&op).unwrap();
11830 // Should emit SUBS (2 bytes) + SBC.W (4 bytes) = 6 bytes
11831 assert_eq!(code.len(), 6, "I64Sub should be 6 bytes (SUBS + SBC.W)");
11832 }
11833
11834 #[test]
11835 fn test_encode_i64_and_thumb2() {
11836 let encoder = ArmEncoder::new_thumb2();
11837 let op = ArmOp::I64And {
11838 rdlo: Reg::R0,
11839 rdhi: Reg::R1,
11840 rnlo: Reg::R0,
11841 rnhi: Reg::R1,
11842 rmlo: Reg::R2,
11843 rmhi: Reg::R3,
11844 };
11845 let code = encoder.encode(&op).unwrap();
11846 // AND.W (4 bytes) + AND.W (4 bytes) = 8 bytes
11847 assert!(code.len() >= 4, "I64And should emit at least 4 bytes");
11848 }
11849
11850 #[test]
11851 fn test_encode_i64_or_thumb2() {
11852 let encoder = ArmEncoder::new_thumb2();
11853 let op = ArmOp::I64Or {
11854 rdlo: Reg::R0,
11855 rdhi: Reg::R1,
11856 rnlo: Reg::R0,
11857 rnhi: Reg::R1,
11858 rmlo: Reg::R2,
11859 rmhi: Reg::R3,
11860 };
11861 let code = encoder.encode(&op).unwrap();
11862 assert!(code.len() >= 4, "I64Or should emit at least 4 bytes");
11863 }
11864
11865 #[test]
11866 fn test_encode_i64_xor_thumb2() {
11867 let encoder = ArmEncoder::new_thumb2();
11868 let op = ArmOp::I64Xor {
11869 rdlo: Reg::R0,
11870 rdhi: Reg::R1,
11871 rnlo: Reg::R0,
11872 rnhi: Reg::R1,
11873 rmlo: Reg::R2,
11874 rmhi: Reg::R3,
11875 };
11876 let code = encoder.encode(&op).unwrap();
11877 assert!(code.len() >= 4, "I64Xor should emit at least 4 bytes");
11878 }
11879
11880 #[test]
11881 fn test_encode_i64_const_small_thumb2() {
11882 let encoder = ArmEncoder::new_thumb2();
11883 // Small constant: only needs MOVW for each half
11884 let op = ArmOp::I64Const {
11885 rdlo: Reg::R0,
11886 rdhi: Reg::R1,
11887 value: 42,
11888 };
11889 let code = encoder.encode(&op).unwrap();
11890 // MOVW R0, #42 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes minimum
11891 assert!(code.len() >= 8, "I64Const should emit at least 8 bytes");
11892 }
11893
11894 #[test]
11895 fn test_encode_i64_const_large_thumb2() {
11896 let encoder = ArmEncoder::new_thumb2();
11897 // Large constant: needs MOVW+MOVT for each half
11898 let op = ArmOp::I64Const {
11899 rdlo: Reg::R0,
11900 rdhi: Reg::R1,
11901 value: 0x1234_5678_9ABC_DEF0_u64 as i64,
11902 };
11903 let code = encoder.encode(&op).unwrap();
11904 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
11905 assert_eq!(
11906 code.len(),
11907 16,
11908 "I64Const with large value should be 16 bytes"
11909 );
11910 }
11911
11912 #[test]
11913 fn test_encode_i64_extend_i32_s_thumb2() {
11914 let encoder = ArmEncoder::new_thumb2();
11915 let op = ArmOp::I64ExtendI32S {
11916 rdlo: Reg::R0,
11917 rdhi: Reg::R1,
11918 rn: Reg::R0,
11919 };
11920 let code = encoder.encode(&op).unwrap();
11921 // When rdlo == rn, only ASR (4 bytes) is emitted
11922 assert_eq!(
11923 code.len(),
11924 4,
11925 "I64ExtendI32S (same reg) should be 4 bytes (ASR only)"
11926 );
11927 }
11928
11929 #[test]
11930 fn test_encode_i64_extend_i32_s_diff_reg_thumb2() {
11931 let encoder = ArmEncoder::new_thumb2();
11932 let op = ArmOp::I64ExtendI32S {
11933 rdlo: Reg::R0,
11934 rdhi: Reg::R1,
11935 rn: Reg::R2,
11936 };
11937 let code = encoder.encode(&op).unwrap();
11938 // MOV rdlo, rn (2 bytes for low regs) + ASR rdhi, rdlo, #31 (4 bytes) = 6 bytes
11939 assert!(
11940 code.len() >= 6,
11941 "I64ExtendI32S (diff reg) should be at least 6 bytes"
11942 );
11943 }
11944
11945 #[test]
11946 fn test_encode_i64_extend_i32_u_thumb2() {
11947 let encoder = ArmEncoder::new_thumb2();
11948 let op = ArmOp::I64ExtendI32U {
11949 rdlo: Reg::R0,
11950 rdhi: Reg::R1,
11951 rn: Reg::R0,
11952 };
11953 let code = encoder.encode(&op).unwrap();
11954 // When rdlo == rn, only MOV rdhi, #0 (2 bytes) is emitted
11955 assert_eq!(
11956 code.len(),
11957 2,
11958 "I64ExtendI32U (same reg) should be 2 bytes (MOV #0 only)"
11959 );
11960 }
11961
11962 #[test]
11963 fn test_encode_i32_wrap_i64_nop_thumb2() {
11964 let encoder = ArmEncoder::new_thumb2();
11965 // When rd == rnlo, should be a NOP
11966 let op = ArmOp::I32WrapI64 {
11967 rd: Reg::R0,
11968 rnlo: Reg::R0,
11969 };
11970 let code = encoder.encode(&op).unwrap();
11971 assert_eq!(code.len(), 2, "I32WrapI64 same reg should be NOP (2 bytes)");
11972 assert_eq!(code, vec![0x00, 0xBF]); // NOP
11973 }
11974
11975 #[test]
11976 fn test_encode_i32_wrap_i64_diff_reg_thumb2() {
11977 let encoder = ArmEncoder::new_thumb2();
11978 let op = ArmOp::I32WrapI64 {
11979 rd: Reg::R2,
11980 rnlo: Reg::R0,
11981 };
11982 let code = encoder.encode(&op).unwrap();
11983 // MOV R2, R0 (2 or 4 bytes)
11984 assert!(
11985 code.len() >= 2,
11986 "I32WrapI64 diff reg should emit at least 2 bytes"
11987 );
11988 }
11989
11990 #[test]
11991 fn test_encode_i64_eqz_thumb2() {
11992 let encoder = ArmEncoder::new_thumb2();
11993 let op = ArmOp::I64Eqz {
11994 rd: Reg::R0,
11995 rnlo: Reg::R0,
11996 rnhi: Reg::R1,
11997 };
11998 let code = encoder.encode(&op).unwrap();
11999 // Delegates to I64SetCondZ which is already encoded
12000 assert!(
12001 code.len() >= 6,
12002 "I64Eqz should emit at least 6 bytes for ORR+ITE+MOV+MOV"
12003 );
12004 }
12005
12006 #[test]
12007 fn test_encode_i64_eq_thumb2() {
12008 let encoder = ArmEncoder::new_thumb2();
12009 let op = ArmOp::I64Eq {
12010 rd: Reg::R0,
12011 rnlo: Reg::R0,
12012 rnhi: Reg::R1,
12013 rmlo: Reg::R2,
12014 rmhi: Reg::R3,
12015 };
12016 let code = encoder.encode(&op).unwrap();
12017 // Delegates to I64SetCond EQ: CMP lo + IT EQ + CMPEQ hi + ITE EQ + MOV 1 + MOV 0
12018 assert!(code.len() >= 10, "I64Eq should emit at least 10 bytes");
12019 }
12020
12021 #[test]
12022 fn test_encode_i64_ldr_thumb2() {
12023 let encoder = ArmEncoder::new_thumb2();
12024 let op = ArmOp::I64Ldr {
12025 rdlo: Reg::R0,
12026 rdhi: Reg::R1,
12027 addr: MemAddr::imm(Reg::SP, 0),
12028 };
12029 let code = encoder.encode(&op).unwrap();
12030 // Two LDR instructions (lo at offset, hi at offset+4)
12031 assert!(code.len() >= 4, "I64Ldr should emit at least 4 bytes");
12032 }
12033
12034 #[test]
12035 fn test_372_i64_ldr_indexed_materializes_address() {
12036 // #372: a memory i64.load carries an index register (R11 + addr + off).
12037 // The encoder must materialize `ip = base + index` (ADD.W) and load via
12038 // `[ip,#off]` — NOT drop the index. A frame (non-indexed) i64.load must
12039 // stay byte-identical (plain `[base,#off]`, no ADD).
12040 let encoder = ArmEncoder::new_thumb2();
12041 let indexed = encoder
12042 .encode(&ArmOp::I64Ldr {
12043 rdlo: Reg::R0,
12044 rdhi: Reg::R1,
12045 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 0),
12046 })
12047 .unwrap();
12048 // ADD.W ip, fp, r0 = eb0b 0c00 (byte-verified vs arm-none-eabi-as).
12049 assert_eq!(
12050 &indexed[0..4],
12051 &[0x0b, 0xeb, 0x00, 0x0c],
12052 "indexed I64Ldr must start with ADD.W ip, base, index"
12053 );
12054 let frame = encoder
12055 .encode(&ArmOp::I64Ldr {
12056 rdlo: Reg::R0,
12057 rdhi: Reg::R1,
12058 addr: MemAddr::imm(Reg::SP, 8),
12059 })
12060 .unwrap();
12061 // No index -> no ADD.W prefix (byte-identical frame access).
12062 assert_ne!(
12063 &frame[0..2],
12064 &[0x0b, 0xeb],
12065 "frame (non-indexed) I64Ldr must NOT emit an ADD.W"
12066 );
12067 }
12068
12069 #[test]
12070 fn test_382_i64_ldst_large_offset_materializes_not_skips() {
12071 // #382: an indexed i64.load/store whose static offset > 0xFFF must
12072 // MATERIALIZE the offset into the base — NOT return Err (skip the fn).
12073 // Sequence for reg_imm(R11, R0, 5000): MOVW ip,#5000 ; ADD ip,r0,ip ;
12074 // ADD ip,ip,fp ; LDR/STR halves at [ip,#0] / [ip,#4]. Byte-verified tail
12075 // vs arm-none-eabi-as.
12076 let encoder = ArmEncoder::new_thumb2();
12077 // 0x1388 > 0xFFF (MemAddr is not Copy, so build it per use).
12078
12079 let ld = encoder
12080 .encode(&ArmOp::I64Ldr {
12081 rdlo: Reg::R0,
12082 rdhi: Reg::R1,
12083 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12084 })
12085 .expect("large-offset i64.load must lower, not skip");
12086 // MOVW ip,#0x1388 (4) + ADD ip,r0,ip (4) + ADD ip,ip,fp (4) + 2 LDR (8).
12087 assert_eq!(ld.len(), 20, "expected MOVW + 2×ADD + 2×LDR");
12088 // Must NOT be the small-offset `ADD.W ip, fp, r0` (0x0b 0xeb) prefix —
12089 // that path can only reach imm12 offsets.
12090 assert_ne!(
12091 &ld[0..2],
12092 &[0x0b, 0xeb],
12093 "must materialize the large offset"
12094 );
12095 // Effective base built in ip, then halves at [ip,#0] / [ip,#4].
12096 assert_eq!(
12097 &ld[4..20],
12098 &[
12099 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12100 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12101 0xdc, 0xf8, 0x00, 0x00, // LDR.W r0, [ip, #0]
12102 0xdc, 0xf8, 0x04, 0x10, // LDR.W r1, [ip, #4]
12103 ],
12104 "large-offset i64.load must fold offset into ip and access [ip,#0]/[ip,#4]"
12105 );
12106
12107 // Store: same base materialization, STR halves.
12108 let st = encoder
12109 .encode(&ArmOp::I64Str {
12110 rdlo: Reg::R2,
12111 rdhi: Reg::R3,
12112 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 5000),
12113 })
12114 .expect("large-offset i64.store must lower, not skip");
12115 assert_eq!(st.len(), 20);
12116 assert_eq!(
12117 &st[4..20],
12118 &[
12119 0x00, 0xeb, 0x0c, 0x0c, // ADD.W ip, r0, ip
12120 0x0c, 0xeb, 0x0b, 0x0c, // ADD.W ip, ip, fp
12121 0xcc, 0xf8, 0x00, 0x20, // STR.W r2, [ip, #0]
12122 0xcc, 0xf8, 0x04, 0x30, // STR.W r3, [ip, #4]
12123 ],
12124 "large-offset i64.store must fold offset into ip and access [ip,#0]/[ip,#4]"
12125 );
12126
12127 // Small-offset (imm12) indexed access stays byte-identical (#372): the
12128 // effective base is a single `ADD.W ip, fp, r0` and the halves keep the
12129 // folded immediates — NO extra MOVW/ADD.
12130 let small = encoder
12131 .encode(&ArmOp::I64Ldr {
12132 rdlo: Reg::R0,
12133 rdhi: Reg::R1,
12134 addr: MemAddr::reg_imm(Reg::R11, Reg::R0, 8),
12135 })
12136 .unwrap();
12137 assert_eq!(
12138 &small[0..4],
12139 &[0x0b, 0xeb, 0x00, 0x0c],
12140 "small-offset indexed i64 must keep the single ADD.W ip, fp, r0"
12141 );
12142 assert_eq!(small.len(), 12, "ADD.W + 2×LDR.W (offset folded in imm12)");
12143 }
12144
12145 #[test]
12146 fn test_encode_i64_str_thumb2() {
12147 let encoder = ArmEncoder::new_thumb2();
12148 let op = ArmOp::I64Str {
12149 rdlo: Reg::R0,
12150 rdhi: Reg::R1,
12151 addr: MemAddr::imm(Reg::SP, 0),
12152 };
12153 let code = encoder.encode(&op).unwrap();
12154 // Two STR instructions (lo at offset, hi at offset+4)
12155 assert!(code.len() >= 4, "I64Str should emit at least 4 bytes");
12156 }
12157
12158 #[test]
12159 fn test_encode_i64_all_comparisons_thumb2() {
12160 let encoder = ArmEncoder::new_thumb2();
12161
12162 let ops = vec![
12163 ArmOp::I64Ne {
12164 rd: Reg::R0,
12165 rnlo: Reg::R0,
12166 rnhi: Reg::R1,
12167 rmlo: Reg::R2,
12168 rmhi: Reg::R3,
12169 },
12170 ArmOp::I64LtS {
12171 rd: Reg::R0,
12172 rnlo: Reg::R0,
12173 rnhi: Reg::R1,
12174 rmlo: Reg::R2,
12175 rmhi: Reg::R3,
12176 },
12177 ArmOp::I64LtU {
12178 rd: Reg::R0,
12179 rnlo: Reg::R0,
12180 rnhi: Reg::R1,
12181 rmlo: Reg::R2,
12182 rmhi: Reg::R3,
12183 },
12184 ArmOp::I64LeS {
12185 rd: Reg::R0,
12186 rnlo: Reg::R0,
12187 rnhi: Reg::R1,
12188 rmlo: Reg::R2,
12189 rmhi: Reg::R3,
12190 },
12191 ArmOp::I64LeU {
12192 rd: Reg::R0,
12193 rnlo: Reg::R0,
12194 rnhi: Reg::R1,
12195 rmlo: Reg::R2,
12196 rmhi: Reg::R3,
12197 },
12198 ArmOp::I64GtS {
12199 rd: Reg::R0,
12200 rnlo: Reg::R0,
12201 rnhi: Reg::R1,
12202 rmlo: Reg::R2,
12203 rmhi: Reg::R3,
12204 },
12205 ArmOp::I64GtU {
12206 rd: Reg::R0,
12207 rnlo: Reg::R0,
12208 rnhi: Reg::R1,
12209 rmlo: Reg::R2,
12210 rmhi: Reg::R3,
12211 },
12212 ArmOp::I64GeS {
12213 rd: Reg::R0,
12214 rnlo: Reg::R0,
12215 rnhi: Reg::R1,
12216 rmlo: Reg::R2,
12217 rmhi: Reg::R3,
12218 },
12219 ArmOp::I64GeU {
12220 rd: Reg::R0,
12221 rnlo: Reg::R0,
12222 rnhi: Reg::R1,
12223 rmlo: Reg::R2,
12224 rmhi: Reg::R3,
12225 },
12226 ];
12227
12228 for op in &ops {
12229 let code = encoder.encode(op).unwrap();
12230 assert!(
12231 code.len() >= 8,
12232 "i64 comparison {:?} should emit at least 8 bytes, got {}",
12233 op,
12234 code.len()
12235 );
12236 }
12237 }
12238
12239 #[test]
12240 fn test_encode_i64_const_zero_thumb2() {
12241 let encoder = ArmEncoder::new_thumb2();
12242 let op = ArmOp::I64Const {
12243 rdlo: Reg::R0,
12244 rdhi: Reg::R1,
12245 value: 0,
12246 };
12247 let code = encoder.encode(&op).unwrap();
12248 // MOVW R0, #0 (4 bytes) + MOVW R1, #0 (4 bytes) = 8 bytes
12249 assert_eq!(code.len(), 8, "I64Const(0) should be 8 bytes");
12250 }
12251
12252 #[test]
12253 fn test_encode_i64_const_negative_one_thumb2() {
12254 let encoder = ArmEncoder::new_thumb2();
12255 let op = ArmOp::I64Const {
12256 rdlo: Reg::R0,
12257 rdhi: Reg::R1,
12258 value: -1, // 0xFFFF_FFFF_FFFF_FFFF
12259 };
12260 let code = encoder.encode(&op).unwrap();
12261 // MOVW + MOVT for lo (8 bytes) + MOVW + MOVT for hi (8 bytes) = 16 bytes
12262 assert_eq!(code.len(), 16, "I64Const(-1) should be 16 bytes");
12263 }
12264
12265 // =========================================================================
12266 // Sub-word load/store encoding tests
12267 // =========================================================================
12268
12269 #[test]
12270 fn test_encode_ldrb_arm32() {
12271 let encoder = ArmEncoder::new_arm32();
12272 let op = ArmOp::Ldrb {
12273 rd: Reg::R0,
12274 addr: MemAddr::imm(Reg::R1, 4),
12275 };
12276 let code = encoder.encode(&op).unwrap();
12277 assert_eq!(code.len(), 4, "ARM32 LDRB should be 4 bytes");
12278 // LDRB R0, [R1, #4] = 0xE5D10004
12279 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12280 assert_eq!(encoded, 0xE5D10004, "Should encode LDRB R0, [R1, #4]");
12281 }
12282
12283 #[test]
12284 fn test_encode_strb_arm32() {
12285 let encoder = ArmEncoder::new_arm32();
12286 let op = ArmOp::Strb {
12287 rd: Reg::R0,
12288 addr: MemAddr::imm(Reg::R1, 0),
12289 };
12290 let code = encoder.encode(&op).unwrap();
12291 assert_eq!(code.len(), 4, "ARM32 STRB should be 4 bytes");
12292 // STRB R0, [R1, #0] = 0xE5C10000
12293 let encoded = u32::from_le_bytes([code[0], code[1], code[2], code[3]]);
12294 assert_eq!(encoded, 0xE5C10000, "Should encode STRB R0, [R1, #0]");
12295 }
12296
12297 #[test]
12298 fn test_encode_ldrh_arm32() {
12299 let encoder = ArmEncoder::new_arm32();
12300 let op = ArmOp::Ldrh {
12301 rd: Reg::R0,
12302 addr: MemAddr::imm(Reg::R1, 2),
12303 };
12304 let code = encoder.encode(&op).unwrap();
12305 assert_eq!(code.len(), 4, "ARM32 LDRH should be 4 bytes");
12306 }
12307
12308 #[test]
12309 fn test_encode_strh_arm32() {
12310 let encoder = ArmEncoder::new_arm32();
12311 let op = ArmOp::Strh {
12312 rd: Reg::R0,
12313 addr: MemAddr::imm(Reg::R1, 0),
12314 };
12315 let code = encoder.encode(&op).unwrap();
12316 assert_eq!(code.len(), 4, "ARM32 STRH should be 4 bytes");
12317 }
12318
12319 #[test]
12320 fn test_encode_ldrsb_arm32() {
12321 let encoder = ArmEncoder::new_arm32();
12322 let op = ArmOp::Ldrsb {
12323 rd: Reg::R0,
12324 addr: MemAddr::imm(Reg::R1, 0),
12325 };
12326 let code = encoder.encode(&op).unwrap();
12327 assert_eq!(code.len(), 4, "ARM32 LDRSB should be 4 bytes");
12328 }
12329
12330 #[test]
12331 fn test_encode_ldrsh_arm32() {
12332 let encoder = ArmEncoder::new_arm32();
12333 let op = ArmOp::Ldrsh {
12334 rd: Reg::R0,
12335 addr: MemAddr::imm(Reg::R1, 0),
12336 };
12337 let code = encoder.encode(&op).unwrap();
12338 assert_eq!(code.len(), 4, "ARM32 LDRSH should be 4 bytes");
12339 }
12340
12341 #[test]
12342 fn test_encode_ldrb_thumb2_16bit() {
12343 let encoder = ArmEncoder::new_thumb2();
12344 let op = ArmOp::Ldrb {
12345 rd: Reg::R0,
12346 addr: MemAddr::imm(Reg::R1, 4),
12347 };
12348 let code = encoder.encode(&op).unwrap();
12349 // Low registers + small offset -> 16-bit encoding
12350 assert_eq!(
12351 code.len(),
12352 2,
12353 "Thumb-2 LDRB with small offset should be 16-bit"
12354 );
12355 }
12356
12357 #[test]
12358 fn test_encode_ldrb_thumb2_32bit() {
12359 let encoder = ArmEncoder::new_thumb2();
12360 let op = ArmOp::Ldrb {
12361 rd: Reg::R0,
12362 addr: MemAddr::imm(Reg::R1, 100), // offset > 31 needs 32-bit
12363 };
12364 let code = encoder.encode(&op).unwrap();
12365 assert_eq!(
12366 code.len(),
12367 4,
12368 "Thumb-2 LDRB with large offset should be 32-bit"
12369 );
12370 }
12371
12372 #[test]
12373 fn test_encode_strb_thumb2_16bit() {
12374 let encoder = ArmEncoder::new_thumb2();
12375 let op = ArmOp::Strb {
12376 rd: Reg::R0,
12377 addr: MemAddr::imm(Reg::R1, 10),
12378 };
12379 let code = encoder.encode(&op).unwrap();
12380 assert_eq!(
12381 code.len(),
12382 2,
12383 "Thumb-2 STRB with small offset should be 16-bit"
12384 );
12385 }
12386
12387 #[test]
12388 fn test_encode_ldrh_thumb2_16bit() {
12389 let encoder = ArmEncoder::new_thumb2();
12390 let op = ArmOp::Ldrh {
12391 rd: Reg::R0,
12392 addr: MemAddr::imm(Reg::R1, 4), // offset aligned to 2, <= 62
12393 };
12394 let code = encoder.encode(&op).unwrap();
12395 assert_eq!(
12396 code.len(),
12397 2,
12398 "Thumb-2 LDRH with small aligned offset should be 16-bit"
12399 );
12400 }
12401
12402 #[test]
12403 fn test_encode_strh_thumb2_16bit() {
12404 let encoder = ArmEncoder::new_thumb2();
12405 let op = ArmOp::Strh {
12406 rd: Reg::R0,
12407 addr: MemAddr::imm(Reg::R1, 4),
12408 };
12409 let code = encoder.encode(&op).unwrap();
12410 assert_eq!(
12411 code.len(),
12412 2,
12413 "Thumb-2 STRH with small aligned offset should be 16-bit"
12414 );
12415 }
12416
12417 #[test]
12418 fn test_encode_ldrsb_thumb2() {
12419 let encoder = ArmEncoder::new_thumb2();
12420 let op = ArmOp::Ldrsb {
12421 rd: Reg::R0,
12422 addr: MemAddr::imm(Reg::R1, 0),
12423 };
12424 let code = encoder.encode(&op).unwrap();
12425 // LDRSB has no 16-bit immediate form, always 32-bit
12426 assert_eq!(code.len(), 4, "Thumb-2 LDRSB should be 32-bit");
12427 }
12428
12429 #[test]
12430 fn test_encode_ldrsh_thumb2() {
12431 let encoder = ArmEncoder::new_thumb2();
12432 let op = ArmOp::Ldrsh {
12433 rd: Reg::R0,
12434 addr: MemAddr::imm(Reg::R1, 0),
12435 };
12436 let code = encoder.encode(&op).unwrap();
12437 assert_eq!(code.len(), 4, "Thumb-2 LDRSH should be 32-bit");
12438 }
12439
12440 #[test]
12441 fn test_encode_memory_size_thumb2() {
12442 let encoder = ArmEncoder::new_thumb2();
12443 let op = ArmOp::MemorySize { rd: Reg::R0 };
12444 let code = encoder.encode(&op).unwrap();
12445 // R0 and R10 are not both low registers, so this needs careful handling
12446 assert!(!code.is_empty(), "MemorySize should produce code");
12447 }
12448
12449 #[test]
12450 fn test_encode_memory_grow_thumb2() {
12451 let encoder = ArmEncoder::new_thumb2();
12452 let op = ArmOp::MemoryGrow {
12453 rd: Reg::R0,
12454 rn: Reg::R0,
12455 };
12456 let code = encoder.encode(&op).unwrap();
12457 assert_eq!(code.len(), 4, "MemoryGrow (MVN) should be 32-bit Thumb-2");
12458 }
12459
12460 #[test]
12461 fn test_encode_subword_reg_offset_thumb2() {
12462 let encoder = ArmEncoder::new_thumb2();
12463
12464 // LDRB with register offset
12465 let op = ArmOp::Ldrb {
12466 rd: Reg::R0,
12467 addr: MemAddr::reg(Reg::R1, Reg::R2),
12468 };
12469 let code = encoder.encode(&op).unwrap();
12470 assert_eq!(
12471 code.len(),
12472 4,
12473 "Thumb-2 LDRB with reg offset should be 32-bit"
12474 );
12475
12476 // STRB with register offset
12477 let op = ArmOp::Strb {
12478 rd: Reg::R0,
12479 addr: MemAddr::reg(Reg::R1, Reg::R2),
12480 };
12481 let code = encoder.encode(&op).unwrap();
12482 assert_eq!(
12483 code.len(),
12484 4,
12485 "Thumb-2 STRB with reg offset should be 32-bit"
12486 );
12487
12488 // LDRH with register offset
12489 let op = ArmOp::Ldrh {
12490 rd: Reg::R0,
12491 addr: MemAddr::reg(Reg::R1, Reg::R2),
12492 };
12493 let code = encoder.encode(&op).unwrap();
12494 assert_eq!(
12495 code.len(),
12496 4,
12497 "Thumb-2 LDRH with reg offset should be 32-bit"
12498 );
12499
12500 // STRH with register offset
12501 let op = ArmOp::Strh {
12502 rd: Reg::R0,
12503 addr: MemAddr::reg(Reg::R1, Reg::R2),
12504 };
12505 let code = encoder.encode(&op).unwrap();
12506 assert_eq!(
12507 code.len(),
12508 4,
12509 "Thumb-2 STRH with reg offset should be 32-bit"
12510 );
12511 }
12512
12513 #[test]
12514 fn test_encode_subword_reg_imm_offset_thumb2() {
12515 let encoder = ArmEncoder::new_thumb2();
12516
12517 // LDRB with both register and immediate offset
12518 let op = ArmOp::Ldrb {
12519 rd: Reg::R0,
12520 addr: MemAddr::reg_imm(Reg::R1, Reg::R2, 4),
12521 };
12522 let code = encoder.encode(&op).unwrap();
12523 // ADD R12, R2, #4 (4 bytes) + LDRB R0, [R1, R12] (4 bytes) = 8 bytes
12524 assert_eq!(
12525 code.len(),
12526 8,
12527 "Thumb-2 LDRB with reg+imm offset should be 8 bytes"
12528 );
12529 }
12530
12531 // ========================================================================
12532 // Helium MVE encoding tests
12533 // ========================================================================
12534
12535 #[test]
12536 fn test_encode_mve_addi32_thumb2() {
12537 let encoder = ArmEncoder::new_thumb2();
12538 let op = ArmOp::MveAddI {
12539 qd: QReg::Q0,
12540 qn: QReg::Q1,
12541 qm: QReg::Q2,
12542 size: MveSize::S32,
12543 };
12544 let code = encoder.encode(&op).unwrap();
12545 assert_eq!(
12546 code.len(),
12547 4,
12548 "MVE VADD.I32 should be 4 bytes (Thumb-2 32-bit)"
12549 );
12550 }
12551
12552 #[test]
12553 fn test_encode_mve_subi16_thumb2() {
12554 let encoder = ArmEncoder::new_thumb2();
12555 let op = ArmOp::MveSubI {
12556 qd: QReg::Q0,
12557 qn: QReg::Q1,
12558 qm: QReg::Q2,
12559 size: MveSize::S16,
12560 };
12561 let code = encoder.encode(&op).unwrap();
12562 assert_eq!(code.len(), 4, "MVE VSUB.I16 should be 4 bytes");
12563 }
12564
12565 #[test]
12566 fn test_encode_mve_muli8_thumb2() {
12567 let encoder = ArmEncoder::new_thumb2();
12568 let op = ArmOp::MveMulI {
12569 qd: QReg::Q0,
12570 qn: QReg::Q1,
12571 qm: QReg::Q2,
12572 size: MveSize::S8,
12573 };
12574 let code = encoder.encode(&op).unwrap();
12575 assert_eq!(code.len(), 4, "MVE VMUL.I8 should be 4 bytes");
12576 }
12577
12578 #[test]
12579 fn test_encode_mve_bitwise_thumb2() {
12580 let encoder = ArmEncoder::new_thumb2();
12581
12582 let ops = vec![
12583 ArmOp::MveAnd {
12584 qd: QReg::Q0,
12585 qn: QReg::Q1,
12586 qm: QReg::Q2,
12587 },
12588 ArmOp::MveOrr {
12589 qd: QReg::Q0,
12590 qn: QReg::Q1,
12591 qm: QReg::Q2,
12592 },
12593 ArmOp::MveEor {
12594 qd: QReg::Q0,
12595 qn: QReg::Q1,
12596 qm: QReg::Q2,
12597 },
12598 ArmOp::MveBic {
12599 qd: QReg::Q0,
12600 qn: QReg::Q1,
12601 qm: QReg::Q2,
12602 },
12603 ];
12604 for op in ops {
12605 let code = encoder.encode(&op).unwrap();
12606 assert_eq!(code.len(), 4, "MVE bitwise op should be 4 bytes");
12607 }
12608 }
12609
12610 #[test]
12611 fn test_encode_mve_mvn_thumb2() {
12612 let encoder = ArmEncoder::new_thumb2();
12613 let op = ArmOp::MveMvn {
12614 qd: QReg::Q0,
12615 qm: QReg::Q1,
12616 };
12617 let code = encoder.encode(&op).unwrap();
12618 assert_eq!(code.len(), 4, "MVE VMVN should be 4 bytes");
12619 }
12620
12621 #[test]
12622 fn test_encode_mve_load_store_thumb2() {
12623 let encoder = ArmEncoder::new_thumb2();
12624
12625 let load = ArmOp::MveLoad {
12626 qd: QReg::Q0,
12627 addr: MemAddr::imm(Reg::R0, 16),
12628 };
12629 let code = encoder.encode(&load).unwrap();
12630 assert_eq!(code.len(), 4, "MVE VLDRW.32 should be 4 bytes");
12631
12632 let store = ArmOp::MveStore {
12633 qd: QReg::Q1,
12634 addr: MemAddr::imm(Reg::R1, 0),
12635 };
12636 let code = encoder.encode(&store).unwrap();
12637 assert_eq!(code.len(), 4, "MVE VSTRW.32 should be 4 bytes");
12638 }
12639
12640 #[test]
12641 fn test_encode_mve_const_thumb2() {
12642 let encoder = ArmEncoder::new_thumb2();
12643 let op = ArmOp::MveConst {
12644 qd: QReg::Q0,
12645 bytes: [1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0],
12646 };
12647 let code = encoder.encode(&op).unwrap();
12648 // Should be 4 words of (MOVW R12 + VMOV Sn) = 4 * (4+4) = 32 bytes min
12649 // Some words with hi16=0 skip MOVT, so length varies
12650 assert!(
12651 code.len() >= 24,
12652 "MVE const should produce multiple instructions"
12653 );
12654 }
12655
12656 #[test]
12657 fn test_encode_mve_dup_thumb2() {
12658 let encoder = ArmEncoder::new_thumb2();
12659 let op = ArmOp::MveDup {
12660 qd: QReg::Q0,
12661 rn: Reg::R0,
12662 size: MveSize::S32,
12663 };
12664 let code = encoder.encode(&op).unwrap();
12665 assert_eq!(code.len(), 4, "MVE VDUP.32 should be 4 bytes");
12666 }
12667
12668 #[test]
12669 fn test_encode_mve_extract_lane_thumb2() {
12670 let encoder = ArmEncoder::new_thumb2();
12671 let op = ArmOp::MveExtractLane {
12672 rd: Reg::R0,
12673 qn: QReg::Q1,
12674 lane: 2,
12675 size: MveSize::S32,
12676 };
12677 let code = encoder.encode(&op).unwrap();
12678 assert_eq!(code.len(), 4, "MVE extract lane should be 4 bytes");
12679 }
12680
12681 #[test]
12682 fn test_encode_mve_insert_lane_thumb2() {
12683 let encoder = ArmEncoder::new_thumb2();
12684 let op = ArmOp::MveInsertLane {
12685 qd: QReg::Q0,
12686 rn: Reg::R1,
12687 lane: 3,
12688 size: MveSize::S32,
12689 };
12690 let code = encoder.encode(&op).unwrap();
12691 assert_eq!(code.len(), 4, "MVE insert lane should be 4 bytes");
12692 }
12693
12694 #[test]
12695 fn test_encode_mve_addf32_thumb2() {
12696 let encoder = ArmEncoder::new_thumb2();
12697 let op = ArmOp::MveAddF32 {
12698 qd: QReg::Q0,
12699 qn: QReg::Q1,
12700 qm: QReg::Q2,
12701 };
12702 let code = encoder.encode(&op).unwrap();
12703 assert_eq!(code.len(), 4, "MVE VADD.F32 should be 4 bytes");
12704 }
12705
12706 #[test]
12707 fn test_encode_mve_divf32_thumb2() {
12708 let encoder = ArmEncoder::new_thumb2();
12709 let op = ArmOp::MveDivF32 {
12710 qd: QReg::Q0,
12711 qn: QReg::Q1,
12712 qm: QReg::Q2,
12713 };
12714 let code = encoder.encode(&op).unwrap();
12715 // Lane-wise: 4 x VDIV.F32 = 4 x 4 = 16 bytes
12716 assert_eq!(
12717 code.len(),
12718 16,
12719 "MVE VDIV.F32 (lane-wise) should be 16 bytes"
12720 );
12721 }
12722
12723 #[test]
12724 fn test_encode_mve_sqrtf32_thumb2() {
12725 let encoder = ArmEncoder::new_thumb2();
12726 let op = ArmOp::MveSqrtF32 {
12727 qd: QReg::Q0,
12728 qm: QReg::Q1,
12729 };
12730 let code = encoder.encode(&op).unwrap();
12731 // Lane-wise: 4 x VSQRT.F32 = 4 x 4 = 16 bytes
12732 assert_eq!(
12733 code.len(),
12734 16,
12735 "MVE VSQRT.F32 (lane-wise) should be 16 bytes"
12736 );
12737 }
12738
12739 #[test]
12740 fn test_encode_mve_negf32_thumb2() {
12741 let encoder = ArmEncoder::new_thumb2();
12742 let op = ArmOp::MveNegF32 {
12743 qd: QReg::Q0,
12744 qm: QReg::Q1,
12745 };
12746 let code = encoder.encode(&op).unwrap();
12747 assert_eq!(code.len(), 4, "MVE VNEG.F32 should be 4 bytes");
12748 }
12749
12750 #[test]
12751 fn test_encode_mve_absf32_thumb2() {
12752 let encoder = ArmEncoder::new_thumb2();
12753 let op = ArmOp::MveAbsF32 {
12754 qd: QReg::Q0,
12755 qm: QReg::Q1,
12756 };
12757 let code = encoder.encode(&op).unwrap();
12758 assert_eq!(code.len(), 4, "MVE VABS.F32 should be 4 bytes");
12759 }
12760
12761 /// VCR-RA-001 / immediate-folding precondition: pins the Thumb-2 `AND`
12762 /// immediate encoding for the byte range and documents its bound.
12763 ///
12764 /// The `And { Operand2::Imm }` encoder packs the low 12 bits straight into
12765 /// the `i:imm3:imm8` field WITHOUT applying ThumbExpandImm (the modified-
12766 /// immediate expansion). For `imm <= 0xFF` (e.g. gale's int8 clamps
12767 /// `#0x7e` / `#0x7f`) that is correct — `i:imm3 = 0000` means "imm8
12768 /// zero-extended". So `and r2, r0, #0x7e` encodes to the canonical
12769 /// `00 f0 7e 02`. For `imm >= 0x100` the field would need a true
12770 /// ThumbExpandImm pattern (rotation / replication), which is NOT
12771 /// implemented here — so **immediate folding must gate on `imm <= 0xFF`**
12772 /// until the encoder is hardened to ThumbExpandImm/Ok-or-Err (the
12773 /// "encoder must be Ok-or-Err, never silently wrong" principle, #180/#185).
12774 /// This bound covers the measured `flat_flight` waste (#209).
12775 #[test]
12776 fn and_immediate_encodes_correctly_in_byte_range_documents_fold_bound() {
12777 let encoder = ArmEncoder::new_thumb2();
12778 let op = ArmOp::And {
12779 rd: Reg::R2,
12780 rn: Reg::R0,
12781 op2: Operand2::Imm(0x7e),
12782 };
12783 let code = encoder.encode(&op).unwrap();
12784 assert_eq!(
12785 code,
12786 vec![0x00, 0xf0, 0x7e, 0x02],
12787 "and r2, r0, #0x7e must encode to the canonical AND.W T1 (imm8=0x7e)"
12788 );
12789 }
12790
12791 /// #255: the shared ThumbExpandImm reverse-encoder underpinning the
12792 /// data-processing immediate fix. Encodable modified immediates round-trip to
12793 /// the expected `i:imm3:imm8` field; a genuinely non-modified value is `None`
12794 /// (caller must materialize into a register). Note `1000 = 0xFA ror 30` *is*
12795 /// representable (field 0xF7A) — the old encoder mis-encoded it (raw 0x3E8);
12796 /// this encodes it correctly.
12797 #[test]
12798 fn try_thumb_expand_imm_encodes_modified_immediates() {
12799 assert_eq!(try_thumb_expand_imm(0x7e), Some(0x07e)); // zero-extended byte
12800 assert_eq!(try_thumb_expand_imm(0xff), Some(0x0ff));
12801 assert_eq!(try_thumb_expand_imm(0x0001_0001), Some(0x101)); // 0x00XY00XY
12802 assert_eq!(try_thumb_expand_imm(0xff00_ff00), Some(0x2ff)); // 0xXY00XY00
12803 assert_eq!(try_thumb_expand_imm(0xffff_ffff), Some(0x3ff)); // 0xXYXYXYXY
12804 assert_eq!(try_thumb_expand_imm(0x100), Some(0xf80)); // 0x80 ror 31
12805 assert_eq!(try_thumb_expand_imm(0x8000_0000), Some(0x400)); // 0x80 ror 8
12806 assert_eq!(try_thumb_expand_imm(1000), Some(0xf7a)); // 0xFA ror 30
12807 // Genuinely unrepresentable (bits too far apart for an 8-bit window).
12808 assert_eq!(try_thumb_expand_imm(0x101), None);
12809 assert_eq!(try_thumb_expand_imm(0x12345), None);
12810 }
12811
12812 /// #255: CMP/ADDS/SUBS encode any valid modified immediate correctly, and
12813 /// ERROR (not silently mis-encode) on a genuinely unrepresentable one,
12814 /// forcing the selector to materialize into a register — closing the
12815 /// silent-miscompile class of #251/#253.
12816 #[test]
12817 fn cmp_adds_subs_immediate_error_on_non_modified_imm() {
12818 let encoder = ArmEncoder::new_thumb2();
12819 // cmp r0, #0xff → valid → Ok; cmp r0, #1000 → valid (0xFA ror 30) → Ok.
12820 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 0xff).is_ok());
12821 assert!(encoder.encode_thumb32_cmp_imm(&Reg::R0, 1000).is_ok());
12822 // cmp r0, #0x101 → NOT a modified immediate → Err (materialize-reg).
12823 assert!(
12824 encoder.encode_thumb32_cmp_imm(&Reg::R0, 0x101).is_err(),
12825 "cmp #0x101 must error, not compare the wrong constant"
12826 );
12827 assert!(
12828 encoder
12829 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x101)
12830 .is_err()
12831 );
12832 assert!(
12833 encoder
12834 .encode_thumb32_subs(&Reg::R0, &Reg::R0, 0x101)
12835 .is_err()
12836 );
12837 // ...but a valid modified immediate still encodes.
12838 assert!(
12839 encoder
12840 .encode_thumb32_adds(&Reg::R0, &Reg::R0, 0x80)
12841 .is_ok()
12842 );
12843 }
12844
12845 /// #257: MLA (multiply-accumulate) encodes as MLS without the bit-4 op flag.
12846 /// `mla r2, r3, r4, r8` (rd=r2, rn=r3, rm=r4, ra=r8) → Thumb-2 `03 fb 04 82`.
12847 #[test]
12848 fn mla_thumb2_encodes_correctly() {
12849 let encoder = ArmEncoder::new_thumb2();
12850 let code = encoder
12851 .encode(&ArmOp::Mla {
12852 rd: Reg::R2,
12853 rn: Reg::R3,
12854 rm: Reg::R4,
12855 ra: Reg::R8,
12856 })
12857 .unwrap();
12858 // hw1 = 0xFB03, hw2 = (8<<12)|(2<<8)|4 = 0x8204
12859 assert_eq!(code, vec![0x03, 0xfb, 0x04, 0x82]);
12860 }
12861
12862 /// #259: LDR/STR (and sub-word) immediate-offset encoders truncated
12863 /// `offset & 0xFFF`, silently targeting the wrong address for offset >= 4096.
12864 /// They now error (the selector must use register-offset addressing) — the
12865 /// load/store sibling of the #253/#255 class. Offsets <= 4095 still encode.
12866 #[test]
12867 fn ldst_imm12_offset_errors_when_out_of_range() {
12868 let encoder = ArmEncoder::new_thumb2();
12869 // offset 0xFFF (4095): valid → Ok; ldr r0, [r1, #4095].
12870 assert!(
12871 encoder
12872 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0xFFF)
12873 .is_ok()
12874 );
12875 // offset 0x1000 (4096): out of imm12 range → Err (not & 0xFFF → #0).
12876 assert!(
12877 encoder
12878 .encode_thumb32_ldr(&Reg::R0, &Reg::R1, 0x1000)
12879 .is_err(),
12880 "ldr offset 4096 must error, not wrap to 0"
12881 );
12882 assert!(
12883 encoder
12884 .encode_thumb32_str(&Reg::R0, &Reg::R1, 0x1000)
12885 .is_err()
12886 );
12887 assert!(
12888 encoder
12889 .encode_thumb32_ldrb_imm(&Reg::R0, &Reg::R1, 5000)
12890 .is_err()
12891 );
12892 assert!(
12893 encoder
12894 .encode_thumb32_strh_imm(&Reg::R0, &Reg::R1, 5000)
12895 .is_err()
12896 );
12897 }
12898
12899 /// Latent miscompile fix: ADD/SUB with a >0xFF immediate (e.g.
12900 /// `add sp, sp, #frame` for a >=256-byte frame) used ADD.W (T3), whose
12901 /// `i:imm3:imm8` is a ThumbExpandImm modified immediate — so `#256` silently
12902 /// encoded as `#0` (stack corruption). Use ADDW/SUBW (T4), a PLAIN 12-bit
12903 /// immediate, for 0x100..=0xFFF; keep T3 for <=0xFF (bit-identical); error
12904 /// beyond 4095.
12905 #[test]
12906 fn add_sub_large_immediate_use_addw_subw_not_misencoded() {
12907 let encoder = ArmEncoder::new_thumb2();
12908 // add sp, sp, #256 → ADDW (T4) SP, SP, #256 = 0d f2 00 1d
12909 assert_eq!(
12910 encoder
12911 .encode(&ArmOp::Add {
12912 rd: Reg::SP,
12913 rn: Reg::SP,
12914 op2: Operand2::Imm(256),
12915 })
12916 .unwrap(),
12917 vec![0x0d, 0xf2, 0x00, 0x1d],
12918 "add sp,sp,#256 must be ADDW (plain imm12), not a mis-encoded ADD.W"
12919 );
12920 // sub sp, sp, #256 → SUBW (T4) SP, SP, #256 = ad f2 00 1d
12921 assert_eq!(
12922 encoder
12923 .encode(&ArmOp::Sub {
12924 rd: Reg::SP,
12925 rn: Reg::SP,
12926 op2: Operand2::Imm(256),
12927 })
12928 .unwrap(),
12929 vec![0xad, 0xf2, 0x00, 0x1d],
12930 );
12931 // > 4095 has no single-instruction encoding → error, not silent wrong.
12932 assert!(
12933 encoder
12934 .encode(&ArmOp::Add {
12935 rd: Reg::SP,
12936 rn: Reg::SP,
12937 op2: Operand2::Imm(5000),
12938 })
12939 .is_err(),
12940 "add #5000 must error (no single ADDW), not mis-encode"
12941 );
12942 }
12943
12944 /// Closes the data-proc immediate class: AND and CMN now go through
12945 /// `try_thumb_expand_imm` like ORR/EOR/CMP — correct for any modified
12946 /// immediate, `Err` (not raw-pack / NOP) on an un-encodable one. The byte
12947 /// range stays bit-identical (`and r2,r0,#0x7e` is unchanged).
12948 #[test]
12949 fn and_cmn_immediate_thumb_expand_else_error() {
12950 let encoder = ArmEncoder::new_thumb2();
12951 // byte range unchanged (bit-identical with the pre-retrofit encoding)
12952 assert_eq!(
12953 encoder
12954 .encode(&ArmOp::And {
12955 rd: Reg::R2,
12956 rn: Reg::R0,
12957 op2: Operand2::Imm(0x7e),
12958 })
12959 .unwrap(),
12960 vec![0x00, 0xf0, 0x7e, 0x02],
12961 );
12962 // a valid replicated modified immediate now encodes (was silently wrong)
12963 assert!(
12964 encoder
12965 .encode(&ArmOp::And {
12966 rd: Reg::R2,
12967 rn: Reg::R0,
12968 op2: Operand2::Imm(0xff00ff00u32 as i32),
12969 })
12970 .is_ok()
12971 );
12972 // a genuinely un-encodable immediate errors (AND was raw-pack; CMN NOP)
12973 assert!(
12974 encoder
12975 .encode(&ArmOp::And {
12976 rd: Reg::R2,
12977 rn: Reg::R0,
12978 op2: Operand2::Imm(0x101),
12979 })
12980 .is_err()
12981 );
12982 assert!(
12983 encoder
12984 .encode(&ArmOp::Cmn {
12985 rn: Reg::R0,
12986 op2: Operand2::Imm(0x101),
12987 })
12988 .is_err(),
12989 "CMN #0x101 must error, not emit a NOP"
12990 );
12991 }
12992
12993 /// VCR-RA-001: ORR/EOR with a small immediate must encode the real
12994 /// instruction (not a silent `0xBF00` NOP). Pins the byte range and the
12995 /// Ok-or-Err bound that makes future Or/Eor immediate folding safe.
12996 #[test]
12997 fn orr_eor_immediate_encode_in_byte_range_else_error() {
12998 let encoder = ArmEncoder::new_thumb2();
12999 // orr r2, r0, #0x7e → ORR.W T1, imm8=0x7e
13000 assert_eq!(
13001 encoder
13002 .encode(&ArmOp::Orr {
13003 rd: Reg::R2,
13004 rn: Reg::R0,
13005 op2: Operand2::Imm(0x7e),
13006 })
13007 .unwrap(),
13008 vec![0x40, 0xf0, 0x7e, 0x02],
13009 );
13010 // eor r2, r0, #0x7e → EOR.W T1, imm8=0x7e
13011 assert_eq!(
13012 encoder
13013 .encode(&ArmOp::Eor {
13014 rd: Reg::R2,
13015 rn: Reg::R0,
13016 op2: Operand2::Imm(0x7e),
13017 })
13018 .unwrap(),
13019 vec![0x80, 0xf0, 0x7e, 0x02],
13020 );
13021 // Out-of-range immediates error rather than silently mis-encode / NOP.
13022 assert!(
13023 encoder
13024 .encode(&ArmOp::Orr {
13025 rd: Reg::R2,
13026 rn: Reg::R0,
13027 op2: Operand2::Imm(0x140),
13028 })
13029 .is_err(),
13030 "ORR #0x140 must error, not emit a NOP"
13031 );
13032 }
13033
13034 #[test]
13035 fn test_encode_mve_different_qregs() {
13036 let encoder = ArmEncoder::new_thumb2();
13037
13038 // Test that different Q-register numbers produce different encodings
13039 let op1 = ArmOp::MveAddI {
13040 qd: QReg::Q0,
13041 qn: QReg::Q0,
13042 qm: QReg::Q0,
13043 size: MveSize::S32,
13044 };
13045 let op2 = ArmOp::MveAddI {
13046 qd: QReg::Q3,
13047 qn: QReg::Q5,
13048 qm: QReg::Q7,
13049 size: MveSize::S32,
13050 };
13051 let code1 = encoder.encode(&op1).unwrap();
13052 let code2 = encoder.encode(&op2).unwrap();
13053 assert_ne!(
13054 code1, code2,
13055 "Different Q-registers should produce different encodings"
13056 );
13057 }
13058
13059 #[test]
13060 fn test_encode_mve_arm32_loud_err() {
13061 // #615: MVE (Helium) is Thumb-2-only. The ARM32 encoder used to emit
13062 // a silent NOP here (dropping the vector op); it must now be a typed
13063 // Err so a broken "MVE implies Thumb" invariant fails loudly.
13064 let encoder = ArmEncoder::new_arm32();
13065 let op = ArmOp::MveAddI {
13066 qd: QReg::Q0,
13067 qn: QReg::Q1,
13068 qm: QReg::Q2,
13069 size: MveSize::S32,
13070 };
13071 let err = encoder
13072 .encode(&op)
13073 .expect_err("ARM32 MVE must be a loud Err, not a silent NOP (#615)");
13074 assert!(
13075 err.to_string().contains("Thumb-2 only"),
13076 "unexpected error message: {err}"
13077 );
13078 }
13079}